/**
* 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')
}
}