OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / auth / keet-verifier.js

109 lines · javascript ref:
/**
 * Keet device-chain verifier (Phase 2b).
 *
 * Plugs into verifyEnvelope's `opts.keetVerifier` hook. When the envelope
 * carries a `chain` field, we verify the chain end-to-end via
 * keet-identity-key and assert that the device pubkey attested by the
 * chain is the same pubkey that signed this envelope. On success the
 * authenticated identity becomes the chain's root, not the device.
 *
 * Wire shape of `envelope.chain`:
 *   base64(deviceProofBuffer)  — the raw buffer returned by
 *                                IdentityKey.bootstrap() or
 *                                IdentityKey.attestDevice()
 *
 * Why a separate module from envelope.js:
 *   envelope.js is pure (no keet-identity-key dep). Daemons that don't
 *   need keet chains never load the library. This file is the thin
 *   glue layer that pulls the dep in only when chains are wanted.
 */

import IdentityKey from 'keet-identity-key'
import b4a from 'b4a'

/**
 * Create a keet chain verifier suitable for verifyEnvelope's
 * `opts.keetVerifier`. The returned function is synchronous because
 * keet-identity-key's verify() is synchronous.
 *
 * @returns {(chainB64: string, devicePubkeyB64: string) => { rootPubkey: string }}
 *   rootPubkey is returned as hex (64 chars) to line up with the
 *   rest of pear-git-v3's IdentityRegistry which keys identities by
 *   hex-encoded pubkeys.
 */
export function createKeetVerifier () {
  return function keetVerifier (chainB64, devicePubkeyB64) {
    if (typeof chainB64 !== 'string' || chainB64.length === 0) {
      throw new Error('chain is required')
    }
    if (typeof devicePubkeyB64 !== 'string' || devicePubkeyB64.length === 0) {
      throw new Error('devicePubkeyB64 is required')
    }

    let proof
    try {
      proof = b4a.from(chainB64, 'base64')
    } catch (err) {
      throw new Error('chain is not valid base64: ' + err.message)
    }
    if (!proof || proof.length === 0) {
      throw new Error('chain decoded to empty buffer')
    }

    // keet-identity-key returns { identityPublicKey, devicePublicKey, ... }
    // on success, or null/false on failure.
    const result = IdentityKey.verify(proof)
    if (!result) throw new Error('chain failed keet verification')

    const { identityPublicKey, devicePublicKey } = result
    if (!identityPublicKey || !devicePublicKey) {
      throw new Error('chain missing identity or device pubkey')
    }

    // Bind the chain to the envelope signer. The chain says "device X
    // belongs to identity Y". The envelope was signed by some pubkey.
    // Unless X === envelope's signer, the chain doesn't prove anything
    // about this call.
    const chainDeviceB64 = b4a.toString(devicePublicKey, 'base64')
    if (chainDeviceB64 !== devicePubkeyB64) {
      throw new Error('chain device pubkey does not match envelope signer')
    }

    return {
      rootPubkey: b4a.toString(identityPublicKey, 'hex')
    }
  }
}

/**
 * Test helper: build a fresh { anKeypair, chainB64, rootHex, deviceHex }
 * tuple — a brand-new keet identity with one attested device. Use it in
 * tests that want to verify chain-bearing envelopes end to end, without
 * each test file importing keet-identity-key directly.
 *
 * @param {object} [opts]
 * @param {object} [opts.crypto] — hypercore-crypto (or compatible),
 *                                 defaults to dynamic import
 * @returns {Promise<{ anKeypair: string, chainB64: string, rootHex: string, deviceHex: string }>}
 */
export async function createTestChain (opts = {}) {
  const cryptoLib = opts.crypto || (await import('hypercore-crypto')).default

  const mnemonic = IdentityKey.generateMnemonic()
  const identityKey = await IdentityKey.from({ mnemonic })

  const deviceKp = cryptoLib.keyPair()
  const proof = await identityKey.bootstrap(deviceKp.publicKey)

  const chainB64 = b4a.toString(proof, 'base64')
  const anKeypair = b4a.toString(deviceKp.publicKey, 'base64') +
                    b4a.toString(deviceKp.secretKey, 'base64')

  return {
    anKeypair,
    chainB64,
    rootHex: b4a.toString(identityKey.identityKeyPair.publicKey, 'hex'),
    deviceHex: b4a.toString(deviceKp.publicKey, 'hex')
  }
}