Changeset 58cad21 in Klonkt for src/middleware/rate-limit.js


Ignore:
Timestamp:
08/19/2026 07:44:50 PM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
598f090
Parents:
5869b1f
git-author:
Robin <roboburr@…> (08/19/2026 07:30:37 PM)
git-committer:
Robin <roboburr@…> (08/19/2026 07:44:50 PM)
Message:

OpenWebAuth: PKCS#1 v1.5 zelf uitpakken, met een grens op het aantal pogingen

Node weigert privateDecrypt met RSA_PKCS1_PADDING sinds de mitigatie voor
CVE-2023-46809 (Marvin). Daarmee was gastlogin stuk: vier tests rood, en
in bedrijf een geworpen TypeError midden in de inlogstroom.

De revert-vlag was geen uitweg. Die bestaat alleen op de lijnen 18/20/21
-- Node voegt een security-revert alleen toe aan wat ondersteund was toen
de fix kwam -- en 20 is sinds 30 april 2026 EOL. Node 22+ heeft hem nooit
gehad. FEP-61cf schrijft v1.5 voor, dus OAEP repareert de test en breekt
de interop met Hubzilla. Blijft over: het omhulsel zelf afhalen via
RSA_NO_PADDING.

Dat is precies het stuk dat de CVE veroorzaakte, dus met de zorg erbij:

  • Geen vroege uitgang en geen worp. De scan loopt altijd het hele blok af. Dat is geen echte constant-time -- die krijg je in JavaScript met JIT en GC niet -- maar het haalt het waarneembare verschil weg.
  • Implicit rejection: bij een ongeldig omhulsel een afgeleide waarde in plaats van een fout. DETERMINISTISCH, uit sleutel + ciphertext. Vers willekeurig zou slechter zijn: dezelfde ciphertext twee keer aanbieden gaf dan twee antwoorden, en juist dat verschil wilden we verbergen.
  • Een teller van 20/uur op POST /magic, per SITE-SLUG want daar hangt het sleutelpaar aan. Hij telt ALLE pogingen, niet alleen de mislukte: een teller die alleen faalt meetelt is zelf weer een orakel, en dan staat de vertakking die we bij de ontsleuteling weghaalden aan de achterdeur terug. Een orakel heeft honderdduizenden pogingen nodig; twintig per uur is voor een mens onzichtbaar.

Onderweg bleek een aanname in de bestaande code niet te kloppen: het
commentaar bij decryptToken en bij de test leunden op OpenSSL's eigen
implicit rejection. Die kwam pas in 3.2; Node 20 brengt 3.0.19 mee en
daar WERPT hij. Die meting was dus op een andere machine gedaan dan waar
het draait. Nu maken we het zelf, dus de eigenschap staat vast ongeacht
de OpenSSL eronder.

Suite 1160/1160. Van de drie eigenschappen bijten er twee bij een
controleproef (de PS-ondergrens en het determinisme); de nep-uitkomst
niet, want de andere tak geeft dan een lege string die net zo goed
sneuvelt. Dat staat zo in het commentaar in plaats van dat ik doe alsof
alle drie dragend zijn.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/middleware/rate-limit.js

    r5869b1f r58cad21  
    101101});
    102102
     103// ─── OpenWebAuth /magic ───────────────────────────────────────────
     104// Elke poging doet EEN RSA-ontsleuteling met de actorsleutel van een site. Dat
     105// is precies de vorm waar een Bleichenbacher/Marvin-orakel op draait: veel
     106// aangepaste ciphertexts, en uit de antwoorden de sleutel afleiden. De
     107// ontsleuteling zelf is daartegen gehard (implicit rejection in
     108// OpenWebAuthService.decryptToken), maar echte constant-time code bestaat niet
     109// in JavaScript. Een grens op het AANTAL pogingen doet daarom het zware werk:
     110// een orakel heeft er honderdduizenden nodig.
     111//
     112// TELT ALLE POGINGEN, niet alleen de mislukte. Een teller die alleen faalt
     113// meetelt is zelf weer een orakel -- dan leest een aanvaller aan het knijpen af
     114// of zijn padding klopte, en is de vertakking die we bij de ontsleuteling
     115// weghaalden aan de achterdeur terug.
     116//
     117// Per SITE-SLUG, want dat is wat een sleutelpaar heeft (getOrCreateKeys(slug)):
     118// de grens hoort bij de sleutel die beschermd wordt, niet bij het IP van de
     119// eigenaar of bij de doel-host die de aanvaller zelf kiest.
     120//
     121// Twintig per uur is voor een mens onzichtbaar -- je klikt een handvol keer per
     122// dag naar een andere site -- en voor een orakel dodelijk.
     123export const owaMagicLimiter = rateLimit({
     124  windowMs: 60 * 60 * 1000,
     125  max: 20,
     126  standardHeaders: true,
     127  legacyHeaders: false,
     128  keyGenerator: (req) => 'owa:' + String((req.body && req.body.slug) || (req.session && req.session.user && req.session.user.id) || 'onbekend'),
     129  validate: { ip: false },
     130});
     131
    103132// Inbox POSTs each trigger an outbound actor fetch (signature verify) → cap the
    104133// amplification/queue-inflation a single source can drive. 120/min/IP is still
Note: See TracChangeset for help on using the changeset viewer.