Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263import { createHash, createSign, createVerify } from 'node:crypto';
// HTTP signatures as the Fediverse does them (draft-cavage, RSA-SHA256):// enough for a server to fetch actors from instances that require signed// requests. The signing actor's key must be fetchable at `keyId` for the// other side to check it.
export function signatureString({ method, url, headers, signed }) { const u = new URL(url); return signed .map((name) => { if (name === '(request-target)') return `(request-target): ${method.toLowerCase()} ${u.pathname}${u.search}`; const value = headers.get(name); if (value == null) throw new Error(`cannot sign missing header ${name}`); return `${name}: ${value}`; }) .join('\n');}
// Returns the headers to send: the originals plus Host, Date, Digest (when// there is a body), and Signature.export function signRequest({ keyId, privateKeyPem, method = 'GET', url, headers, body }) { const h = new Headers(headers); const u = new URL(url); if (!h.has('host')) h.set('host', u.host); if (!h.has('date')) h.set('date', new Date().toUTCString()); const signed = ['(request-target)', 'host', 'date']; if (body != null) { h.set('digest', `SHA-256=${createHash('sha256').update(body).digest('base64')}`); signed.push('digest'); } const signature = createSign('RSA-SHA256').update(signatureString({ method, url, headers: h, signed })).sign(privateKeyPem, 'base64'); h.set('signature', `keyId="${keyId}",algorithm="rsa-sha256",headers="${signed.join(' ')}",signature="${signature}"`); return h;}
export function parseSignatureHeader(value) { const out = {}; for (const m of String(value ?? '').matchAll(/([a-zA-Z]+)="([^"]*)"/g)) out[m[1]] = m[2]; return out;}
// Check a signed request against a public key. Used by tests here, and by// servers that want to know who is knocking.export function verifyRequest({ publicKeyPem, method, url, headers }) { const h = new Headers(headers); const sig = parseSignatureHeader(h.get('signature')); if (!sig.signature || !sig.headers) return false; const str = signatureString({ method, url, headers: h, signed: sig.headers.split(/\s+/) }); return createVerify('RSA-SHA256').update(str).verify(publicKeyPem, sig.signature, 'base64');}
// A fetch that signs every request as `keyId`. Wrap a hardened fetch with it.export function createSignedFetch({ keyId, privateKeyPem, fetch = globalThis.fetch }) { return async (input, init = {}) => { const url = typeof input === 'string' || input instanceof URL ? String(input) : input.url; const method = init.method ?? (input instanceof Request ? input.method : 'GET'); const body = init.body ?? undefined; const headers = signRequest({ keyId, privateKeyPem, method, url, headers: init.headers ?? (input instanceof Request ? input.headers : undefined), body }); return fetch(input, { ...init, method, headers }); };}