Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/authorized-fetch.test.js

    r0bafe9e rc66cbb4  
    1 // Authorized fetch (shaer-afq): een instance in Mastodons secure mode geeft zijn
    2 // actor-document -- en dus zijn publieke sleutel -- alleen aan een ONDERTEKEND
    3 // verzoek. Zonder die handtekening kregen we 401, vonden we geen sleutel, en
    4 // wezen we elke correct ondertekende Follow van die instance af. Op boiert.eu
    5 // bleven daardoor vier accounts eindeloos hangen.
    6 //
    7 // De fetch is hier gestubd: het gaat om de vraag OF er ondertekend wordt en of
    8 // er wordt teruggevallen, niet om echte HTTP of crypto.
    9 //
    10 // Run: npm test
    11 
     1// FEP-633c §5.3 note: a committed guardian is recognised for authorized fetch.
    122import { test } from 'node:test';
    133import assert from 'node:assert/strict';
    144
    155process.env.DATABASE_PATH = ':memory:';
    16 process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
     6process.env.PUBLIC_BASE_URL = 'https://test.example';
    177
    188const dbMod = await import('../src/config/database.js');
    199const db = dbMod.default;
    2010dbMod.initializeDatabase();
    21 const AP = await import('../src/services/ActivityPubService.js');
     11const AP = (await import('../src/services/ActivityPubService.js')).default;
    2212
    23 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
    24   .run('u1', 'u1', 'u1@test', 'x', 'god');
    25 db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
     13db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god');
     14db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
     15// Commit a guardian relation: kid (ward) is guarded by mom.
     16const MOM = 'https://mom.example/ap/users/mom';
     17db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status, created_at) VALUES ('kid','ward',?, 'accepted', CURRENT_TIMESTAMP)").run(MOM);
    2618
    27 // IP-literals: safeFetch slaat de DNS-lookup over, dus de test blijft offline.
    28 const OPEN_ACTOR = 'https://203.0.113.30/users/open';       // gewone instance
    29 const SECURE_ACTOR = 'https://203.0.113.40/users/gesloten'; // authorized fetch
    30 
    31 let verzoeken = [];
    32 const echteFetch = globalThis.fetch;
    33 globalThis.fetch = async (url, opts = {}) => {
    34   const u = String(url);
    35   const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
    36   verzoeken.push({ url: u, ondertekend });
    37   const doc = (id) => new Response(JSON.stringify({
    38     id, type: 'Person', preferredUsername: 'x', inbox: `${id}/inbox`,
    39     publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: '-----BEGIN PUBLIC KEY-----\nx\n-----END PUBLIC KEY-----' },
    40   }), { status: 200, headers: { 'content-type': 'application/activity+json' } });
    41 
    42   if (u === OPEN_ACTOR) return doc(OPEN_ACTOR);
    43   // De kern van secure mode: onbetekend is het 401, ondertekend krijg je hem wel.
    44   if (u === SECURE_ACTOR) return ondertekend ? doc(SECURE_ACTOR) : new Response('unauthorized', { status: 401 });
    45   return new Response('not found', { status: 404 });
    46 };
    47 
    48 test('een actor achter authorized fetch wordt nu wél opgehaald', async () => {
    49   verzoeken = [];
    50   const actor = await AP.fetchActor(SECURE_ACTOR, { asSlug: 'me' });
    51   assert.ok(actor, 'de actor hoort binnen te komen');
    52   assert.equal(actor.id, SECURE_ACTOR);
    53   assert.ok(verzoeken.some((v) => v.url === SECURE_ACTOR && v.ondertekend), 'het verzoek hoort ondertekend te zijn');
     19test('a committed guardian is recognised; a stranger is not', () => {
     20  assert.equal(AP.isWardGuardian('kid', MOM), true);
     21  assert.equal(AP.isWardGuardian('kid', 'https://x.example/ap/users/stranger'), false);
     22  assert.equal(AP.isWardGuardian('nosuch', MOM), false);
    5423});
    55 
    56 test('zonder ondertekenaar blijft dezelfde actor onbereikbaar', async () => {
    57   // Dit is precies het oude gedrag, en het bewijst dat de stub echt onderscheid
    58   // maakt in plaats van altijd mee te werken.
    59   const actor = await AP.fetchActor(SECURE_ACTOR);
    60   assert.equal(actor, null);
    61 });
    62 
    63 test('een gewone instance blijft werken, ondertekend of niet', async () => {
    64   assert.ok(await AP.fetchActor(OPEN_ACTOR), 'onbetekend');
    65   assert.ok(await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }), 'ondertekend');
    66 });
    67 
    68 test('een OPEN instance wordt NIET ondertekend opgehaald', () => {
    69   // De veiligheidskant van shaer-afq: verifyRequest haalt de keyId-URL op
    70   // voordat er iets geverifieerd is, en die URL komt uit een header die iedereen
    71   // mag sturen. Tekenden we standaard, dan kan een vreemde ons een ondertekend
    72   // verzoek naar een adres van zijn keuze laten sturen, met onze identiteit
    73   // eronder. Onbetekend eerst dus, en alleen tekenen als het anders niet lukt.
    74   verzoeken = [];
    75   return AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }).then((actor) => {
    76     assert.ok(actor);
    77     assert.equal(verzoeken.length, 1, 'één poging, geen tweede');
    78     assert.equal(verzoeken[0].ondertekend, false, 'en die was onbetekend');
    79   });
    80 });
    81 
    82 test('een document zonder sleutel telt als mislukt en leidt tot een ondertekende poging', async () => {
    83   // Sommige instances serveren onbetekend wel iets, maar zonder publicKey. Voor
    84   // een verificatie hebben we daar niets aan.
    85   const KAAL = 'https://203.0.113.50/users/kaal';
    86   const stubOrig = globalThis.fetch;
    87   globalThis.fetch = async (url, opts = {}) => {
    88     const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
    89     if (String(url) === KAAL) {
    90       verzoeken.push({ url: String(url), ondertekend });
    91       const body = ondertekend
    92         ? { id: KAAL, type: 'Person', publicKey: { id: `${KAAL}#k`, owner: KAAL, publicKeyPem: 'x' } }
    93         : { id: KAAL, type: 'Person' };   // kaal: geen sleutel
    94       return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/activity+json' } });
    95     }
    96     return stubOrig(url, opts);
    97   };
    98   verzoeken = [];
    99   const actor = await AP.fetchActor(KAAL, { asSlug: 'me' });
    100   globalThis.fetch = stubOrig;
    101   assert.ok(actor.publicKey && actor.publicKey.publicKeyPem, 'de ondertekende poging levert de sleutel');
    102   assert.deepEqual(verzoeken.map((v) => v.ondertekend), [false, true], 'eerst onbetekend, daarna pas ondertekend');
    103 });
    104 
    105 test('een onbekende actor blijft null, ondertekend of niet', async () => {
    106   assert.equal(await AP.fetchActor('https://203.0.113.99/users/weg', { asSlug: 'me' }), null);
    107 });
    108 
    109 test.after(() => { globalThis.fetch = echteFetch; });
Note: See TracChangeset for help on using the changeset viewer.