crypto: support Ed448 and ML-DSA context parameter in node:crypto

PR-URL: https://github.com/nodejs/node/pull/59570
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Filip Skokan 2025-08-23 17:04:49 +02:00 committed by Node.js GitHub Bot
parent 0124e0e0d7
commit a3cd430ef8
4 changed files with 98 additions and 5 deletions

View File

@ -5712,6 +5712,9 @@ Throws an error if FIPS mode is not available.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59570
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59537
description: Add support for SLH-DSA signing.
@ -5772,6 +5775,9 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
this option specifies the optional context to differentiate signatures generated
for different purposes with the same key.
If the `callback` function is provided this function uses libuv's threadpool.
@ -5831,6 +5837,9 @@ not introduce timing vulnerabilities.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59570
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59537
description: Add support for SLH-DSA signature verification.
@ -5897,6 +5906,9 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
this option specifies the optional context to differentiate signatures generated
for different purposes with the same key.
The `signature` argument is the previously calculated signature for the `data`.

View File

@ -101,6 +101,19 @@ function getDSASignatureEncoding(options) {
return kSigEncDER;
}
function getContext(options) {
if (options?.context === undefined) {
return undefined;
}
if (!isArrayBufferView(options.context)) {
throw new ERR_INVALID_ARG_TYPE(
'options.context', ['Buffer', 'TypedArray', 'DataView'], options.context);
}
return options.context;
}
function getIntOption(name, options) {
const value = options[name];
if (value !== undefined) {
@ -153,6 +166,9 @@ function signOneShot(algorithm, data, key, callback) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);
// Options specific to Ed448 and ML-DSA
const context = getContext(key);
const {
data: keyData,
format: keyFormat,
@ -172,7 +188,7 @@ function signOneShot(algorithm, data, key, callback) {
pssSaltLength,
rsaPadding,
dsaSigEnc,
undefined,
context,
undefined);
if (!callback) {
@ -251,6 +267,9 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);
// Options specific to Ed448 and ML-DSA
const context = getContext(key);
if (!isArrayBufferView(signature)) {
throw new ERR_INVALID_ARG_TYPE(
'signature',
@ -278,7 +297,7 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
pssSaltLength,
rsaPadding,
dsaSigEnc,
undefined,
context,
signature);
if (!callback) {

View File

@ -246,6 +246,18 @@ bool SupportsContextString(const EVPKeyPointer& key) {
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87:
case EVP_PKEY_SLH_DSA_SHA2_128F:
case EVP_PKEY_SLH_DSA_SHA2_128S:
case EVP_PKEY_SLH_DSA_SHA2_192F:
case EVP_PKEY_SLH_DSA_SHA2_192S:
case EVP_PKEY_SLH_DSA_SHA2_256F:
case EVP_PKEY_SLH_DSA_SHA2_256S:
case EVP_PKEY_SLH_DSA_SHAKE_128F:
case EVP_PKEY_SLH_DSA_SHAKE_128S:
case EVP_PKEY_SLH_DSA_SHAKE_192F:
case EVP_PKEY_SLH_DSA_SHAKE_192S:
case EVP_PKEY_SLH_DSA_SHAKE_256F:
case EVP_PKEY_SLH_DSA_SHAKE_256S:
#endif
return true;
default:

View File

@ -9,7 +9,7 @@ const exec = require('child_process').exec;
const crypto = require('crypto');
const fixtures = require('../common/fixtures');
const {
hasOpenSSL3,
hasOpenSSL,
opensslCli,
} = require('../common/crypto');
@ -66,7 +66,7 @@ const keySize = 2048;
key: keyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
});
}, { message: hasOpenSSL3 ?
}, { message: hasOpenSSL(3) ?
'error:1C8000A5:Provider routines::illegal or unsupported padding mode' :
'bye, bye, error stack' });
@ -344,7 +344,7 @@ assert.throws(
key: keyPem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
});
}, hasOpenSSL3 ? {
}, hasOpenSSL(3) ? {
code: 'ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE',
message: /illegal or unsupported padding mode/,
} : {
@ -426,6 +426,7 @@ assert.throws(
{ private: fixtures.readKey('ed448_private.pem', 'ascii'),
public: fixtures.readKey('ed448_public.pem', 'ascii'),
algo: null,
supportsContext: true,
sigLen: 114 },
{ private: fixtures.readKey('rsa_private_2048.pem', 'ascii'),
public: fixtures.readKey('rsa_public_2048.pem', 'ascii'),
@ -473,6 +474,55 @@ assert.throws(
assert.strictEqual(crypto.verify(algo, data, pair.private, sig),
true);
});
if (pair.supportsContext && hasOpenSSL(3, 2)) {
const data = Buffer.from('Hello world');
{
const context = new Uint8Array();
const sig = crypto.sign(algo, data, { key: pair.private, context });
assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), true);
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true);
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false);
}
{
const context = new Uint8Array(32);
const sig = crypto.sign(algo, data, { key: pair.private, context });
assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), false);
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true);
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false);
}
assert.throws(() => crypto.sign(algo, data, { key: pair.private, context: new Uint8Array(256) }), {
code: 'ERR_OUT_OF_RANGE',
message: 'context string must be at most 255 bytes',
});
assert.throws(() => {
crypto.verify(algo, data, { key: pair.public, context: new Uint8Array(256) }, new Uint8Array());
}, {
code: 'ERR_OUT_OF_RANGE',
message: 'context string must be at most 255 bytes',
});
} else if (pair.supportsContext) {
const data = Buffer.from('Hello world');
{
const context = new Uint8Array();
const sig = crypto.sign(algo, data, { key: pair.private, context });
assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), true);
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true);
}
{
const context = new Uint8Array(32);
assert.throws(() => {
crypto.sign(algo, data, { key: pair.private, context });
}, { message: 'Context parameter is unsupported' });
assert.throws(() => {
crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, crypto.randomBytes(32));
}, { message: 'Context parameter is unsupported' });
}
}
});
[1, {}, [], true, Infinity].forEach((input) => {