Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5650,9 +5650,11 @@ DataPointer RSA_Cipher(const EVPKeyPointer& key,
if (!key) return {};
EVPKeyCtxPointer ctx = key.newCtx();

const Digest& mgf1_digest =
params.mgf1_digest != nullptr ? params.mgf1_digest : params.digest;
if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) ||
(params.digest != nullptr && (!ctx.setRsaOaepMd(params.digest) ||
!ctx.setRsaMgf1Md(params.digest)))) {
(params.digest != nullptr &&
(!ctx.setRsaOaepMd(params.digest) || !ctx.setRsaMgf1Md(mgf1_digest)))) {
return {};
}

Expand Down Expand Up @@ -5691,7 +5693,9 @@ DataPointer CipherImpl(const EVPKeyPointer& key,
if (!key) return {};
EVPKeyCtxPointer ctx = key.newCtx();
if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) ||
(params.digest != nullptr && !ctx.setRsaOaepMd(params.digest))) {
(params.digest != nullptr && !ctx.setRsaOaepMd(params.digest)) ||
(params.mgf1_digest != nullptr &&
!ctx.setRsaMgf1Md(params.mgf1_digest))) {
return {};
}

Expand Down
1 change: 1 addition & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ class Cipher final {
struct CipherParams {
int padding;
Digest digest;
Digest mgf1_digest;
const Buffer<const void> label;
};

Expand Down
20 changes: 16 additions & 4 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -5340,6 +5340,9 @@ An array of supported digest functions can be retrieved using
<!-- YAML
added: v0.11.14
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65073
description: The `mgf1Hash` option was added.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63188
description: Passing a CryptoKey as `privateKey` is no longer supported.
Expand Down Expand Up @@ -5370,8 +5373,11 @@ changes:
<!--lint disable maximum-line-length remark-lint-->

* `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|URL}
* `oaepHash` {string} The hash function to use for OAEP padding and MGF1.
**Default:** `'sha1'`
* `oaepHash` {string} The hash function to use for OAEP padding and, unless
`mgf1Hash` is set, MGF1. **Default:** `'sha1'`
* `mgf1Hash` {string} The hash function to use for the MGF1 mask generation
function of OAEP padding. If not specified, the value of `oaepHash` is used.
This allows the OAEP digest and the MGF1 digest to differ.
* `oaepLabel` {string|ArrayBuffer|Buffer|TypedArray|DataView} The label to
use for OAEP padding. If not specified, no label is used.
* `padding` {crypto.constants} An optional padding value defined in
Expand Down Expand Up @@ -5491,6 +5497,9 @@ be passed instead of a public key.
<!-- YAML
added: v0.11.14
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65073
description: The `mgf1Hash` option was added.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63188
description: Passing a CryptoKey as `key` is no longer supported.
Expand All @@ -5516,8 +5525,11 @@ changes:
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject}
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject}
A PEM encoded public or private key, or {KeyObject}.
* `oaepHash` {string} The hash function to use for OAEP padding and MGF1.
**Default:** `'sha1'`
* `oaepHash` {string} The hash function to use for OAEP padding and, unless
`mgf1Hash` is set, MGF1. **Default:** `'sha1'`
* `mgf1Hash` {string} The hash function to use for the MGF1 mask generation
function of OAEP padding. If not specified, the value of `oaepHash` is used.
This allows the OAEP digest and the MGF1 digest to differ.
* `oaepLabel` {string|ArrayBuffer|Buffer|TypedArray|DataView} The label to
use for OAEP padding. If not specified, no label is used.
* `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} An optional
Expand Down
6 changes: 4 additions & 2 deletions lib/internal/crypto/cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,17 @@ function rsaFunctionFor(method, defaultPadding, keyType) {
preparePrivateKey(key, keyName) :
preparePublicOrPrivateKey(key, keyName);
const padding = key.padding || defaultPadding;
const { oaepHash, encoding } = key;
const { oaepHash, mgf1Hash, encoding } = key;
let { oaepLabel } = key;
if (oaepHash !== undefined)
validateString(oaepHash, 'key.oaepHash');
if (mgf1Hash !== undefined)
validateString(mgf1Hash, 'key.mgf1Hash');
if (oaepLabel !== undefined)
oaepLabel = getArrayBufferOrView(oaepLabel, 'key.oaepLabel', encoding);
buffer = getArrayBufferOrView(buffer, 'buffer', encoding);
return method(data, format, type, passphrase, namedCurve, buffer,
padding, oaepHash, oaepLabel);
padding, oaepHash, oaepLabel, mgf1Hash);
};
}

