~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
83 lines · 2.8 KB Raw History
// 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<boolean>}} [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<boolean>}
 */
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),
  );
}