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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
135 lines · 3.9 KB Raw History
/**
 * The HTTP message signature a space credential travels with (RFC 9421).
 *
 * A credential is bound to an ephemeral P-256 key generated for one helper
 * invocation and never stored. The exchange names the key, and the authority
 * binds the credential to it. Each read then signs the credential together
 * with the DID the read is addressed to, under the label and tag
 * `atproto-space-credential`.
 *
 * Self-contained on WebCrypto because this package installs as a global CLI
 * and keeps its dependency list empty.
 */

const LABEL = 'atproto-space-credential';
const SIGNATURE_INPUT =
  '("authorization" "atproto-space-audience");tag="atproto-space-credential"';
const P256_N =
  0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

/**
 * @typedef {Object} SpaceSigKey
 * @property {string} did - the public key as a P-256 did:key
 * @property {CryptoKey} privateKey
 */

/**
 * @param {Uint8Array} bytes
 * @returns {string}
 */
export function base58btc(bytes) {
  const digits = [0];
  for (const byte of bytes) {
    let carry = byte;
    for (let i = 0; i < digits.length; i++) {
      carry += digits[i] << 8;
      digits[i] = carry % 58;
      carry = (carry / 58) | 0;
    }
    while (carry) {
      digits.push(carry % 58);
      carry = (carry / 58) | 0;
    }
  }
  let out = '';
  for (const byte of bytes) {
    if (byte !== 0) break;
    out += '1';
  }
  for (let i = digits.length - 1; i >= 0; i--) out += B58[digits[i]];
  return out;
}

/** @returns {Promise<SpaceSigKey>} */
export async function generateSpaceSigKey() {
  const pair = /** @type {CryptoKeyPair} */ (
    await crypto.subtle.generateKey(
      { name: 'ECDSA', namedCurve: 'P-256' },
      false,
      ['sign', 'verify'],
    )
  );
  // A did:key carries the compressed point behind the p256-pub multicodec
  // prefix (0x1200, varint 0x80 0x24).
  const raw = new Uint8Array(
    await crypto.subtle.exportKey('raw', pair.publicKey),
  );
  return {
    did: `did:key:z${base58btc(new Uint8Array([0x80, 0x24, ...compressedP256(raw)]))}`,
    privateKey: pair.privateKey,
  };
}

/**
 * The compressed form of an uncompressed P-256 point (0x04 || x || y).
 * @param {Uint8Array} raw
 * @returns {Uint8Array}
 */
export function compressedP256(raw) {
  const compressed = new Uint8Array(33);
  compressed[0] = raw[64] & 1 ? 0x03 : 0x02;
  compressed.set(raw.subarray(1, 33), 1);
  return compressed;
}

/**
 * The raw 64-byte r || s, normalized to low-S. The reference accepts either;
 * a stricter verifier accepts only low-S.
 * @param {CryptoKey} privateKey
 * @param {Uint8Array} data
 * @returns {Promise<Uint8Array>}
 */
async function signLowS(privateKey, data) {
  const sig = new Uint8Array(
    await crypto.subtle.sign(
      { name: 'ECDSA', hash: 'SHA-256' },
      privateKey,
      /** @type {BufferSource} */ (data),
    ),
  );
  let s = 0n;
  for (const b of sig.subarray(32)) s = (s << 8n) | BigInt(b);
  if (s > P256_N / 2n) {
    let low = P256_N - s;
    for (let i = 63; i >= 32; i--) {
      sig[i] = Number(low & 0xffn);
      low >>= 8n;
    }
  }
  return sig;
}

/**
 * Authorization, audience and signature headers for a request presenting a
 * space credential.
 * @param {SpaceSigKey} key
 * @param {{authorization: string, audience: string}} opts
 * @returns {Promise<Record<string, string>>}
 */
export async function createSpaceSigHeaders(key, { authorization, audience }) {
  const base = [
    `"authorization": ${authorization}`,
    `"atproto-space-audience": ${audience}`,
    `"@signature-params": ${SIGNATURE_INPUT}`,
  ].join('\n');
  const sig = await signLowS(key.privateKey, new TextEncoder().encode(base));
  let binary = '';
  for (const b of sig) binary += String.fromCharCode(b);
  return {
    Authorization: authorization,
    'Atproto-Space-Audience': audience,
    'Signature-Input': `${LABEL}=${SIGNATURE_INPUT}`,
    Signature: `${LABEL}=:${btoa(binary)}:`,
  };
}