diff --git a/README.md b/README.md index 80d0959..b901536 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ var options = { An optional OAEP label may be supplied as `keyEncryptionOaepParams` (a Buffer or a base64 string); it is emitted as `` and honoured on decrypt. -Note: for the digest/MGF1 combinations Node's `crypto` cannot express, the OAEP padding is computed in JavaScript over the raw RSA primitive. That code path cannot offer the constant-time guarantees of OpenSSL's C implementation. It is used only when the MGF1 digest differs from the message digest; all other combinations go through `crypto.privateDecrypt` unchanged. +Note: for the digest/MGF1 combinations Node's `crypto` cannot express, the OAEP padding is computed in JavaScript over the raw RSA primitive. That code path is blocked when Node is using a FIPS compliant crypto provider and cannot offer the constant-time guarantees of OpenSSL's C implementation. It is used only when the MGF1 digest differs from the message digest; all other combinations go through `crypto.privateDecrypt` unchanged. **Breaking change:** in versions 3.1.0 through 5.0.0, `rsa-oaep-mgf1p` with `keyEncryptionDigest: 'sha256'` or `'sha512'` produced ciphertext using MGF1-SHA256 or MGF1-SHA512, which was never compliant with the W3C specification. `rsa-oaep-mgf1p` now produces MGF1-SHA1 ciphertext regardless of `keyEncryptionDigest`. Documents encrypted with the earlier behaviour will not decrypt with the current version; they were never interoperable with Java xmlsec, .NET `System.Security.Cryptography.Xml`, or other spec-compliant peers. Callers who genuinely need MGF1-SHA256 or MGF1-SHA512 should use `http://www.w3.org/2009/xmlenc11#rsa-oaep` with the `keyEncryptionMgf` option. diff --git a/lib/oaep.js b/lib/oaep.js index 78d3659..093c7d2 100644 --- a/lib/oaep.js +++ b/lib/oaep.js @@ -1,5 +1,19 @@ var crypto = require('crypto'); +// CRYPTO MODULE (non-FIPS-validated) +// This file hand-rolls the RSA-OAEP padding (EME-OAEP encode/decode, RFC 8017 +// ยง7.1) in JavaScript. It exists only to express the OAEP-digest / MGF1-digest combinations +// that Node's `crypto` cannot. Because the padding is computed here rather than inside OpenSSL, +// this is not a FIPS valid code path. + +function assertShimAllowed() { + if (crypto.getFips?.()) { + var err = new Error('unsupported cryptographic operation'); + err.code = 'ERR_XMLENC_FIPS_UNSUPPORTED'; + throw err; + } +} + // MGF1 mask generation function (RFC 8017 B.2.1). function mgf1(seed, length, hash) { var hLen = crypto.createHash(hash).digest().length; @@ -30,6 +44,7 @@ function decodingError() { // chosen independently. Node's privateDecrypt cannot express that combination: // it only sets the OAEP digest, and OpenSSL then defaults MGF1 to match it. function privateDecryptOaep(privateKey, ciphertext, options) { + assertShimAllowed(); var opts = options || {}; var oaepHash = opts.oaepHash || 'sha1'; var mgf1Hash = opts.mgf1Hash || oaepHash; @@ -84,6 +99,7 @@ function privateDecryptOaep(privateKey, ciphertext, options) { // EME-OAEP-ENCODE (RFC 8017 7.1.1) followed by the raw RSA public operation. function publicEncryptOaep(publicKey, message, options) { + assertShimAllowed(); var opts = options || {}; var oaepHash = opts.oaepHash || 'sha1'; var mgf1Hash = opts.mgf1Hash || oaepHash; diff --git a/test/oaep.js b/test/oaep.js index a259d4a..e7b96be 100644 --- a/test/oaep.js +++ b/test/oaep.js @@ -1,5 +1,6 @@ var assert = require('assert'); var crypto = require('crypto'); +var sinon = require('sinon'); var oaep = require('../lib/oaep'); // Throwaway 2048-bit key + ciphertext from the ESD-63620 repro. Produced by: @@ -148,6 +149,39 @@ describe('oaep', function () { }); }); + describe('FIPS mode gate', function () { + var fs = require('fs'); + var pub = fs.readFileSync(__dirname + '/test-auth0_rsa.pub'); + var key = fs.readFileSync(__dirname + '/test-auth0.key'); + + afterEach(function () { + sinon.restore(); + }); + + it('refuses publicEncryptOaep when FIPS mode is enabled', function () { + sinon.stub(crypto, 'getFips').returns(1); + assert.throws(function () { + oaep.publicEncryptOaep(pub, Buffer.from('test'), { oaepHash: 'sha256', mgf1Hash: 'sha1' }); + }, function (e) { return e.code === 'ERR_XMLENC_FIPS_UNSUPPORTED'; }); + }); + + it('refuses privateDecryptOaep when FIPS mode is enabled', function () { + // Encrypt outside FIPS, then assert decrypt is blocked under FIPS. + var ct = oaep.publicEncryptOaep(pub, Buffer.from('test'), { oaepHash: 'sha256', mgf1Hash: 'sha1' }); + sinon.stub(crypto, 'getFips').returns(1); + assert.throws(function () { + oaep.privateDecryptOaep(key, ct, { oaepHash: 'sha256', mgf1Hash: 'sha1' }); + }, function (e) { return e.code === 'ERR_XMLENC_FIPS_UNSUPPORTED'; }); + }); + + it('runs normally when FIPS mode is disabled', function () { + sinon.stub(crypto, 'getFips').returns(0); + var ct = oaep.publicEncryptOaep(pub, Buffer.from('test'), { oaepHash: 'sha256', mgf1Hash: 'sha1' }); + var pt = oaep.privateDecryptOaep(key, ct, { oaepHash: 'sha256', mgf1Hash: 'sha1' }); + assert.equal(pt.toString(), 'test'); + }); + }); + describe('mgf1', function () { it('matches the RFC 8017 counter construction for one block', function () { var seed = Buffer.from('abc');