source: Klonkt/test/signature-verification.test.js

main
Last change on this file was f4f0a6d, checked in by roboburr <roboburr@…>, 5 weeks ago

Test die een echte handtekening maakt en verifieert

De suite maakte nergens een HTTP-handtekening aan: de inbox-tests injecteren
preVerified en slaan verifyRequest over, de authorized-fetch-tests raken alleen
fetchActor. Ondertussen is het sleutel-ophaalpad vandaag tweemaal verbouwd
(9561d58, efe5633) zonder dat iets aantoonde dat verificatie nog slaagt.

Deze test genereert een RSA-paar, serveert het actor-document via de gestubde
fetch en ondertekent een verzoek zoals een fediverse-server dat doet. Ook de
combinatie met authorized fetch, plus de weigeringen: verdraaid lichaam, oude
Date, verkeerde sleutel, geen handtekening.

Gecontroleerd dat de test bijt: haal de ondertekende terugval uit fetchActor en
geval 3 valt om.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 5.5 KB
Line 
1// verifyRequest met een ECHTE handtekening.
2//
3// Dit ontbrak, en dat is hoe het hoort te heten: de hele testsuite maakte
4// nergens een handtekening aan. De inbox-tests injecteren preVerified en slaan
5// verifyRequest dus over; de authorized-fetch-tests raken alleen fetchActor.
6// Ondertussen is het sleutel-ophaalpad op 2026-08-06 twee keer verbouwd
7// (9561d58 ondertekenen bij secure mode, efe5633 onbetekend eerst) zonder één
8// test die aantoont dat verificatie nog SLAAGT.
9//
10// Hier wordt een echt RSA-paar gemaakt, het actor-document met die publieke
11// sleutel geserveerd via de gestubde fetch, en een verzoek ondertekend zoals
12// een fediverse-server het doet.
13//
14// Run: npm test
15
16import { test } from 'node:test';
17import assert from 'node:assert/strict';
18import crypto from 'crypto';
19
20process.env.DATABASE_PATH = ':memory:';
21process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
22
23const dbMod = await import('../src/config/database.js');
24const db = dbMod.default;
25dbMod.initializeDatabase();
26const AP = await import('../src/services/ActivityPubService.js');
27
28db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
29 .run('u1', 'u1', 'u1@test', 'x', 'god');
30db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
31
32const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
33 modulusLength: 2048,
34 publicKeyEncoding: { type: 'spki', format: 'pem' },
35 privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
36});
37
38const ACTOR = 'https://203.0.113.70/users/zender';
39const KEY_ID = `${ACTOR}#main-key`;
40// Secure mode: dit actor-document komt alleen op een ondertekend verzoek.
41const SECURE_ACTOR = 'https://203.0.113.80/users/gesloten';
42const SECURE_KEY_ID = `${SECURE_ACTOR}#main-key`;
43
44const actorDoc = (id) => ({
45 id, type: 'Person', preferredUsername: 'zender', inbox: `${id}/inbox`,
46 publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: publicKey },
47});
48
49const echteFetch = globalThis.fetch;
50globalThis.fetch = async (url, opts = {}) => {
51 const u = String(url);
52 const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
53 const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { 'content-type': 'application/activity+json' } });
54 if (u === ACTOR) return json(actorDoc(ACTOR));
55 if (u === SECURE_ACTOR) return ondertekend ? json(actorDoc(SECURE_ACTOR)) : new Response('unauthorized', { status: 401 });
56 return new Response('not found', { status: 404 });
57};
58
59/** Bouwt een verzoek zoals een fediverse-server het aflevert: ondertekend over
60 * (request-target), host, date en digest — precies wat verifyRequest eist. */
61function ondertekendVerzoek({ keyId = KEY_ID, body = { type: 'Follow', actor: ACTOR }, date = new Date().toUTCString(), key = privateKey } = {}) {
62 const raw = Buffer.from(JSON.stringify(body));
63 const digest = 'SHA-256=' + crypto.createHash('sha256').update(raw).digest('base64');
64 const host = 'klonkt.test';
65 const target = 'post /ap/users/me/inbox';
66 const headers = '(request-target) host date digest';
67 const line = [
68 `(request-target): ${target}`,
69 `host: ${host}`,
70 `date: ${date}`,
71 `digest: ${digest}`,
72 ].join('\n');
73 const sig = crypto.sign('sha256', Buffer.from(line), key).toString('base64');
74 return {
75 method: 'POST',
76 originalUrl: '/ap/users/me/inbox',
77 rawBody: raw,
78 body,
79 ip: '203.0.113.9',
80 headers: {
81 host, date, digest,
82 signature: `keyId="${keyId}",headers="${headers}",signature="${sig}"`,
83 },
84 };
85}
86
87test('een correct ondertekend verzoek verifieert', async () => {
88 const verified = await AP.verifyRequest(ondertekendVerzoek(), 'me');
89 assert.ok(verified, 'verifyRequest hoort de actor terug te geven');
90 assert.equal(verified.id, ACTOR);
91});
92
93test('en ook zonder aangewezen site (de gedeelde inbox)', async () => {
94 const verified = await AP.verifyRequest(ondertekendVerzoek());
95 assert.ok(verified);
96 assert.equal(verified.id, ACTOR);
97});
98
99test('een actor achter authorized fetch verifieert ook', async () => {
100 // De combinatie die vandaag tweemaal is verbouwd: de sleutel zit achter secure
101 // mode, dus de ophaal moet ondertekend worden voordat verificatie kan slagen.
102 const verified = await AP.verifyRequest(ondertekendVerzoek({ keyId: SECURE_KEY_ID }), 'me');
103 assert.ok(verified, 'de sleutel hoort ondertekend opgehaald te worden');
104 assert.equal(verified.id, SECURE_ACTOR);
105});
106
107test('een verdraaid lichaam breekt de handtekening', async () => {
108 const req = ondertekendVerzoek();
109 req.rawBody = Buffer.from(JSON.stringify({ type: 'Follow', actor: 'https://kwaad.test/users/x' }));
110 assert.equal(await AP.verifyRequest(req, 'me'), null);
111});
112
113test('een oude Date wordt geweigerd (replay)', async () => {
114 const oud = new Date(Date.now() - 25 * 60 * 60 * 1000).toUTCString();
115 assert.equal(await AP.verifyRequest(ondertekendVerzoek({ date: oud }), 'me'), null);
116});
117
118test('ondertekend met een ANDERE sleutel dan de actor publiceert → null', async () => {
119 const ander = crypto.generateKeyPairSync('rsa', {
120 modulusLength: 2048,
121 publicKeyEncoding: { type: 'spki', format: 'pem' },
122 privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
123 });
124 assert.equal(await AP.verifyRequest(ondertekendVerzoek({ key: ander.privateKey }), 'me'), null);
125});
126
127test('geen handtekening → null, en geen enkele ophaalpoging', async () => {
128 assert.equal(await AP.verifyRequest({ method: 'POST', originalUrl: '/x', headers: {} }, 'me'), null);
129});
130
131test.after(() => { globalThis.fetch = echteFetch; });
Note: See TracBrowser for help on using the repository browser.