Changeset 58cad21 in Klonkt for test


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@…>

Location:
test
Files:
1 added
1 edited

Legend:

Unmodified
Added
Removed
  • test/openwebauth.test.js

    r5869b1f r58cad21  
    7474  assert.ok(!/[+/]/.test(versleuteld), 'en URL-veilig');
    7575
    76   const terug = crypto.privateDecrypt(
    77     { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
    78     Buffer.from(versleuteld.replace(/-/g, '+').replace(/_/g, '/'), 'base64'),
    79   ).toString('utf8');
     76  // Door ONZE eigen uitpakker, niet rechtstreeks door crypto.privateDecrypt:
     77  // die weigert PKCS#1 v1.5 sinds de mitigatie voor CVE-2023-46809, en de
     78  // revert-vlag bestaat alleen op Node 18/20/21 -- allemaal EOL. decryptToken
     79  // haalt het omhulsel zelf af en werkt dus op een Node die nog leeft.
     80  const terug = OWA.decryptToken(versleuteld, privateKey.export({ type: 'pkcs8', format: 'pem' }));
    8081  assert.equal(terug, token);
    8182
     
    8384  // de voor de hand liggende toets (assert.throws) is hier fout.
    8485  //
    85   // PKCS#1 v1.5 gooit bij een verkeerde sleutel geen fout: OpenSSL 3 doet aan
    86   // "implicit rejection" en geeft afgeleide onzin terug in plaats van te falen,
    87   // juist zodat een aanvaller niet aan het foutgedrag kan aflezen of zijn gok
    88   // klopte (Bleichenbacher/Marvin). Gemeten: 200 vreemde sleutels, 0 fouten,
    89   // 200 keer bytes -- en 0 keer het token.
     86  // Bij een verkeerde sleutel hoort er geen FOUT te komen maar afgeleide onzin:
     87  // implicit rejection. Anders leest een aanvaller aan het foutgedrag af of zijn
     88  // gok klopte (Bleichenbacher/Marvin).
     89  //
     90  // LET OP waar dat vandaan komt. OpenSSL doet dit pas zelf vanaf 3.2; op 3.0.19
     91  // -- wat Node 20 meebrengt en wat wij draaien -- WERPT hij gewoon. Gemeten op
     92  // 19-8-2026. Een eerdere versie van deze test ging uit van OpenSSL's gedrag en
     93  // was daarmee afhankelijk van welke Node de meting toevallig deed. Nu maakt
     94  // decryptToken de implicit rejection ZELF, dus de eigenschap staat vast
     95  // ongeacht de OpenSSL eronder.
    9096  //
    9197  // De eigenschap die telt is dus niet "het knalt" maar "er komt iets anders
    9298  // uit". Wat de home instance daarna terugstuurt matcht geen enkel opgeslagen
    9399  // token, en de inlog mislukt gewoon.
    94   const ruw = Buffer.from(versleuteld.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
    95100  for (let i = 0; i < 5; i++) {
    96     const vreemde = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey;
     101    const vreemde = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })
     102      .privateKey.export({ type: 'pkcs8', format: 'pem' });
    97103    let uit = null;
    98     try {
    99       uit = crypto.privateDecrypt({ key: vreemde, padding: crypto.constants.RSA_PKCS1_PADDING }, ruw).toString('utf8');
    100     } catch { uit = null; }                     // een fout mag, maar is niet de regel
     104    assert.doesNotThrow(() => { uit = OWA.decryptToken(versleuteld, vreemde); },
     105      'werpen is het signaal waar Bleichenbacher op draait');
    101106    assert.notEqual(uit, token, 'andermans sleutel levert nooit het token');
    102107  }
Note: See TracChangeset for help on using the changeset viewer.