Skip to content

Commit 7bd6d1e

Browse files
committed
crypto: support ML-DSA KeyObject, sign, and verify
1 parent 0ba6e0d commit 7bd6d1e

26 files changed

+1177
-25
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 48 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,32 @@ 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+
// TODO(@panva): use OSSL_PKEY_PARAM_ML_DSA_SEED instead of "seed"
2027+
if (EVP_PKEY_get_octet_string_param(
2028+
get(), "seed", buf.data, len, &seed_len) != 1)
2029+
return {};
2030+
return data;
2031+
}
2032+
return {};
2033+
}
2034+
#endif
2035+
20012036
DataPointer EVPKeyPointer::rawPrivateKey() const {
20022037
if (!pkey_) return {};
20032038
if (auto data = DataPointer::Alloc(rawPrivateKeySize())) {
@@ -2453,7 +2488,18 @@ bool EVPKeyPointer::isRsaVariant() const {
24532488
bool EVPKeyPointer::isOneShotVariant() const {
24542489
if (!pkey_) return false;
24552490
int type = id();
2456-
return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448;
2491+
switch (type) {
2492+
case EVP_PKEY_ED25519:
2493+
case EVP_PKEY_ED448:
2494+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
2495+
case EVP_PKEY_ML_DSA_44:
2496+
case EVP_PKEY_ML_DSA_65:
2497+
case EVP_PKEY_ML_DSA_87:
2498+
#endif
2499+
return true;
2500+
default:
2501+
return false;
2502+
}
24572503
}
24582504

24592505
bool EVPKeyPointer::isSigVariant() const {

deps/ncrypto/ncrypto.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,10 @@ class EVPKeyPointer final {
910910
DataPointer rawPrivateKey() const;
911911
BIOPointer derPublicKey() const;
912912

913+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
914+
DataPointer rawSeed() const;
915+
#endif
916+
913917
Result<BIOPointer, bool> writePrivateKey(
914918
const PrivateKeyEncodingConfig& config) const;
915919
Result<BIOPointer, bool> writePublicKey(

doc/api/crypto.md

Lines changed: 36 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.
@@ -3434,11 +3446,16 @@ must be an object with the properties described above.
34343446
If the private key is encrypted, a `passphrase` must be specified. The length
34353447
of the passphrase is limited to 1024 bytes.
34363448

3449+
Note: ML-DSA keys JWK import is not yet supported.
3450+
34373451
### `crypto.createPublicKey(key)`
34383452

34393453
<!-- YAML
34403454
added: v11.6.0
34413455
changes:
3456+
- version: REPLACEME
3457+
pr-url: https://github.com/nodejs/node/pull/59259
3458+
description: Add support for ML-DSA keys.
34423459
- version: v15.12.0
34433460
pr-url: https://github.com/nodejs/node/pull/37254
34443461
description: The key can also be a JWK object.
@@ -3484,6 +3501,8 @@ extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type
34843501
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
34853502
and it will be impossible to extract the private key from the returned object.
34863503

3504+
Note: ML-DSA keys JWK import is not yet supported.
3505+
34873506
### `crypto.createSecretKey(key[, encoding])`
34883507

34893508
<!-- YAML
@@ -3648,6 +3667,9 @@ underlying hash function. See [`crypto.createHmac()`][] for more information.
36483667
<!-- YAML
36493668
added: v10.12.0
36503669
changes:
3670+
- version: REPLACEME
3671+
pr-url: https://github.com/nodejs/node/pull/59259
3672+
description: Add support for ML-DSA key pairs.
36513673
- version: v18.0.0
36523674
pr-url: https://github.com/nodejs/node/pull/41678
36533675
description: Passing an invalid callback to the `callback` argument
@@ -3767,6 +3789,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties.
37673789
<!-- YAML
37683790
added: v10.12.0
37693791
changes:
3792+
- version: REPLACEME
3793+
pr-url: https://github.com/nodejs/node/pull/59259
3794+
description: Add support for ML-DSA key pairs.
37703795
- version: v16.10.0
37713796
pr-url: https://github.com/nodejs/node/pull/39927
37723797
description: Add ability to define `RSASSA-PSS-params` sequence parameters
@@ -3792,7 +3817,8 @@ changes:
37923817
-->
37933818

37943819
* `type`: {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`,
3795-
`'ed448'`, `'x25519'`, `'x448'`, or `'dh'`.
3820+
`'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35],
3821+
`'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35].
37963822
* `options`: {Object}
37973823
* `modulusLength`: {number} Key size in bits (RSA, DSA).
37983824
* `publicExponent`: {number} Public exponent (RSA). **Default:** `0x10001`.
@@ -3816,7 +3842,7 @@ changes:
38163842
* `privateKey`: {string | Buffer | KeyObject}
38173843

38183844
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.
3845+
Ed25519, Ed448, X25519, X448, DH, and ML-DSA[^openssl35] are currently supported.
38203846

38213847
If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
38223848
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
@@ -5416,6 +5442,9 @@ Throws an error if FIPS mode is not available.
54165442
<!-- YAML
54175443
added: v12.0.0
54185444
changes:
5445+
- version: REPLACEME
5446+
pr-url: https://github.com/nodejs/node/pull/59259
5447+
description: Add support for ML-DSA signing.
54195448
- version: v18.0.0
54205449
pr-url: https://github.com/nodejs/node/pull/41678
54215450
description: Passing an invalid callback to the `callback` argument
@@ -5526,6 +5555,9 @@ not introduce timing vulnerabilities.
55265555
<!-- YAML
55275556
added: v12.0.0
55285557
changes:
5558+
- version: REPLACEME
5559+
pr-url: https://github.com/nodejs/node/pull/59259
5560+
description: Add support for ML-DSA signature verification.
55295561
- version: v18.0.0
55305562
pr-url: https://github.com/nodejs/node/pull/41678
55315563
description: Passing an invalid callback to the `callback` argument
@@ -6150,6 +6182,8 @@ See the [list of SSL OP Flags][] for details.
61506182
</tr>
61516183
</table>
61526184

6185+
[^openssl35]: Requires OpenSSL >= 3.5
6186+
61536187
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
61546188
[CCM mode]: #ccm-mode
61556189
[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
{

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',

src/crypto/crypto_keys.cc

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#include "crypto/crypto_keys.h"
2+
#include "async_wrap-inl.h"
3+
#include "base_object-inl.h"
24
#include "crypto/crypto_common.h"
5+
#include "crypto/crypto_dh.h"
36
#include "crypto/crypto_dsa.h"
47
#include "crypto/crypto_ec.h"
5-
#include "crypto/crypto_dh.h"
8+
#include "crypto/crypto_ml_dsa.h"
69
#include "crypto/crypto_rsa.h"
710
#include "crypto/crypto_util.h"
8-
#include "async_wrap-inl.h"
9-
#include "base_object-inl.h"
1011
#include "env-inl.h"
1112
#include "memory_tracker-inl.h"
1213
#include "node.h"
@@ -176,6 +177,14 @@ bool ExportJWKAsymmetricKey(Environment* env,
176177
// Fall through
177178
case EVP_PKEY_X448:
178179
return ExportJWKEdKey(env, key, target);
180+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
181+
case EVP_PKEY_ML_DSA_44:
182+
// Fall through
183+
case EVP_PKEY_ML_DSA_65:
184+
// Fall through
185+
case EVP_PKEY_ML_DSA_87:
186+
return ExportJwkMlDsaKey(env, key, target);
187+
#endif
179188
}
180189
THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env);
181190
return false;
@@ -903,6 +912,14 @@ Local<Value> KeyObjectHandle::GetAsymmetricKeyType() const {
903912
return env()->crypto_x25519_string();
904913
case EVP_PKEY_X448:
905914
return env()->crypto_x448_string();
915+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
916+
case EVP_PKEY_ML_DSA_44:
917+
return env()->crypto_ml_dsa_44_string();
918+
case EVP_PKEY_ML_DSA_65:
919+
return env()->crypto_ml_dsa_65_string();
920+
case EVP_PKEY_ML_DSA_87:
921+
return env()->crypto_ml_dsa_87_string();
922+
#endif
906923
default:
907924
return Undefined(env()->isolate());
908925
}
@@ -1178,6 +1195,11 @@ void Initialize(Environment* env, Local<Object> target) {
11781195
NODE_DEFINE_CONSTANT(target, kWebCryptoKeyFormatJWK);
11791196
NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED25519);
11801197
NODE_DEFINE_CONSTANT(target, EVP_PKEY_ED448);
1198+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
1199+
NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_44);
1200+
NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_65);
1201+
NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_87);
1202+
#endif
11811203
NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519);
11821204
NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448);
11831205
NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1);

0 commit comments

Comments
 (0)