Skip to content

Commit a08d556

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

26 files changed

+1173
-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: 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
{

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)