| 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 |
|
|---|
| 16 | import { test } from 'node:test';
|
|---|
| 17 | import assert from 'node:assert/strict';
|
|---|
| 18 | import crypto from 'crypto';
|
|---|
| 19 |
|
|---|
| 20 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 21 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 22 |
|
|---|
| 23 | const dbMod = await import('../src/config/database.js');
|
|---|
| 24 | const db = dbMod.default;
|
|---|
| 25 | dbMod.initializeDatabase();
|
|---|
| 26 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 27 |
|
|---|
| 28 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 29 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 30 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 31 |
|
|---|
| 32 | const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|---|
| 33 | modulusLength: 2048,
|
|---|
| 34 | publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|---|
| 35 | privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|---|
| 36 | });
|
|---|
| 37 |
|
|---|
| 38 | const ACTOR = 'https://203.0.113.70/users/zender';
|
|---|
| 39 | const KEY_ID = `${ACTOR}#main-key`;
|
|---|
| 40 | // Secure mode: dit actor-document komt alleen op een ondertekend verzoek.
|
|---|
| 41 | const SECURE_ACTOR = 'https://203.0.113.80/users/gesloten';
|
|---|
| 42 | const SECURE_KEY_ID = `${SECURE_ACTOR}#main-key`;
|
|---|
| 43 |
|
|---|
| 44 | const actorDoc = (id) => ({
|
|---|
| 45 | id, type: 'Person', preferredUsername: 'zender', inbox: `${id}/inbox`,
|
|---|
| 46 | publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: publicKey },
|
|---|
| 47 | });
|
|---|
| 48 |
|
|---|
| 49 | const echteFetch = globalThis.fetch;
|
|---|
| 50 | globalThis.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. */
|
|---|
| 61 | function 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 |
|
|---|
| 87 | test('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 |
|
|---|
| 93 | test('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 |
|
|---|
| 99 | test('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 |
|
|---|
| 107 | test('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 |
|
|---|
| 113 | test('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 |
|
|---|
| 118 | test('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 |
|
|---|
| 127 | test('geen handtekening → null, en geen enkele ophaalpoging', async () => {
|
|---|
| 128 | assert.equal(await AP.verifyRequest({ method: 'POST', originalUrl: '/x', headers: {} }, 'me'), null);
|
|---|
| 129 | });
|
|---|
| 130 |
|
|---|
| 131 | test.after(() => { globalThis.fetch = echteFetch; });
|
|---|