Changeset 0bafe9e in Klonkt


Ignore:
Timestamp:
08/06/2026 10:49:06 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
d74a461
Parents:
5d0d41d
git-author:
Robin <roboburr@…> (08/06/2026 10:49:05 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 10:49:06 AM)
Message:

Niet standaard ondertekenen bij de sleutel-ophaal (regressie van 9561d58)

Robins vraag legde bloot wat ik een uur eerder zelf had verslechterd.
verifyRequest haalt de keyId-URL op VOORDAT er iets geverifieerd is, en die URL
komt uit een header die iedereen mag sturen. Sinds 9561d58 werd dat verzoek
standaard ondertekend als een van onze actors -- dus kon een volslagen onbekende
met één POST boiert een ONDERTEKEND verzoek laten sturen naar een adres van zijn
keuze, met onze identiteit eronder. Dat is hoe een instance op een blocklist
belandt.

fetchActor probeert nu onbetekend eerst en tekent alleen als dat niets bruikbaars
oplevert. Voor een instance met authorized fetch verandert er niets behalve één
extra verzoek; voor alle andere verdwijnt de handtekening weer. Een document dat
onbetekend wél komt maar zonder publicKey telt als mislukt -- voor een
verificatie heb je daar niets aan.

Twee tests erbij die de VOLGORDE vastleggen, want dat is hier de hele zaak: een
open instance krijgt precies één, onbetekend verzoek, en een kaal document leidt
tot een tweede, ondertekende poging.

Wat hiermee NIET is opgelost, en in shaer-afq staat: de ophaal blijft aanroepbaar
door een onbekende, alleen niet meer namens ons. Het smaller maken van de
dereference (alleen bij een inReplyTo die we kennen) en het onthouden van
mislukte pogingen zijn aparte stappen.

Suite 454/454.

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

Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r5d0d41d r0bafe9e  
    12951295  // Geen kip-ei: om ONZE handtekening te controleren haalt de andere kant ons
    12961296  // actor-document op, en dat serveert Klonkt publiek.
    1297   if (opts.asSlug) {
    1298     const signed = await signedGetJson(opts.asSlug, url).catch(() => null);
    1299     if (signed && signed.id) return signed;
    1300     // Geen terugkeer bij mislukking: een instance zonder secure mode moet
    1301     // blijven werken, en niet elke 401 komt van authorized fetch.
    1302   }
     1297  //
     1298  // ONBETEKEND EERST, en dat is een veiligheidskeuze en geen optimalisatie.
     1299  // verifyRequest haalt de keyId-URL op VOORDAT er iets geverifieerd is, en die
     1300  // URL komt uit een header die iedereen mag sturen. Tekenden we dat verzoek
     1301  // standaard, dan kan een volslagen onbekende ons een ONDERTEKEND verzoek laten
     1302  // sturen naar een adres van zijn keuze -- met onze identiteit eronder. Dat is
     1303  // precies hoe een instance op een blocklist belandt. Ondertekenen doen we dus
     1304  // pas als het onbetekend niet lukt, en dan alleen voor deze ene URL.
     1305  let doc = null;
    13031306  try {
    13041307    const r = await safeFetch(url, { headers: { Accept: 'application/activity+json' } });
    1305     if (!r.ok) return null;
    1306     const len = Number(r.headers.get('content-length') || 0);
    1307     if (len > 2_000_000) return null; // refuse oversized actor docs
    1308     return await r.json();
    1309   } catch { return null; }
     1308    if (r.ok) {
     1309      const len = Number(r.headers.get('content-length') || 0);
     1310      if (len > 2_000_000) return null; // refuse oversized actor docs
     1311      doc = await r.json();
     1312    }
     1313  } catch { /* val door naar de ondertekende poging */ }
     1314  // Genoeg? Dan klaar. Sommige instances serveren onbetekend wel een document
     1315  // maar zonder sleutel; voor een verificatie hebben we daar niets aan, dus die
     1316  // telt als mislukt.
     1317  if (doc && (!opts.asSlug || (doc.publicKey && doc.publicKey.publicKeyPem))) return doc;
     1318  if (!opts.asSlug) return doc;
     1319  const signed = await signedGetJson(opts.asSlug, url).catch(() => null);
     1320  return (signed && signed.id) ? signed : doc;
    13101321}
    13111322
  • test/authorized-fetch.test.js

    r5d0d41d r0bafe9e  
    6666});
    6767
    68 test('mislukt ondertekend ophalen, dan volgt de onbetekende poging alsnog', async () => {
    69   // Niet elke 401 komt van secure mode, en een instance die geen handtekening
    70   // verwacht mag er niet door stukgaan. OPEN_ACTOR antwoordt op allebei, dus we
    71   // meten dat er ECHT twee pogingen zijn wanneer de eerste niets oplevert.
     68test('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.
    7274  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
     82test('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';
    7386  const stubOrig = globalThis.fetch;
    74   let eerste = true;
    7587  globalThis.fetch = async (url, opts = {}) => {
    7688    const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
    77     if (String(url) === OPEN_ACTOR && ondertekend && eerste) { eerste = false; return new Response('nee', { status: 500 }); }
     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    }
    7896    return stubOrig(url, opts);
    7997  };
    80   const actor = await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' });
     98  verzoeken = [];
     99  const actor = await AP.fetchActor(KAAL, { asSlug: 'me' });
    81100  globalThis.fetch = stubOrig;
    82   assert.ok(actor, 'de terugval hoort hem alsnog op te halen');
     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');
    83103});
    84104
Note: See TracChangeset for help on using the changeset viewer.