Skip to content

Commit 7a8e2c2

Browse files
panvatargos
authored andcommitted
crypto: support Ed448 and ML-DSA context parameter in node:crypto
PR-URL: #59570 Reviewed-By: James M Snell <[email protected]>
1 parent 4b631be commit 7a8e2c2

File tree

4 files changed

+98
-5
lines changed

4 files changed

+98
-5
lines changed

doc/api/crypto.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5712,6 +5712,9 @@ Throws an error if FIPS mode is not available.
57125712
<!-- YAML
57135713
added: v12.0.0
57145714
changes:
5715+
- version: REPLACEME
5716+
pr-url: https://github.com/nodejs/node/pull/59570
5717+
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
57155718
- version: REPLACEME
57165719
pr-url: https://github.com/nodejs/node/pull/59537
57175720
description: Add support for SLH-DSA signing.
@@ -5772,6 +5775,9 @@ additional properties can be passed:
57725775
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
57735776
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
57745777
maximum permissible value.
5778+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
5779+
this option specifies the optional context to differentiate signatures generated
5780+
for different purposes with the same key.
57755781

57765782
If the `callback` function is provided this function uses libuv's threadpool.
57775783

@@ -5831,6 +5837,9 @@ not introduce timing vulnerabilities.
58315837
<!-- YAML
58325838
added: v12.0.0
58335839
changes:
5840+
- version: REPLACEME
5841+
pr-url: https://github.com/nodejs/node/pull/59570
5842+
description: Add support for ML-DSA, Ed448, and SLH-DSA context parameter.
58345843
- version: REPLACEME
58355844
pr-url: https://github.com/nodejs/node/pull/59537
58365845
description: Add support for SLH-DSA signature verification.
@@ -5897,6 +5906,9 @@ additional properties can be passed:
58975906
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
58985907
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
58995908
maximum permissible value.
5909+
* `context` {ArrayBuffer|Buffer|TypedArray|DataView} For Ed448, ML-DSA, and SLH-DSA,
5910+
this option specifies the optional context to differentiate signatures generated
5911+
for different purposes with the same key.
59005912

59015913
The `signature` argument is the previously calculated signature for the `data`.
59025914

lib/internal/crypto/sig.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,19 @@ function getDSASignatureEncoding(options) {
101101
return kSigEncDER;
102102
}
103103