Expand Down
13 changes: 12 additions & 1 deletion src/crypto/crypto_cipher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ bool PublicKeyCipher::Cipher(
const EVPKeyPointer& pkey,
int padding,
const Digest& digest,
const Digest& mgf1_digest,
const ArrayBufferOrViewContents<unsigned char>& oaep_label,
const ArrayBufferOrViewContents<unsigned char>& data,
std::unique_ptr<BackingStore>* out) {
Expand All @@ -810,6 +811,7 @@ bool PublicKeyCipher::Cipher(
const ncrypto::Cipher::CipherParams params{
.padding = padding,
.digest = digest,
.mgf1_digest = mgf1_digest,
.label = label,
};

Expand Down Expand Up @@ -882,8 +884,17 @@ void PublicKeyCipher::Cipher(const FunctionCallbackInfo<Value>& args) {
if (!oaep_label.CheckSizeInt32()) [[unlikely]] {
return THROW_ERR_OUT_OF_RANGE(env, "oaepLabel is too big");
}

Digest mgf1_digest;
if (args[offset + 4]->IsString()) {
Utf8Value mgf1_str(env->isolate(), args[offset + 4]);
mgf1_digest = Digest::FromName(*mgf1_str);
if (!mgf1_digest) return THROW_ERR_OSSL_EVP_INVALID_DIGEST(env);
}

std::unique_ptr<BackingStore> out;
if (!Cipher<cipher>(env, pkey, padding, digest, oaep_label, buf, &out)) {
if (!Cipher<cipher>(
env, pkey, padding, digest, mgf1_digest, oaep_label, buf, &out)) {
return ThrowCryptoError(env, ERR_get_error());
}

Expand Down
1 change: 1 addition & 0 deletions src/crypto/crypto_cipher.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class PublicKeyCipher {
const ncrypto::EVPKeyPointer& pkey,
int padding,
const ncrypto::Digest& digest,
const ncrypto::Digest& mgf1_digest,
const ArrayBufferOrViewContents<unsigned char>& oaep_label,
const ArrayBufferOrViewContents<unsigned char>& data,
std::unique_ptr<v8::BackingStore>* out);
Expand Down
1 change: 1 addition & 0 deletions src/crypto/crypto_rsa.cc
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ WebCryptoCipherStatus RSA_Cipher(Environment* env,
const ncrypto::Rsa::CipherParams nparams{
.padding = params.padding,
.digest = params.digest,
.mgf1_digest = params.digest,
.label = params.label,
};

Expand Down
149 changes: 149 additions & 0 deletions test/parallel/test-crypto-rsa-oaep-mgf1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

// Tests the `mgf1Hash` option of crypto.publicEncrypt() and
// crypto.privateDecrypt(), which allows the MGF1 digest of RSA-OAEP padding to
// differ from the OAEP message digest (`oaepHash`). This is required for
// interoperability with profiles such as XML Encryption's `rsa-oaep-mgf1p`,
// where the OAEP digest may be changed but MGF1 is fixed to SHA-1.

const assert = require('assert');
const crypto = require('crypto');
const fixtures = require('../common/fixtures');
const { hasFIPS } = require('../common/crypto');

const constants = crypto.constants;

const publicKey = fixtures.readKey('rsa_public.pem', 'ascii');
const privateKey = fixtures.readKey('rsa_private.pem', 'ascii');

const input = Buffer.from('the quick brown fox jumps over the lazy dog');

// A round-trip with mismatched OAEP and MGF1 digests must succeed when both
// sides agree on the digests.
{
const encrypted = crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
mgf1Hash: 'sha1',
}, input);

const decrypted = crypto.privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
mgf1Hash: 'sha1',
}, encrypted);

assert.deepStrictEqual(decrypted, input);
}

// mgf1Hash actually affects the padding: a ciphertext produced with
// oaepHash=sha256 and mgf1Hash=sha1 must NOT decrypt when MGF1 defaults to the
// OAEP digest (sha256), which is the pre-existing behavior.
{
const encrypted = crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
mgf1Hash: 'sha1',
}, input);

assert.throws(() => {
crypto.privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
// No mgf1Hash: MGF1 follows oaepHash (sha256) and must fail to unpad.
}, encrypted);
}, {
code: hasFIPS(3, 5) ? 'ERR_OSSL_EVP_PROVIDER_ASYM_CIPHER_FAILURE' :
'ERR_OSSL_RSA_OAEP_DECODING_ERROR'
});
}

// Backward compatibility: omitting mgf1Hash on both sides keeps MGF1 == oaepHash
// (the historical behavior), so this round-trips, and setting mgf1Hash equal to
// oaepHash is equivalent to omitting it.
{
const encrypted = crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
}, input);

const decrypted = crypto.privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
mgf1Hash: 'sha256',
}, encrypted);

assert.deepStrictEqual(decrypted, input);
}

// The default oaepHash is sha1, so mgf1Hash defaults to sha1 as well. A
// ciphertext encrypted with all defaults must decrypt with an explicit
// mgf1Hash: 'sha1'.
{
const encrypted = crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
}, input);

const decrypted = crypto.privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
mgf1Hash: 'sha1',
}, encrypted);

assert.deepStrictEqual(decrypted, input);
}

// A few other digest combinations round-trip.
for (const [oaepHash, mgf1Hash] of [
['sha512', 'sha1'],
['sha384', 'sha256'],
['sha1', 'sha256'],
]) {
const encrypted = crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash,
mgf1Hash,
}, input);

const decrypted = crypto.privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash,
mgf1Hash,
}, encrypted);

assert.deepStrictEqual(decrypted, input);
}

// mgf1Hash must be a string.
for (const mgf1Hash of [1, true, {}, [], null]) {
assert.throws(() => {
crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
mgf1Hash,
}, input);
}, { code: 'ERR_INVALID_ARG_TYPE' });
}

// An unknown mgf1Hash digest name is rejected.
assert.throws(() => {
crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
mgf1Hash: 'not-a-real-digest',
}, input);
}, { code: 'ERR_OSSL_EVP_INVALID_DIGEST' });
1 change: 1 addition & 0 deletions typings/internalBinding/crypto.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,7 @@ declare namespace InternalCryptoBinding {
padding: number,
oaepHash: string | undefined,
oaepLabel: OptionalByteSource,
mgf1Hash: string | undefined,
]
) => Buffer;
}
Expand Down
Loading