Index: test/signature-verification.test.js
===================================================================
--- test/signature-verification.test.js	(revision f4f0a6d2b553fe600016875f0c5789076851f2a1)
+++ test/signature-verification.test.js	(revision f4f0a6d2b553fe600016875f0c5789076851f2a1)
@@ -0,0 +1,131 @@
+// verifyRequest met een ECHTE handtekening.
+//
+// Dit ontbrak, en dat is hoe het hoort te heten: de hele testsuite maakte
+// nergens een handtekening aan. De inbox-tests injecteren preVerified en slaan
+// verifyRequest dus over; de authorized-fetch-tests raken alleen fetchActor.
+// Ondertussen is het sleutel-ophaalpad op 2026-08-06 twee keer verbouwd
+// (9561d58 ondertekenen bij secure mode, efe5633 onbetekend eerst) zonder één
+// test die aantoont dat verificatie nog SLAAGT.
+//
+// Hier wordt een echt RSA-paar gemaakt, het actor-document met die publieke
+// sleutel geserveerd via de gestubde fetch, en een verzoek ondertekend zoals
+// een fediverse-server het doet.
+//
+// Run: npm test
+
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import crypto from 'crypto';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = await import('../src/services/ActivityPubService.js');
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@test', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
+
+const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
+  modulusLength: 2048,
+  publicKeyEncoding: { type: 'spki', format: 'pem' },
+  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+});
+
+const ACTOR = 'https://203.0.113.70/users/zender';
+const KEY_ID = `${ACTOR}#main-key`;
+// Secure mode: dit actor-document komt alleen op een ondertekend verzoek.
+const SECURE_ACTOR = 'https://203.0.113.80/users/gesloten';
+const SECURE_KEY_ID = `${SECURE_ACTOR}#main-key`;
+
+const actorDoc = (id) => ({
+  id, type: 'Person', preferredUsername: 'zender', inbox: `${id}/inbox`,
+  publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: publicKey },
+});
+
+const echteFetch = globalThis.fetch;
+globalThis.fetch = async (url, opts = {}) => {
+  const u = String(url);
+  const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
+  const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { 'content-type': 'application/activity+json' } });
+  if (u === ACTOR) return json(actorDoc(ACTOR));
+  if (u === SECURE_ACTOR) return ondertekend ? json(actorDoc(SECURE_ACTOR)) : new Response('unauthorized', { status: 401 });
+  return new Response('not found', { status: 404 });
+};
+
+/** Bouwt een verzoek zoals een fediverse-server het aflevert: ondertekend over
+ *  (request-target), host, date en digest — precies wat verifyRequest eist. */
+function ondertekendVerzoek({ keyId = KEY_ID, body = { type: 'Follow', actor: ACTOR }, date = new Date().toUTCString(), key = privateKey } = {}) {
+  const raw = Buffer.from(JSON.stringify(body));
+  const digest = 'SHA-256=' + crypto.createHash('sha256').update(raw).digest('base64');
+  const host = 'klonkt.test';
+  const target = 'post /ap/users/me/inbox';
+  const headers = '(request-target) host date digest';
+  const line = [
+    `(request-target): ${target}`,
+    `host: ${host}`,
+    `date: ${date}`,
+    `digest: ${digest}`,
+  ].join('\n');
+  const sig = crypto.sign('sha256', Buffer.from(line), key).toString('base64');
+  return {
+    method: 'POST',
+    originalUrl: '/ap/users/me/inbox',
+    rawBody: raw,
+    body,
+    ip: '203.0.113.9',
+    headers: {
+      host, date, digest,
+      signature: `keyId="${keyId}",headers="${headers}",signature="${sig}"`,
+    },
+  };
+}
+
+test('een correct ondertekend verzoek verifieert', async () => {
+  const verified = await AP.verifyRequest(ondertekendVerzoek(), 'me');
+  assert.ok(verified, 'verifyRequest hoort de actor terug te geven');
+  assert.equal(verified.id, ACTOR);
+});
+
+test('en ook zonder aangewezen site (de gedeelde inbox)', async () => {
+  const verified = await AP.verifyRequest(ondertekendVerzoek());
+  assert.ok(verified);
+  assert.equal(verified.id, ACTOR);
+});
+
+test('een actor achter authorized fetch verifieert ook', async () => {
+  // De combinatie die vandaag tweemaal is verbouwd: de sleutel zit achter secure
+  // mode, dus de ophaal moet ondertekend worden voordat verificatie kan slagen.
+  const verified = await AP.verifyRequest(ondertekendVerzoek({ keyId: SECURE_KEY_ID }), 'me');
+  assert.ok(verified, 'de sleutel hoort ondertekend opgehaald te worden');
+  assert.equal(verified.id, SECURE_ACTOR);
+});
+
+test('een verdraaid lichaam breekt de handtekening', async () => {
+  const req = ondertekendVerzoek();
+  req.rawBody = Buffer.from(JSON.stringify({ type: 'Follow', actor: 'https://kwaad.test/users/x' }));
+  assert.equal(await AP.verifyRequest(req, 'me'), null);
+});
+
+test('een oude Date wordt geweigerd (replay)', async () => {
+  const oud = new Date(Date.now() - 25 * 60 * 60 * 1000).toUTCString();
+  assert.equal(await AP.verifyRequest(ondertekendVerzoek({ date: oud }), 'me'), null);
+});
+
+test('ondertekend met een ANDERE sleutel dan de actor publiceert → null', async () => {
+  const ander = crypto.generateKeyPairSync('rsa', {
+    modulusLength: 2048,
+    publicKeyEncoding: { type: 'spki', format: 'pem' },
+    privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
+  });
+  assert.equal(await AP.verifyRequest(ondertekendVerzoek({ key: ander.privateKey }), 'me'), null);
+});
+
+test('geen handtekening → null, en geen enkele ophaalpoging', async () => {
+  assert.equal(await AP.verifyRequest({ method: 'POST', originalUrl: '/x', headers: {} }, 'me'), null);
+});
+
+test.after(() => { globalThis.fetch = echteFetch; });