104+
function getContext(options) {
105+
if (options?.context === undefined) {
106+
return undefined;
107+
}
108+
109+
if (!isArrayBufferView(options.context)) {
110+
throw new ERR_INVALID_ARG_TYPE(
111+
'options.context', ['Buffer', 'TypedArray', 'DataView'], options.context);
112+
}
113+
114+
return options.context;
115+
}
116+
104117
function getIntOption(name, options) {
105118
const value = options[name];
106119
if (value !== undefined) {
@@ -153,6 +166,9 @@ function signOneShot(algorithm, data, key, callback) {
153166
// Options specific to (EC)DSA
154167
const dsaSigEnc = getDSASignatureEncoding(key);
155168

169+
// Options specific to Ed448 and ML-DSA
170+
const context = getContext(key);
171+
156172
const {
157173
data: keyData,
158174
format: keyFormat,
@@ -172,7 +188,7 @@ function signOneShot(algorithm, data, key, callback) {
172188
pssSaltLength,
173189
rsaPadding,
174190
dsaSigEnc,
175-
undefined,
191+
context,
176192
undefined);
177193

178194
if (!callback) {
@@ -251,6 +267,9 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
251267
// Options specific to (EC)DSA
252268
const dsaSigEnc = getDSASignatureEncoding(key);
253269

270+
// Options specific to Ed448 and ML-DSA
271+
const context = getContext(key);
272+
254273
if (!isArrayBufferView(signature)) {
255274
throw new ERR_INVALID_ARG_TYPE(
256275
'signature',
@@ -278,7 +297,7 @@ function verifyOneShot(algorithm, data, key, signature, callback) {
278297
pssSaltLength,
279298
rsaPadding,
280299
dsaSigEnc,
281-
undefined,
300+
context,
282301
signature);
283302

284303
if (!callback) {

src/crypto/crypto_sig.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,18 @@ bool SupportsContextString(const EVPKeyPointer& key) {
246246
case EVP_PKEY_ML_DSA_44:
247247
case EVP_PKEY_ML_DSA_65:
248248
case EVP_PKEY_ML_DSA_87:
249+
case EVP_PKEY_SLH_DSA_SHA2_128F:
250+
case EVP_PKEY_SLH_DSA_SHA2_128S:
251+
case EVP_PKEY_SLH_DSA_SHA2_192F:
252+
case EVP_PKEY_SLH_DSA_SHA2_192S:
253+
case EVP_PKEY_SLH_DSA_SHA2_256F:
254+
case EVP_PKEY_SLH_DSA_SHA2_256S:
255+
case EVP_PKEY_SLH_DSA_SHAKE_128F:
256+
case EVP_PKEY_SLH_DSA_SHAKE_128S:
257+
case EVP_PKEY_SLH_DSA_SHAKE_192F:
258+
case EVP_PKEY_SLH_DSA_SHAKE_192S:
259+
case EVP_PKEY_SLH_DSA_SHAKE_256F:
260+
case EVP_PKEY_SLH_DSA_SHAKE_256S:
249261
#endif
250262
return true;
251263
default:

test/parallel/test-crypto-sign-verify.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const exec = require('child_process').exec;
99
const crypto = require('crypto');
1010
const fixtures = require('../common/fixtures');
1111
const {
12-
hasOpenSSL3,
12+
hasOpenSSL,
1313
opensslCli,
1414
} = require('../common/crypto');
1515

@@ -66,7 +66,7 @@ const keySize = 2048;
6666
key: keyPem,
6767
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
6868
});
69-
}, { message: hasOpenSSL3 ?
69+
}, { message: hasOpenSSL(3) ?
7070
'error:1C8000A5:Provider routines::illegal or unsupported padding mode' :
7171
'bye, bye, error stack' });
7272

@@ -344,7 +344,7 @@ assert.throws(
344344
key: keyPem,
345345
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
346346
});
347-
}, hasOpenSSL3 ? {
347+
}, hasOpenSSL(3) ? {
348348
code: 'ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE',
349349
message: /illegal or unsupported padding mode/,
350350
} : {
@@ -426,6 +426,7 @@ assert.throws(
426426
{ private: fixtures.readKey('ed448_private.pem', 'ascii'),
427427
public: fixtures.readKey('ed448_public.pem', 'ascii'),
428428
algo: null,
429+
supportsContext: true,
429430
sigLen: 114 },
430431
{ private: fixtures.readKey('rsa_private_2048.pem', 'ascii'),
431432
public: fixtures.readKey('rsa_public_2048.pem', 'ascii'),
@@ -473,6 +474,55 @@ assert.throws(
473474
assert.strictEqual(crypto.verify(algo, data, pair.private, sig),
474475
true);
475476
});
477+
478+
if (pair.supportsContext && hasOpenSSL(3, 2)) {
479+
const data = Buffer.from('Hello world');
480+
{
481+
const context = new Uint8Array();
482+
const sig = crypto.sign(algo, data, { key: pair.private, context });
483+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), true);
484+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true);
485+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false);
486+
}
487+
488+
{
489+
const context = new Uint8Array(32);
490+
const sig = crypto.sign(algo, data, { key: pair.private, context });
491+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), false);
492+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true);
493+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false);
494+
}
495+
496+
assert.throws(() => crypto.sign(algo, data, { key: pair.private, context: new Uint8Array(256) }), {
497+
code: 'ERR_OUT_OF_RANGE',
498+
message: 'context string must be at most 255 bytes',
499+
});
500+
501+
assert.throws(() => {
502+
crypto.verify(algo, data, { key: pair.public, context: new Uint8Array(256) }, new Uint8Array());
503+
}, {
504+
code: 'ERR_OUT_OF_RANGE',
505+
message: 'context string must be at most 255 bytes',
506+
});
507+
} else if (pair.supportsContext) {
508+
const data = Buffer.from('Hello world');
509+
{
510+
const context = new Uint8Array();
511+
const sig = crypto.sign(algo, data, { key: pair.private, context });
512+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), true);
513+
assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true);
514+
}
515+
516+
{
517+
const context = new Uint8Array(32);
518+
assert.throws(() => {
519+
crypto.sign(algo, data, { key: pair.private, context });
520+
}, { message: 'Context parameter is unsupported' });
521+
assert.throws(() => {
522+
crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, crypto.randomBytes(32));
523+
}, { message: 'Context parameter is unsupported' });
524+
}
525+
}
476526
});
477527

478528
[1, {}, [], true, Infinity].forEach((input) => {

0 commit comments

Comments
 (0)