chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* 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)}:`,
};
}