| 1 | // PKCS#1 v1.5 zelf uitpakken (shaer-r15).
|
|---|
| 2 | //
|
|---|
| 3 | // Node weigert privateDecrypt met RSA_PKCS1_PADDING sinds de mitigatie voor
|
|---|
| 4 | // CVE-2023-46809, en de revert-vlag bestaat alleen op 18/20/21 -- allemaal EOL.
|
|---|
| 5 | // FEP-61cf schrijft v1.5 voor, dus OAEP breekt de interop. Blijft over: het
|
|---|
| 6 | // omhulsel zelf afhalen, met implicit rejection erin.
|
|---|
| 7 | //
|
|---|
| 8 | // DEZE TESTS DRAAIEN ZONDER --security-revert. Dat is de hele inzet: als ze
|
|---|
| 9 | // groen zijn, werkt gastlogin op een Node die nog ondersteund wordt.
|
|---|
| 10 | import { test } from 'node:test';
|
|---|
| 11 | import assert from 'node:assert/strict';
|
|---|
| 12 | import crypto from 'node:crypto';
|
|---|
| 13 | import { decryptToken, encryptTokenFor, _nepUitkomst } from '../src/services/OpenWebAuthService.js';
|
|---|
| 14 |
|
|---|
| 15 | const paar = () => crypto.generateKeyPairSync('rsa', {
|
|---|
| 16 | modulusLength: 2048,
|
|---|
| 17 | publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|---|
| 18 | privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|---|
| 19 | });
|
|---|
| 20 | const A = paar();
|
|---|
| 21 | const TOKEN = 'abcdefghijklmnopqrstuvwxyz0123456789._~-';
|
|---|
| 22 |
|
|---|
| 23 | test('een echt token komt er heel uit, zonder revert-vlag', () => {
|
|---|
| 24 | const ct = encryptTokenFor(TOKEN, A.publicKey);
|
|---|
| 25 | assert.equal(decryptToken(ct, A.privateKey), TOKEN);
|
|---|
| 26 | });
|
|---|
| 27 |
|
|---|
| 28 | test('een verkeerde sleutel WERPT niet maar levert niets op', () => {
|
|---|
| 29 | const B = paar();
|
|---|
| 30 | const ct = encryptTokenFor(TOKEN, A.publicKey);
|
|---|
| 31 | // Werpen is het signaal waar Bleichenbacher op draait. Dat mag hier niet.
|
|---|
| 32 | assert.doesNotThrow(() => decryptToken(ct, B.privateKey));
|
|---|
| 33 | assert.equal(decryptToken(ct, B.privateKey), null);
|
|---|
| 34 | });
|
|---|
| 35 |
|
|---|
| 36 | // Dezelfde meting als waar het oude commentaar op stond, nu tegen de nieuwe weg.
|
|---|
| 37 | test('300 vreemde sleutels: geen enkele worp, geen enkel token', () => {
|
|---|
| 38 | const ct = encryptTokenFor(TOKEN, A.publicKey);
|
|---|
| 39 | let worpen = 0, tokens = 0;
|
|---|
| 40 | for (let i = 0; i < 300; i++) {
|
|---|
| 41 | try { if (decryptToken(ct, paar().privateKey) !== null) tokens++; } catch { worpen++; }
|
|---|
| 42 | }
|
|---|
| 43 | assert.equal(worpen, 0, 'geen enkele aanroep mag werpen');
|
|---|
| 44 | assert.equal(tokens, 0, 'afgeleide onzin mag nooit als token doorgaan');
|
|---|
| 45 | });
|
|---|
| 46 |
|
|---|
| 47 | // De eigenschap waar implicit rejection op staat of valt.
|
|---|
| 48 | test('de nep-uitkomst is DETERMINISTISCH, niet vers willekeurig', () => {
|
|---|
| 49 | const ct = Buffer.from('een-ciphertext-om-te-herhalen');
|
|---|
| 50 | assert.equal(_nepUitkomst(A.privateKey, ct), _nepUitkomst(A.privateKey, ct),
|
|---|
| 51 | 'dezelfde ciphertext moet hetzelfde antwoord geven; anders verklikt herhaling het verschil');
|
|---|
| 52 | assert.notEqual(_nepUitkomst(A.privateKey, ct), _nepUitkomst(paar().privateKey, ct),
|
|---|
| 53 | 'en hij moet aan de sleutel hangen, anders is hij te voorspellen');
|
|---|
| 54 | assert.notEqual(_nepUitkomst(A.privateKey, ct), _nepUitkomst(A.privateKey, Buffer.from('iets anders')));
|
|---|
| 55 | });
|
|---|
| 56 |
|
|---|
| 57 | test('rommel erin werpt ook niet', () => {
|
|---|
| 58 | for (const rommel of ['', 'geen base64!!', 'a'.repeat(10), 'x'.repeat(400), null, undefined]) {
|
|---|
| 59 | assert.doesNotThrow(() => decryptToken(rommel, A.privateKey), String(rommel).slice(0, 12));
|
|---|
| 60 | assert.equal(decryptToken(rommel, A.privateKey), null);
|
|---|
| 61 | }
|
|---|
| 62 | });
|
|---|
| 63 |
|
|---|
| 64 | // De regels van het omhulsel, ECHT getoetst. Met RSA_NO_PADDING kun je een blok
|
|---|
| 65 | // naar keuze versleutelen (rauw m^e mod n), dus we kunnen precies bepalen wat er
|
|---|
| 66 | // na ontsleuteling uit komt -- en dus elke regel apart uitproberen.
|
|---|
| 67 | const rauwVersleutel = (blok) =>
|
|---|
| 68 | crypto.publicEncrypt({ key: A.publicKey, padding: crypto.constants.RSA_NO_PADDING }, blok)
|
|---|
| 69 | .toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|---|
| 70 |
|
|---|
| 71 | /**
|
|---|
| 72 | * 00 02 <ps bytes vulling> 00 <boodschap>, met de vulling instelbaar.
|
|---|
| 73 | *
|
|---|
| 74 | * De boodschap staat tegen het EINDE van het blok: bij PKCS#1 vult hij precies
|
|---|
| 75 | * de rest op. Zet je hem er los achter, dan houd je staartnullen over en die
|
|---|
| 76 | * sneuvelen terecht op de tekenset-controle -- daar liep mijn eerste opzet op
|
|---|
| 77 | * vast, en dat is meteen het bewijs dat deze test iets doet.
|
|---|
| 78 | */
|
|---|
| 79 | function bouwBlok({ kop = [0x00, 0x02], ps = 200, scheider = true } = {}) {
|
|---|
| 80 | const k = 256;
|
|---|
| 81 | const boodschap = 'a'.repeat(k - 3 - ps);
|
|---|
| 82 | const b = Buffer.alloc(k, 0x00);
|
|---|
| 83 | b[0] = kop[0]; b[1] = kop[1];
|
|---|
| 84 | for (let i = 2; i < 2 + ps; i++) b[i] = 0xAB; // vulling, nooit nul
|
|---|
| 85 | b[2 + ps] = scheider ? 0x00 : 0xAB;
|
|---|
| 86 | Buffer.from(boodschap).copy(b, 2 + ps + 1);
|
|---|
| 87 | return { blok: b, boodschap };
|
|---|
| 88 | }
|
|---|
| 89 |
|
|---|
| 90 | test('een handgemaakt GELDIG omhulsel levert de boodschap op', () => {
|
|---|
| 91 | const { blok, boodschap } = bouwBlok({ ps: 200 });
|
|---|
| 92 | assert.equal(decryptToken(rauwVersleutel(blok), A.privateKey), boodschap,
|
|---|
| 93 | 'dit bewijst dat de uitpakker echt uitpakt');
|
|---|
| 94 | });
|
|---|
| 95 |
|
|---|
| 96 | test('te korte PS wordt geweigerd: RFC 8017 eist er acht', () => {
|
|---|
| 97 | const kort = bouwBlok({ ps: 7 }); // scheider op 9, grens ligt op 10
|
|---|
| 98 | assert.equal(decryptToken(rauwVersleutel(kort.blok), A.privateKey), null);
|
|---|
| 99 | const net = bouwBlok({ ps: 8 }); // scheider op 10, precies goed
|
|---|
| 100 | assert.equal(decryptToken(rauwVersleutel(net.blok), A.privateKey), net.boodschap);
|
|---|
| 101 | });
|
|---|
| 102 |
|
|---|
| 103 | test('een verkeerde kop wordt geweigerd', () => {
|
|---|
| 104 | for (const kop of [[0x00, 0x01], [0x01, 0x02], [0x00, 0x00]])
|
|---|
| 105 | assert.equal(decryptToken(rauwVersleutel(bouwBlok({ kop }).blok), A.privateKey), null, String(kop));
|
|---|
| 106 | });
|
|---|
| 107 |
|
|---|
| 108 | test('zonder scheidende nulbyte komt er niets uit', () => {
|
|---|
| 109 | const { blok } = bouwBlok({ ps: 200, scheider: false }); // nergens een nulbyte
|
|---|
| 110 | assert.equal(decryptToken(rauwVersleutel(blok), A.privateKey), null);
|
|---|
| 111 | });
|
|---|