import { test } from 'node:test'; import assert from 'node:assert/strict'; import { generateKeyPairSync } from 'node:crypto'; import { signRequest, verifyRequest, parseSignatureHeader, createSignedFetch } from '../src/httpsig.js'; const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); const keyId = 'https://fedi.example/actor#main-key'; test('signs a GET the way Mastodon expects and verifies', () => { const url = 'https://mastodon.social/users/robin?x=1'; const headers = signRequest({ keyId, privateKeyPem: privateKey, url, headers: { accept: 'application/activity+json' } }); assert.equal(headers.get('host'), 'mastodon.social'); assert.ok(headers.get('date')); assert.equal(headers.get('accept'), 'application/activity+json'); const sig = parseSignatureHeader(headers.get('signature')); assert.equal(sig.keyId, keyId); assert.equal(sig.algorithm, 'rsa-sha256'); assert.equal(sig.headers, '(request-target) host date'); assert.ok(verifyRequest({ publicKeyPem: publicKey, method: 'GET', url, headers })); // tamper headers.set('date', new Date(0).toUTCString()); assert.ok(!verifyRequest({ publicKeyPem: publicKey, method: 'GET', url, headers })); }); test('a body gets a digest that is signed', () => { const url = 'https://fedi.example/users/x/inbox'; const headers = signRequest({ keyId, privateKeyPem: privateKey, method: 'POST', url, body: '{"type":"Follow"}' }); assert.match(headers.get('digest'), /^SHA-256=/); assert.equal(parseSignatureHeader(headers.get('signature')).headers, '(request-target) host date digest'); assert.ok(verifyRequest({ publicKeyPem: publicKey, method: 'POST', url, headers })); assert.ok(!verifyRequest({ publicKeyPem: publicKey, method: 'GET', url, headers })); }); test('createSignedFetch signs what it sends', async () => { let seen; const fetch = createSignedFetch({ keyId, privateKeyPem: privateKey, fetch: async (input, init) => { seen = { input, init }; return new Response('ok'); } }); await fetch('https://mastodon.social/users/robin', { headers: { accept: 'application/activity+json' } }); assert.equal(seen.init.method, 'GET'); assert.ok(verifyRequest({ publicKeyPem: publicKey, method: 'GET', url: 'https://mastodon.social/users/robin', headers: seen.init.headers })); await fetch(new Request('https://mastodon.social/users/robin', { method: 'POST', body: 'x' }), { body: 'x' }); assert.equal(seen.init.method, 'POST'); assert.ok(seen.init.headers.get('digest')); }); test('verifyRequest rejects missing or malformed signatures', () => { assert.ok(!verifyRequest({ publicKeyPem: publicKey, method: 'GET', url: 'https://x.example/', headers: {} })); assert.ok(!verifyRequest({ publicKeyPem: publicKey, method: 'GET', url: 'https://x.example/', headers: { signature: 'keyId="x",headers="date",signature="AAAA"', date: 'now' } })); });