// Signature verification for did:key public keys. // // P-256 ships built in on crypto.subtle. secp256k1 is injected: hand-rolling // ECDSA verification is a different risk class from hand-rolling a hash, since // a subtle bug means accepting a forged signature rather than a failing test. // The reference stack verifies secp256k1 with @noble/curves; a deployment that // wants to verify secp256k1 keys (the curve the reference PDS gives every // account) injects an implementation backed by it. Without one, this verifier // interoperates with P-256 keys only. import { base58btcDecode, P256_MULTICODEC, SECP256K1_MULTICODEC, } from './plc.js'; /** * Decode a did:key (or bare multibase) into its curve and public key bytes. * @param {string} didKey - did:key:z… or the z… multibase value alone * @returns {{curve: 'p256'|'secp256k1', publicKey: Uint8Array}} */ export function parseDidKey(didKey) { const value = didKey.startsWith('did:key:') ? didKey.slice(8) : didKey; if (!value.startsWith('z')) { throw new Error(`expected a did:key multibase value, got: ${didKey}`); } const bytes = base58btcDecode(value.slice(1)); if (bytes[0] === P256_MULTICODEC[0] && bytes[1] === P256_MULTICODEC[1]) { return { curve: 'p256', publicKey: bytes.slice(2) }; } if ( bytes[0] === SECP256K1_MULTICODEC[0] && bytes[1] === SECP256K1_MULTICODEC[1] ) { return { curve: 'secp256k1', publicKey: bytes.slice(2) }; } throw new Error(`unsupported did:key multicodec: ${didKey}`); } /** * Build a signature verifier. P-256 is handled with crypto.subtle; secp256k1 * is delegated to an injected implementation, or refused if none was given. * * @param {{secp256k1?: (publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array) => Promise}} [opts] * @returns {import('./ports.js').SignatureVerifierPort} */ export function createVerifier(opts = {}) { return { async verify(didKey, data, sig) { const { curve, publicKey } = parseDidKey(didKey); if (curve === 'p256') return verifyP256(publicKey, data, sig); if (!opts.secp256k1) { throw new Error( 'secp256k1 verification requires an injected verifier; ' + 'this deployment supports P-256 keys only', ); } return opts.secp256k1(publicKey, data, sig); }, }; } /** * @param {Uint8Array} publicKey - 33-byte compressed point * @param {Uint8Array} data * @param {Uint8Array} sig - 64-byte raw r||s * @returns {Promise} */ async function verifyP256(publicKey, data, sig) { const key = await crypto.subtle.importKey( 'raw', /** @type {BufferSource} */ (publicKey), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'], ); return crypto.subtle.verify( { name: 'ECDSA', hash: 'SHA-256' }, key, /** @type {BufferSource} */ (sig), /** @type {BufferSource} */ (data), ); }