source: Klonkt/test/webfinger-did.test.js

main
Last change on this file was 5d0d3f35, checked in by Robin <roboburr@…>, 4 hours ago

WebFinger op een did:key: welk account zegt dat het deze sleutel is

FEP-c390 zet het identiteitsbewijs op de actor en zegt niets over vinden. Wie
een papieren sleutel vasthoudt (shaer-jpxj) -- bij een verhuizing, of om
iemand na te lopen -- kan nu vragen welk account erbij hoort:

GET /.well-known/webfinger?resource=did:key:z6Mk...

RFC 7033 laat elke URI als resource toe, dus dit is gewoon WebFinger. Het
antwoord is een JRD met per account een self-link naar de actor; 404 als
niemand hier die sleutel draagt.

Alleen wat NU klopt: elke opgeslagen verklaring wordt opnieuw nagelopen tegen
de actor waar ze op staat (een verklaring voor een ander account, of een die
niet meer natelopen is, telt niet), en alleen openbare sites, net als bij
acct:. Twee accounts met dezelfde sleutel staan er allebei: dat is het oude en
het nieuwe na een verhuizing. De vrager leert niets dat niet al openbaar op
die actors staat. Cache 60 seconden, korter dan acct:, zodat weghalen snel
zichtbaar is.

test/webfinger-did.test.js: vier toetsen, met drie mutaties apart gedraaid
(geen eigen controle, ook besloten sites, geen did-tak), elk gevangen. Een
acct:-vraag gaat precies als vroeger. Volledige suite 1350 groen.

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

  • Property mode set to 100644
File size: 4.3 KB
RevLine 
[5d0d3f35]1// WebFinger op een did:key (FEP-c390, shaer-jpxj): wie een papieren sleutel
2// vasthoudt, vraagt welk account zegt dat het die sleutel is.
3//
4// Wat hier bewaakt wordt: alleen wat NU klopt (een verklaring die niet meer
5// natelopen is telt niet), alleen openbare sites (net als acct:), twee accounts
6// met dezelfde sleutel staan er allebei (oud en nieuw na een verhuizing), en
7// een acct:-vraag gaat precies als vroeger.
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15const db = dbMod.default;
16{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
17const I = await import('../src/services/IdentityProof.js');
18
19const seedA = Buffer.alloc(32, 7);
20const seedB = Buffer.alloc(32, 9);
21const didA = I.didKeyFromPublicKey(I.publicKeyFromSeed(seedA));
22const didB = I.didKeyFromPublicKey(I.publicKeyFromSeed(seedB));
23const actorOf = (slug) => `https://test.example/ap/users/${slug}`;
24function verklaring(seed, slug) {
25 const did = I.didKeyFromPublicKey(I.publicKeyFromSeed(seed));
26 const doc = { '@context': I.STATEMENT_CONTEXT, type: 'VerifiableIdentityStatement', subject: did, alsoKnownAs: actorOf(slug) };
27 return I.signEddsaJcs2022(doc, seed, { created: '2026-10-24T10:00:00Z', verificationMethod: did, proofPurpose: 'assertionMethod' });
28}
29
30db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god');
31const site = (id, slug, proofs, isPublic = 1) => db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary, identity_proofs) VALUES (?,?,?,?,?,?,?)')
32 .run(id, slug, slug, 'u1', isPublic, id === 's1' ? 1 : 0, proofs ? JSON.stringify(proofs) : null);
33site('s1', 'oud', [verklaring(seedA, 'oud')]);
34site('s2', 'nieuw', [verklaring(seedA, 'nieuw')]); // dezelfde sleutel, na een verhuizing
35site('s3', 'besloten', [verklaring(seedB, 'besloten')], 0); // niet openbaar
36// Een verklaring die op de verkeerde actor staat: geldig ondertekend, maar voor 'oud'.
37site('s4', 'leent', [verklaring(seedA, 'oud')]);
38
39const express = (await import('express')).default;
40const routes = (await import('../src/routes/activitypub.js')).default;
41const app = express(); app.use(routes);
42const server = app.listen(0); server.unref();
43const base = `http://127.0.0.1:${server.address().port}`;
44test.after(() => server.close());
45const wf = (resource) => fetch(`${base}/.well-known/webfinger?resource=${encodeURIComponent(resource)}`);
46
47test('een gekoppelde sleutel vindt zijn accounts, oud en nieuw allebei', async () => {
48 const r = await wf(didA);
49 assert.equal(r.status, 200);
50 assert.match(r.headers.get('content-type'), /application\/jrd\+json/);
51 const jrd = await r.json();
52 assert.equal(jrd.subject, didA);
53 assert.deepEqual(jrd.links.map((l) => l.href), [actorOf('nieuw'), actorOf('oud')]);
54 assert.ok(jrd.links.every((l) => l.rel === 'self' && l.type === 'application/activity+json'));
55 assert.ok(!jrd.links.some((l) => l.href === actorOf('leent')), 'een verklaring voor een ander account telt niet voor deze');
56});
57
58test('een sleutel die nergens aan hangt, of alleen aan een besloten site, is er niet', async () => {
59 assert.equal((await wf(I.didKeyFromPublicKey(I.publicKeyFromSeed(Buffer.alloc(32, 1))))).status, 404);
60 assert.equal((await wf(didB)).status, 404, 'besloten sites tellen niet mee, net als bij acct:');
61});
62
63test('een verklaring die niet meer klopt, telt niet', async () => {
64 const geknoeid = verklaring(seedA, 'oud'); geknoeid.proof.created = '2030-01-01T00:00:00Z';
65 db.prepare("UPDATE sites SET identity_proofs = ? WHERE id = 's1'").run(JSON.stringify([geknoeid]));
66 try {
67 const jrd = await (await wf(didA)).json();
68 assert.deepEqual(jrd.links.map((l) => l.href), [actorOf('nieuw')]);
69 } finally {
70 db.prepare("UPDATE sites SET identity_proofs = ? WHERE id = 's1'").run(JSON.stringify([verklaring(seedA, 'oud')]));
71 }
72});
73
74test('geen did:key is geen did-vraag, en acct: gaat als vroeger', async () => {
75 assert.equal((await wf('did:key:zNietBase58!!')).status, 400);
76 const r = await wf('acct:oud@test.example');
77 assert.equal(r.status, 200);
78 assert.equal((await r.json()).subject, 'acct:oud@test.example');
79});
Note: See TracBrowser for help on using the repository browser.