kjur / jsrsasign

The 'jsrsasign' (RSA-Sign JavaScript Library) is an opensource free cryptography library supporting RSA/RSAPSS/ECDSA/DSA signing/validation, ASN.1, PKCS#1/5/8 private/public key, X.509 certificate, CRL, OCSP, CMS SignedData, TimeStamp, CAdES and JSON Web Signature/Token in pure JavaScript.
https://kjur.github.io/jsrsasign
Other
3.25k stars 646 forks source link

jsrsasign vulnerable to the Marvin Attack #598

Closed tomato42 closed 8 months ago

tomato42 commented 9 months ago

(Since I haven't found a security policy that would ask for filing security issues over email, I'm making a regular bug report)

I've tested jsrsasign 10.8.6 on nodejs 21.1.0 and I have found it vulnerable to the Marvin Attack.

Looking at the results, both the bit size of the raw RSA decryption is leaking (so all padding modes will be vulnerable, both PKCS#1 v1.5 and OAEP), and in case of PKCS#1 v1.5 the size of the decrypted message is leaking. As such, it provides timing oracles useful in mounting a timing variant of the Bleichenbacher attack.

I've collected 10000 measurements per sample on an isolated core of an AMD Ryzen 5 5600X. The test returned statistically significant results even with 100 measurements per sample, I've executed with with 10000 to look for side channels other then the bit size of the raw RSA operation. That means that the returned p-values are 0, as they are smaller in reality than a double precision floating point numbers can represent. For 100k measurements the summary looks as follows:

Sign test mean p-value: 0.08346, median p-value: 6.623e-14, min p-value: 0.0
Friedman test (chisquare approximation) for all samples
p-value: 0.0
Worst pair: 2(no_padding_48), 11(zero_byte_in_padding_48_4)
Mean of differences: 2.67060e-05s, 95% CI: 2.39585e-05s, 2.993167e-05s (±2.987e-06s)
Median of differences: 2.61110e-05s, 95% CI: 2.55910e-05s, 2.649050e-05s (±4.498e-07s)
Trimmed mean (5%) of differences: 2.59561e-05s, 95% CI: 2.51313e-05s, 2.690085e-05s (±8.848e-07s)
Trimmed mean (25%) of differences: 2.59850e-05s, 95% CI: 2.56003e-05s, 2.642811e-05s (±4.139e-07s)
Trimmed mean (45%) of differences: 2.60091e-05s, 95% CI: 2.56006e-05s, 2.643911e-05s (±4.193e-07s)
Trimean of differences: 2.60815e-05s, 95% CI: 2.56166e-05s, 2.649325e-05s (±4.383e-07s)

and the confidence interval graph for the individual probes: conf_interval_plot_trim_mean_25 Legend to the graph:

ID,Name
0,header_only
1,no_header_with_payload_48
2,no_padding_48
3,no_structure
4,signature_padding_8
5,valid_0
6,valid_48
7,valid_192
8,valid_246
9,valid_repeated_byte_payload_246_1
10,valid_repeated_byte_payload_246_255
11,zero_byte_in_padding_48_4

Explanation for the ciphertexts is in the step2.py file.

Side note: the valid_246 probe is actually invalid, it has padding string of 7 bytes, which is less than the mandatory 8.

The reproducer I used for the test:

var program = require('commander');
var rs = require('jsrsasign');
var rsu = require('jsrsasign-util');
var path = require('path');
var fs = require('fs');

program
  .version('1.0.0 (2016-Nov-05)')
  .usage('[options] <encrypted data file> <output time file or "-"> <PEM RSA private key file> [RSA|RSAOEAP*>]')
  .description('encrypt data')
  .parse(process.argv);

if (program.args.length < 3)
  throw "wrong number of arguments";

var keyObj, inHex, encHex;
var algName = "RSA";
var keyStr = "";
var inFileOrHex = program.args[0];
var outFile = program.args[1];
var keyFileOrStr = program.args[2];
if (program.args.length > 3) algName = program.args[3];

try {
  keyStr = rsu.readFile(keyFileOrStr);
} catch(ex) {
  keyStr = keyFileOrStr;
}

try {
  keyObj = rs.KEYUTIL.getKey(keyStr);
} catch(ex) {};

const fileDescriptor = fs.openSync(inFileOrHex, 'r');

const outFD = fs.openSync(outFile, 'w');

const buffer = Buffer.alloc(256);

let bytesRead;

do {

    bytesRead = fs.readSync(fileDescriptor, buffer, 0, buffer.length);

    if (bytesRead > 0) {

        inHex = buffer.toString('hex');

        var startTime = process.hrtime();

        var plainStr = rs.KJUR.crypto.Cipher.decrypt(inHex, keyObj, algName);

        var endTime = process.hrtime();

        var diff = (endTime[0] - startTime[0]) * 1000000000 + endTime[1] - startTime[1];

        var outBuffer = Buffer.alloc(4);
        outBuffer.writeInt32LE(diff, 0);
        fs.writeSync(outFD, outBuffer);
    }
} while (bytesRead === buffer.length);

fs.closeSync(fileDescriptor);
fs.closeSync(outFD);

It can be used in similar way as the python reproducer but in the extract step you need to additionally specify --binary 4.

kjur commented 9 months ago

@tomato42 , thank you for your report. I'll investigate and try to fix it.

kjur commented 8 months ago

Hi @tomato42 , I've just released jsrsasign 11.0.0. RSA and RSAOAEP encryption/decryption functions have been removed. I'm talking with Synk for CVE number coordination and I'll publish security advisory for it. Thank you.

kjur commented 8 months ago

Its security advisory is published. https://github.com/kjur/jsrsasign/security/advisories/GHSA-rh63-9qcf-83gf