Skip to content

Commit 2739dba

Browse files
committed
crypto: support ML-DSA KeyObject, sign, and verify
1 parent 5335c10 commit 2739dba

27 files changed

+1299
-26
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1942,7 +1942,16 @@ EVP_PKEY* EVPKeyPointer::release() {
19421942

19431943
int EVPKeyPointer::id(const EVP_PKEY* key) {
19441944
if (key == nullptr) return 0;
1945-
return EVP_PKEY_id(key);
1945+
int type = EVP_PKEY_id(key);
1946+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
1947+
// https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870
1948+
if (type == -1) {
1949+
if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44;
1950+
if (EVP_PKEY_is_a(key, "ML-DSA-65")) return EVP_PKEY_ML_DSA_65;
1951+
if (EVP_PKEY_is_a(key, "ML-DSA-87")) return EVP_PKEY_ML_DSA_87;
1952+
}
1953+
#endif
1954+
return type;
19461955
}
19471956

19481957
int EVPKeyPointer::base_id(const EVP_PKEY* key) {
@@ -1998,6 +2007,31 @@ DataPointer EVPKeyPointer::rawPublicKey() const {
19982007
return {};
19992008
}
20002009

2010+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
2011+
DataPointer EVPKeyPointer::rawSeed() const {
2012+
if (!pkey_) return {};
2013+
switch (id()) {
2014+
case EVP_PKEY_ML_DSA_44:
2015+
case EVP_PKEY_ML_DSA_65:
2016+
case EVP_PKEY_ML_DSA_87:
2017+
break;
2018+
default:
2019+
unreachable();
2020+
}
2021+
2022+
size_t seed_len = 32;
2023+
if (auto data = DataPointer::Alloc(seed_len)) {
2024+
const Buffer<unsigned char> buf = data;
2025+
size_t len = data.size();
2026+
if (EVP_PKEY_get_octet_string_param(
2027+
get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1)
2028+
return {};
2029+
return data;
2030+
}
2031+
return {};
2032+
}
2033+
#endif
2034+
20012035
DataPointer EVPKeyPointer::rawPrivateKey() const {
20022036
if (!pkey_) return {};
20032037
if (auto data = DataPointer::Alloc(rawPrivateKeySize())) {
@@ -2453,7 +2487,18 @@ bool EVPKeyPointer::isRsaVariant() const {
24532487
bool EVPKeyPointer::isOneShotVariant() const {
24542488
if (!pkey_) return false;
24552489
int type = id();
2456-
return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448;
2490+
switch (type) {
2491+
case EVP_PKEY_ED25519:
2492+
case EVP_PKEY_ED448:
2493+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
2494+
case EVP_PKEY_ML_DSA_44:
2495+
case EVP_PKEY_ML_DSA_65:
2496+
case EVP_PKEY_ML_DSA_87:
2497+
#endif
2498+
return true;
2499+
default:
2500+
return false;
2501+
}
24572502
}
24582503

24592504
bool EVPKeyPointer::isSigVariant() const {

deps/ncrypto/ncrypto.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030

3131
#if OPENSSL_VERSION_MAJOR >= 3
3232
#define OSSL3_CONST const
33+
#if OPENSSL_VERSION_MINOR >= 5
34+
#include <openssl/core_names.h>
35+
#endif
3336
#else
3437
#define OSSL3_CONST
3538
#endif
@@ -910,6 +913,10 @@ class EVPKeyPointer final {
910913
DataPointer rawPrivateKey() const;
911914
BIOPointer derPublicKey() const;
912915

916+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
917+
DataPointer rawSeed() const;
918+
#endif
919+
913920
Result<BIOPointer, bool> writePrivateKey(
914921
const PrivateKeyEncodingConfig& config) const;
915922
Result<BIOPointer, bool> writePublicKey(

doc/api/crypto.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,6 +1916,9 @@ This can be called many times with new data as it is streamed.
19161916
<!-- YAML
19171917
added: v11.6.0
19181918
changes:
1919+
- version: REPLACEME
1920+
pr-url: https://github.com/nodejs/node/pull/59259
1921+
description: Add support for ML-DSA keys.
19191922
- version:
19201923
- v14.5.0
19211924
- v12.19.0
@@ -2021,6 +2024,9 @@ Other key details might be exposed via this API using additional attributes.
20212024
<!-- YAML
20222025
added: v11.6.0
20232026
changes:
2027+
- version: REPLACEME
2028+
pr-url: https://github.com/nodejs/node/pull/59259
2029+
description: Add support for ML-DSA keys.
20242030
- version:
20252031
- v13.9.0
20262032
- v12.17.0
@@ -2055,6 +2061,9 @@ types are:
20552061
* `'ed25519'` (OID 1.3.101.112)
20562062
* `'ed448'` (OID 1.3.101.113)
20572063
* `'dh'` (OID 1.2.840.113549.1.3.1)
2064+
* `'ml-dsa-44'`[^openssl35] (OID 2.16.840.1.101.3.4.3.17)
2065+
* `'ml-dsa-65'`[^openssl35] (OID 2.16.840.1.101.3.4.3.18)
2066+
* `'ml-dsa-87'`[^openssl35] (OID 2.16.840.1.101.3.4.3.19)
20582067

20592068
This property is `undefined` for unrecognized `KeyObject` types and symmetric
20602069
keys.
@@ -3403,6 +3412,9 @@ input.on('readable', () => {
34033412
<!-- YAML
34043413
added: v11.6.0
34053414
changes:
3415+
- version: REPLACEME
3416+
pr-url: https://github.com/nodejs/node/pull/59259
3417+
description: Add support for ML-DSA keys.
34063418
- version: v15.12.0
34073419
pr-url: https://github.com/nodejs/node/pull/37254
34083420
description: The key can also be a JWK object.
@@ -3439,6 +3451,9 @@ of the passphrase is limited to 1024 bytes.
34393451
<!-- YAML
34403452
added: v11.6.0
34413453
changes:
3454+
- version: REPLACEME
3455+
pr-url: https://github.com/nodejs/node/pull/59259
3456+
description: Add support for ML-DSA keys.
34423457
- version: v15.12.0
34433458
pr-url: https://github.com/nodejs/node/pull/37254
34443459
description: The key can also be a JWK object.
@@ -3648,6 +3663,9 @@ underlying hash function. See [`crypto.createHmac()`][] for more information.
36483663
<!-- YAML
36493664
added: v10.12.0
36503665
changes:
3666+
- version: REPLACEME
3667+
pr-url: https://github.com/nodejs/node/pull/59259
3668+
description: Add support for ML-DSA key pairs.
36513669
- version: v18.0.0
36523670
pr-url: https://github.com/nodejs/node/pull/41678
36533671
description: Passing an invalid callback to the `callback` argument
@@ -3767,6 +3785,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties.
37673785
<!-- YAML
37683786
added: v10.12.0
37693787
changes:
3788+
- version: REPLACEME
3789+
pr-url: https://github.com/nodejs/node/pull/59259
3790+
description: Add support for ML-DSA key pairs.
37703791
- version: v16.10.0
37713792
pr-url: https://github.com/nodejs/node/pull/39927
37723793
description: Add ability to define `RSASSA-PSS-params` sequence parameters
@@ -3792,7 +3813,8 @@ changes:
37923813
-->
37933814

37943815
* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`,
3795-
`'ed448'`, `'x25519'`, `'x448'`, or `'dh'`.
3816+
`'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35],
3817+
`'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35].
37963818
* `options` {Object}
37973819
* `modulusLength` {number} Key size in bits (RSA, DSA).
37983820
* `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`.
@@ -3816,7 +3838,7 @@ changes:
38163838
* `privateKey` {string | Buffer | KeyObject}
38173839

38183840
Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC,
3819-
Ed25519, Ed448, X25519, X448, and DH are currently supported.
3841+
Ed25519, Ed448, X25519, X448, DH, and ML-DSA[^openssl35] are currently supported.
38203842

38213843
If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
38223844
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
@@ -5416,6 +5438,9 @@ Throws an error if FIPS mode is not available.
54165438
<!-- YAML
54175439
added: v12.0.0
54185440
changes:
5441+
- version: REPLACEME
5442+
pr-url: https://github.com/nodejs/node/pull/59259
5443+
description: Add support for ML-DSA signing.
54195444
- version: v18.0.0
54205445
pr-url: https://github.com/nodejs/node/pull/41678
54215446
description: Passing an invalid callback to the `callback` argument
@@ -5526,6 +5551,9 @@ not introduce timing vulnerabilities.
55265551
<!-- YAML
55275552
added: v12.0.0
55285553
changes:
5554+
- version: REPLACEME
5555+
pr-url: https://github.com/nodejs/node/pull/59259
5556+
description: Add support for ML-DSA signature verification.
55295557
- version: v18.0.0
55305558
pr-url: https://github.com/nodejs/node/pull/41678
55315559
description: Passing an invalid callback to the `callback` argument
@@ -6150,6 +6178,8 @@ See the [list of SSL OP Flags][] for details.
61506178
</tr>
61516179
</table>
61526180

6181+
[^openssl35]: Requires OpenSSL >= 3.5
6182+
61536183
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
61546184
[CCM mode]: #ccm-mode
61556185
[CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532

lib/internal/crypto/keygen.js

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const {
1919
kKeyVariantRSA_SSA_PKCS1_v1_5,
2020
EVP_PKEY_ED25519,
2121
EVP_PKEY_ED448,
22+
EVP_PKEY_ML_DSA_44,
23+
EVP_PKEY_ML_DSA_65,
24+
EVP_PKEY_ML_DSA_87,
2225
EVP_PKEY_X25519,
2326
EVP_PKEY_X448,
2427
OPENSSL_EC_NAMED_CURVE,
@@ -162,6 +165,16 @@ function parseKeyEncoding(keyType, options = kEmptyObject) {
162165
];
163166
}
164167

168+
const ids = {
169+
'ed25519': EVP_PKEY_ED25519,
170+
'ed448': EVP_PKEY_ED448,
171+
'x25519': EVP_PKEY_X25519,
172+
'x448': EVP_PKEY_X448,
173+
'ml-dsa-44': EVP_PKEY_ML_DSA_44,
174+
'ml-dsa-65': EVP_PKEY_ML_DSA_65,
175+
'ml-dsa-87': EVP_PKEY_ML_DSA_87,
176+
};
177+
165178
function createJob(mode, type, options) {
166179
validateString(type, 'type');
167180

@@ -278,23 +291,14 @@ function createJob(mode, type, options) {
278291
case 'ed448':
279292
case 'x25519':
280293
case 'x448':
294+
case 'ml-dsa-44':
295+
case 'ml-dsa-65':
296+
case 'ml-dsa-87':
281297
{
282-
let id;
283-
switch (type) {
284-
case 'ed25519':
285-
id = EVP_PKEY_ED25519;
286-
break;
287-
case 'ed448':
288-
id = EVP_PKEY_ED448;
289-
break;
290-
case 'x25519':
291-
id = EVP_PKEY_X25519;
292-
break;
293-
case 'x448':
294-
id = EVP_PKEY_X448;
295-
break;
298+
if (ids[type] === undefined) {
299+
throw new ERR_INVALID_ARG_VALUE('type', type, 'must be a supported key type');
296300
}
297-
return new NidKeyPairGenJob(mode, id, ...encoding);
301+
return new NidKeyPairGenJob(mode, ids[type], ...encoding);
298302
}
299303
case 'dh':
300304
{

lib/internal/crypto/keys.js

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,12 +509,90 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
509509
return types;
510510
}
511511

512+
const oids = {
513+
'ML-DSA-44': [96, 134, 72, 1, 101, 3, 4, 3, 17],
514+
'ML-DSA-65': [96, 134, 72, 1, 101, 3, 4, 3, 18],
515+
'ML-DSA-87': [96, 134, 72, 1, 101, 3, 4, 3, 19],
516+
};
517+
518+
function mlDsaPubLen(alg) {
519+
switch (alg) {
520+
case 'ML-DSA-44': return 1312;
521+
case 'ML-DSA-65': return 1952;
522+
case 'ML-DSA-87': return 2592;
523+
}
524+
}
525+
526+
/**
527+
* Encodes length for use in DER
528+
* @param {number} length
529+
* @returns {number[]}
530+
*/
531+
function encodeLength(length) {
532+
if (length < 128) {
533+
return [length];
534+
}
535+
536+
const bytes = [];
537+
let temp = length;
538+
while (temp > 0) {
539+
bytes.unshift(temp & 0xff);
540+
temp >>>= 8;
541+
}
542+
543+
return [0x80 | bytes.length, ...bytes];
544+
}
545+
512546
function getKeyObjectHandleFromJwk(key, ctx) {
513547
validateObject(key, 'key');
514548
validateOneOf(
515-
key.kty, 'key.kty', ['RSA', 'EC', 'OKP']);
549+
key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']);
516550
const isPublic = ctx === kConsumePublic || ctx === kCreatePublic;
517551

552+
if (key.kty === 'AKP') {
553+
validateOneOf(
554+
key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']);
555+
validateString(key.pub, 'key.pub');
556+
557+
if (!isPublic)
558+
validateString(key.priv, 'key.priv');
559+
560+
let keyData = Buffer.from(key.pub, 'base64url');
561+
if (keyData.byteLength !== mlDsaPubLen(key.alg)) {
562+
throw new ERR_CRYPTO_INVALID_JWK();
563+
}
564+
565+
let encoding;
566+
let bytes;
567+
// Uses seed when available for both kKeyTypePublic and kKeyTypePrivate
568+
if (key.priv) {
569+
keyData = Buffer.from(key.priv, 'base64url');
570+
if (keyData.byteLength !== 32) {
571+
throw new ERR_CRYPTO_INVALID_JWK();
572+
}
573+
574+
bytes = [48, 52, 2, 1, 0, 48, 11, 6, 9, ...oids[key.alg], 4, 34, 128, 32, ...keyData];
575+
encoding = kKeyEncodingPKCS8;
576+
} else {
577+
bytes = [
578+
48, ...encodeLength(keyData.length + 18),
579+
48, 11, 6, 9, ...oids[key.alg],
580+
3, ...encodeLength(keyData.length + 1),
581+
0, ...keyData,
582+
];
583+
encoding = kKeyEncodingSPKI;
584+
}
585+
586+
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
587+
const handle = new KeyObjectHandle();
588+
try {
589+
handle.init(keyType, new Uint8Array(bytes), kKeyFormatDER, encoding, null);
590+
} catch {
591+
throw new ERR_CRYPTO_INVALID_JWK();
592+
}
593+
return handle;
594+
}
595+
518596
if (key.kty === 'OKP') {
519597
validateString(key.crv, 'key.crv');
520598
validateOneOf(

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@
336336
'src/crypto/crypto_cipher.cc',
337337
'src/crypto/crypto_context.cc',
338338
'src/crypto/crypto_ec.cc',
339+
'src/crypto/crypto_ml_dsa.cc',
339340
'src/crypto/crypto_hmac.cc',
340341
'src/crypto/crypto_random.cc',
341342
'src/crypto/crypto_rsa.cc',
@@ -367,6 +368,7 @@
367368
'src/crypto/crypto_clienthello.h',
368369
'src/crypto/crypto_context.h',
369370
'src/crypto/crypto_ec.h',
371+
'src/crypto/crypto_ml_dsa.h',
370372
'src/crypto/crypto_hkdf.h',
371373
'src/crypto/crypto_pbkdf2.h',
372374
'src/crypto/crypto_sig.h',

0 commit comments

Comments
 (0)