/** * 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} */ 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} */ 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>} */ 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)}:`, }; }