Ignore:
Timestamp:
08/07/2026 05:00:09 PM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
952baf3
Parents:
12bed59
Message:

Bind de sleutel aan de actor waarvoor hij spreekt

verifyRequest haalde het actor-document op bij de keyId uit de handtekening en gaf
dat document daarna terug zoals het binnenkwam. De inbox beslist op verified.id,
dus op wat het document over zichzelf beweert. Wie een document neerzet met de id
van zijn slachtoffer naast zijn eigen publieke sleutel, en ondertekent met zijn
eigen private helft, werd geloofd. De server van het slachtoffer wordt daarbij
nooit geraadpleegd en kan het dus ook niet tegenspreken.

De identiteit moet komen uit waar de sleutel is opgehaald, niet uit wat het
document over zichzelf zegt. Drie voorwaarden erbij: dezelfde herkomst als de
keyId, de keyId moet de sleutel zijn die de actor adverteert, en het sleutelblok
moet naar die actor zelf verwijzen.

Er gaat niets af en er is bewust geen uitzonderingslijst. Een "behalve bij een
bekende peer"-ontsnapping is precies de deur die dit dichtdoet. Het versmalt de
compatibiliteit ook niet: de regel erboven eiste al het ingebedde publicKey-object,
dus een array of een kale URI-verwijzing werkte hier nooit.

De Shaer-clients raakt dit niet. Die gaan via OAuth.verifyBearer; verifyRequest is
uitsluitend het server-naar-server-pad. Een client ondertekent nooit met een
HTTP-handtekening.

Changed files:
src/services/ActivityPubService.js

  • drie bindingscontroles in verifyRequest, direct na het ophalen
  • onparseerbare id of keyId valt naar null in plaats van door te glippen

New file:
test/ap-signature-keyid-binding.test.js

  • de aanval zelf: een document dat de id van een andere host claimt
  • en dat de echte server van het slachtoffer niet eens bevraagd wordt
  • de sleutel van een buurman op dezelfde host
  • een sleutelblok met een vreemde owner
  • gewone federatie op de eigen host blijft werken
  • een verdraaide handtekening blijft falen, dus de wiskunde is niet vervangen
  • hosts zijn IP-literals uit de documentatiereeksen en fetch is gestubd, dus de test gaat nooit het netwerk op

remarks: nagemeten dat de test zonder de fix ook echt faalt (3 van 5 rood, en juist
de twee die moeten blijven werken bleven groen). Volledige suite 405 groen, in UTC
en in Europe/Amsterdam. Dit gat zit ook in stable (859b170, regel 1204), dus het
hoort daar als 1.6.1 heen; die backport is deze drie regels.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r12bed59 rf85b2c3  
    12381238  const pem = actor && actor.publicKey && actor.publicKey.publicKeyPem;
    12391239  if (!pem) return null;
     1240  // Bind the key to the actor it speaks for. Without this we hand back whatever
     1241  // `id` the fetched document claims, so anyone could host a document carrying a
     1242  // VICTIM's id next to their OWN public key, sign with their own private half,
     1243  // and be believed: the victim's server is never contacted. The caller decides on
     1244  // `verified.id`, so the identity has to come from where the key was FETCHED,
     1245  // never from what the document says about itself.
     1246  // Adds conditions only, and there is no exemption list on purpose: an
     1247  // "unless it's a known peer" escape hatch is exactly the door this closes.
     1248  // Note this does not narrow what we accept in practice, since the line above
     1249  // already requires the embedded publicKey object (an array or a bare URI
     1250  // reference never worked here).
     1251  const key = actor.publicKey;
     1252  try {
     1253    if (new URL(p.keyId).host !== new URL(actor.id).host) return null;   // same origin as the key
     1254    if (key.id && key.id !== p.keyId) return null;                       // this key, not a neighbour's
     1255    if (key.owner && key.owner !== actor.id) return null;                // and it belongs to this actor
     1256  } catch { return null; }                                               // unparseable id or keyId
    12401257  const hs = (p.headers || '(request-target) host date').split(/\s+/);
    12411258  // Behind a reverse proxy the raw Host header is the backend bind (e.g. localhost:3000, when
Note: See TracChangeset for help on using the changeset viewer.