Ignore:
Timestamp:
08/06/2026 10:30:50 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
57aee3b
Parents:
25f2e64
git-author:
Robin <roboburr@…> (08/06/2026 10:30:49 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 10:30:50 AM)
Message:

Doorgestuurde activiteiten alsnog verifiëren bij de bron (shaer-s8k)

De inbox eiste dat een activiteit was ondertekend door de actor die hij claimde
te zijn. Bij DOORSTUREN klopt dat niet: reageert iemand in een thread, dan stuurt
de server van de oorspronkelijke poster die reactie door en ondertekent met zijn
EIGEN sleutel. Op boiert.eu leverde dat tientallen geweigerde Creates op van
echte accounts -- reacties die niet aankwamen, zonder dat iemand een fout zag.

Mastodon lost dit op met een LD-Signature over de payload. Dat vraagt
JSON-LD-canonicalisatie; dit doet het lichter en strenger: bij een mismatch
geloven we de bezorgde inhoud NIET en halen we het object op bij de bron.

Vier voorwaarden, en geen ervan is optioneel:

  • alleen Create en Update. Een doorgestuurde Delete is niet te dereferencen (het object is weg) en blijft geweigerd.
  • de host van de object-id moet die van de geclaimde actor zijn. Zonder dit anker wijst een doorsturer je naar een host die hij zelf beheert, waar attributedTo alles kan beweren.
  • het OPGEHAALDE object wordt gebruikt, niet de bezorgde payload. Anders levert een doorsturer een echt id met verdraaide inhoud.
  • mislukt het ophalen, wijkt het id af, of schrijft de bron het object niet toe aan de geclaimde actor, dan blijft het een weigering.

Een ongetekende activiteit wordt niet gedereferenced: zonder bewijs van wie het
bezorgde is er niets om op te bouwen. Beide fetch-paden lopen door safeFetch met
zijn SSRF-preflight.

Ook de logregel gesplitst. "(unsigned/invalid)" betekende drie dingen tegelijk;
het onderscheidt nu 'no signature', 'signature present, unverifiable' (meestal
een opgeheven account waarvan de sleutel weg is -- dat verklaart het gros van de
Delete-regels) en 'signer mismatch'.

Acht tests, waarvan zes weiger-gevallen: andere host, verkeerde attributedTo,
bron onbereikbaar, afwijkend id, doorgestuurde Delete, en ongetekend. Die
gebruiken IP-literals uit TEST-NET-3, want safeFetch doet dan geen DNS-lookup en
de test blijft offline.

Suite 449/449.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r25f2e64 r5296349  
    16211621}
    16221622
     1623/**
     1624 * Een DOORGESTUURDE activiteit alsnog verifiëren (shaer-s8k).
     1625 *
     1626 * Reageert iemand in een thread, dan stuurt de server van de oorspronkelijke
     1627 * poster die reactie door naar de deelnemers -- en ondertekent met zijn EIGEN
     1628 * sleutel. De handtekening klopt dan, maar de ondertekenaar is niet de auteur,
     1629 * dus de gate hieronder wees hem af. Gevolg: reacties van derden kwamen niet
     1630 * binnen, zonder dat iemand een fout zag.
     1631 *
     1632 * Mastodon lost dit op met een LD-Signature over de payload. Dat vraagt
     1633 * JSON-LD-canonicalisatie; wij doen het lichter en strenger: we geloven de
     1634 * bezorgde inhoud NIET en halen het object op bij de bron.
     1635 *
     1636 * Vier voorwaarden, en geen ervan is optioneel:
     1637 *
     1638 *  1. Alleen Create en Update. Een doorgestuurde Delete is per definitie niet te
     1639 *     dereferencen -- het object is weg -- dus die blijft geweigerd.
     1640 *  2. De host van de object-id MOET die van de geclaimde actor zijn. Zonder dit
     1641 *     anker wijst een doorsturer je naar een host die hij zelf beheert, waar
     1642 *     attributedTo alles kan beweren.
     1643 *  3. Het OPGEHAALDE object wordt gebruikt, niet de bezorgde payload. Anders
     1644 *     levert een doorsturer een echt id met verdraaide inhoud.
     1645 *  4. Mislukt het ophalen, of wijst het object zichzelf niet toe aan de
     1646 *     geclaimde actor, dan blijft het een weigering. Geen twijfelgeval opslaan.
     1647 */
     1648async function dereferenceForwarded(act, claimedActor, type, slugParam) {
     1649  if (type !== 'Create' && type !== 'Update') return null;
     1650  const o = act && act.object;
     1651  const objId = typeof o === 'string' ? o : (o && o.id);
     1652  if (!objId || typeof objId !== 'string' || !/^https:\/\//i.test(objId)) return null;
     1653  try {
     1654    if (new URL(objId).host !== new URL(claimedActor).host) return null;   // ankereis
     1655  } catch { return null; }
     1656  // Ondertekend als de ontvangende site wanneer we die kennen; anders anoniem,
     1657  // wat voor een publieke note volstaat. Beide gaan door safeFetch.
     1658  const fetched = await signedGetJson(slugParam, objId).catch(() => null);
     1659  if (!fetched || fetched.id !== objId) return null;   // ook: geen omleiding naar iets anders
     1660  const attributed = typeof fetched.attributedTo === 'string'
     1661    ? fetched.attributedTo
     1662    : (fetched.attributedTo && fetched.attributedTo.id);
     1663  if (attributed !== claimedActor) return null;
     1664  return fetched;
     1665}
     1666
    16231667// Handle an incoming inbox POST. slugParam = null for the shared /ap/inbox.
    16241668export async function handleInbox(req, slugParam, preVerified = null) {
     
    16441688  const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject', 'Add', 'Remove', 'Update', 'Flag', 'Offer', 'Move'];
    16451689  if (GATED.includes(type)) {
    1646     if (!verified || !claimedActor || verified.id !== claimedActor) {
    1647       console.warn('[AP] inbox REJECTED (signature)', type, claimedActor || '?', 'from', ip, verified ? '(signer mismatch)' : '(unsigned/invalid)');
     1690    // Een geldige handtekening van iemand anders dan de auteur is doorsturen,
     1691    // geen vervalsing. Haal het object dan bij de bron op in plaats van het af
     1692    // te wijzen; lukt dat niet, dan valt het door naar de weigering hieronder.
     1693    let forwarded = null;
     1694    if (verified && claimedActor && verified.id !== claimedActor) {
     1695      forwarded = await dereferenceForwarded(act, claimedActor, type, slugParam).catch(() => null);
     1696      if (forwarded) {
     1697        act.object = forwarded;   // de OPGEHAALDE inhoud, niet de bezorgde
     1698        console.log('[AP] inbox forwarded, geverifieerd bij de bron:', type, claimedActor, 'via', verified.id);
     1699      }
     1700    }
     1701    if (!forwarded && (!verified || !claimedActor || verified.id !== claimedActor)) {
     1702      // Drie verschillende oorzaken, die eerder allemaal "unsigned/invalid"
     1703      // heetten: geen handtekening meegestuurd, wel een handtekening maar niet
     1704      // te verifiëren (meestal een opgeheven account waarvan de sleutel weg is),
     1705      // of geldig ondertekend door iemand anders.
     1706      const reden = verified ? '(signer mismatch)'
     1707        : (req.headers && req.headers.signature) ? '(signature present, unverifiable)'
     1708        : '(no signature)';
     1709      console.warn('[AP] inbox REJECTED (signature)', type, claimedActor || '?', 'from', ip, reden);
    16481710      return 401;
    16491711    }
Note: See TracChangeset for help on using the changeset viewer.