Skip to content

Commit 38aedfb

Browse files
panvaRafaelGSS
authored andcommitted
crypto: support ML-DSA KeyObject, sign, and verify
PR-URL: #59259 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 41b4f4d commit 38aedfb

31 files changed

+1479
-44
lines changed

benchmark/crypto/create-keyobject.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ function readKeyPair(publicKeyName, privateKeyName) {
1818
}
1919

2020
const keyFixtures = {
21-
ec: readKeyPair('ec_p256_public', 'ec_p256_private'),
22-
rsa: readKeyPair('rsa_public_2048', 'rsa_private_2048'),
23-
ed25519: readKeyPair('ed25519_public', 'ed25519_private'),
21+
'ec': readKeyPair('ec_p256_public', 'ec_p256_private'),
22+
'rsa': readKeyPair('rsa_public_2048', 'rsa_private_2048'),
23+
'ed25519': readKeyPair('ed25519_public', 'ed25519_private'),
24+
'ml-dsa-44': readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'),
2425
};
2526

2627
const bench = common.createBenchmark(main, {
27-
keyType: ['rsa', 'ec', 'ed25519'],
28+
keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'],
2829
keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private'],
2930
n: [1e3],
3031
});

benchmark/crypto/oneshot-sign.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ const fs = require('fs');
66
const path = require('path');
77
const fixtures_keydir = path.resolve(__dirname, '../../test/fixtures/keys/');
88

9+
function readKey(name) {
10+
return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8');
11+
}
12+
913
const keyFixtures = {
10-
ec: fs.readFileSync(`${fixtures_keydir}/ec_p256_private.pem`, 'utf-8'),
11-
rsa: fs.readFileSync(`${fixtures_keydir}/rsa_private_2048.pem`, 'utf-8'),
12-
ed25519: fs.readFileSync(`${fixtures_keydir}/ed25519_private.pem`, 'utf-8'),
14+
'ec': readKey('ec_p256_private'),
15+
'rsa': readKey('rsa_private_2048'),
16+
'ed25519': readKey('ed25519_private'),
17+
'ml-dsa-44': readKey('ml_dsa_44_private'),
1318
};
1419

1520
const data = crypto.randomBytes(256);
@@ -18,7 +23,7 @@ let pems;
1823
let keyObjects;
1924

2025
const bench = common.createBenchmark(main, {
21-
keyType: ['rsa', 'ec', 'ed25519'],
26+
keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'],
2227
mode: ['sync', 'async', 'async-parallel'],
2328
keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'],
2429
n: [1e3],
@@ -90,6 +95,7 @@ function main({ n, mode, keyFormat, keyType }) {
9095
digest = 'sha256';
9196
break;
9297
case 'ed25519':
98+
case 'ml-dsa-44':
9399
break;
94100
default:
95101
throw new Error('not implemented');

benchmark/crypto/oneshot-verify.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ function readKeyPair(publicKeyName, privateKeyName) {
1818
}
1919

2020
const keyFixtures = {
21-
ec: readKeyPair('ec_p256_public', 'ec_p256_private'),
22-
rsa: readKeyPair('rsa_public_2048', 'rsa_private_2048'),
23-
ed25519: readKeyPair('ed25519_public', 'ed25519_private'),
21+
'ec': readKeyPair('ec_p256_public', 'ec_p256_private'),
22+
'rsa': readKeyPair('rsa_public_2048', 'rsa_private_2048'),
23+
'ed25519': readKeyPair('ed25519_public', 'ed25519_private'),
24+
'ml-dsa-44': readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'),
2425
};
2526

2627
const data = crypto.randomBytes(256);
@@ -29,7 +30,7 @@ let pems;
2930
let keyObjects;
3031

3132
const bench = common.createBenchmark(main, {
32-
keyType: ['rsa', 'ec', 'ed25519'],
33+
keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'],
3334
mode: ['sync', 'async', 'async-parallel'],
3435
keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'],
3536
n: [1e3],
@@ -104,6 +105,7 @@ function main({ n, mode, keyFormat, keyType }) {
104105
digest = 'sha256';
105106
break;
106107
case 'ed25519':
108+
case 'ml-dsa-44':
107109
break;
108110
default:
109111
throw new Error('not implemented');

deps/ncrypto/ncrypto.cc

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,31 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate(
18971897
EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len));
18981898
}
18991899

1900+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
1901+
EVPKeyPointer EVPKeyPointer::NewRawSeed(
1902+
int id, const Buffer<const unsigned char>& data) {
1903+
if (id == 0) return {};
1904+
1905+
OSSL_PARAM params[] = {
1906+
OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED,
1907+
const_cast<unsigned char*>(data.data),
1908+
data.len),
1909+
OSSL_PARAM_END};
1910+
1911+
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr);
1912+
if (ctx == nullptr) return {};
1913+
1914+
EVP_PKEY* pkey = nullptr;
1915+
if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 ||
1916+
EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) {
1917+
EVP_PKEY_CTX_free(ctx);
1918+
return {};
1919+
}
1920+
1921+
return EVPKeyPointer(pkey);
1922+
}
1923+
#endif
1924+
19001925
EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) {
19011926
if (!dh) return {};
19021927
auto key = New();
@@ -1942,7 +1967,16 @@ EVP_PKEY* EVPKeyPointer::release() {
19421967

19431968
int EVPKeyPointer::id(const EVP_PKEY* key) {
19441969
if (key == nullptr) return 0;
1945-
return EVP_PKEY_id(key);
1970+
int type = EVP_PKEY_id(key);
1971+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
1972+
// https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870
1973+
if (type == -1) {
1974+
if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44;
1975+
if (EVP_PKEY_is_a(key, "ML-DSA-65")) return EVP_PKEY_ML_DSA_65;
1976+
if (EVP_PKEY_is_a(key, "ML-DSA-87")) return EVP_PKEY_ML_DSA_87;
1977+
}
1978+
#endif
1979+
return type;
19461980
}
19471981

19481982
int EVPKeyPointer::base_id(const EVP_PKEY* key) {
@@ -1998,6 +2032,31 @@ DataPointer EVPKeyPointer::rawPublicKey() const {
19982032
return {};
19992033
}
20002034

2035+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
2036+
DataPointer EVPKeyPointer::rawSeed() const {
2037+
if (!pkey_) return {};
2038+
switch (id()) {
2039+
case EVP_PKEY_ML_DSA_44:
2040+
case EVP_PKEY_ML_DSA_65:
2041+
case EVP_PKEY_ML_DSA_87:
2042+
break;
2043+
default:
2044+
unreachable();
2045+
}
2046+
2047+
size_t seed_len = 32;
2048+
if (auto data = DataPointer::Alloc(seed_len)) {
2049+
const Buffer<unsigned char> buf = data;
2050+
size_t len = data.size();
2051+
if (EVP_PKEY_get_octet_string_param(
2052+
get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1)
2053+
return {};
2054+
return data;
2055+
}
2056+
return {};
2057+
}
2058+
#endif
2059+
20012060
DataPointer EVPKeyPointer::rawPrivateKey() const {
20022061
if (!pkey_) return {};
20032062
if (auto data = DataPointer::Alloc(rawPrivateKeySize())) {
@@ -2453,7 +2512,18 @@ bool EVPKeyPointer::isRsaVariant() const {
24532512
bool EVPKeyPointer::isOneShotVariant() const {
24542513
if (!pkey_) return false;
24552514
int type = id();
2456-
return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448;
2515+
switch (type) {
2516+
case EVP_PKEY_ED25519:
2517+
case EVP_PKEY_ED448:
2518+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
2519+
case EVP_PKEY_ML_DSA_44:
2520+
case EVP_PKEY_ML_DSA_65:
2521+
case EVP_PKEY_ML_DSA_87:
2522+
#endif
2523+
return true;
2524+
default:
2525+
return false;
2526+
}
24572527
}
24582528

24592529
bool EVPKeyPointer::isSigVariant() const {

deps/ncrypto/ncrypto.h

Lines changed: 11 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
@@ -817,6 +820,10 @@ class EVPKeyPointer final {
817820
const Buffer<const unsigned char>& data);
818821
static EVPKeyPointer NewRawPrivate(int id,
819822
const Buffer<const unsigned char>& data);
823+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
824+
static EVPKeyPointer NewRawSeed(int id,
825+
const Buffer<const unsigned char>& data);
826+
#endif
820827
static EVPKeyPointer NewDH(DHPointer&& dh);
821828
static EVPKeyPointer NewRSA(RSAPointer&& rsa);
822829

@@ -910,6 +917,10 @@ class EVPKeyPointer final {
910917
DataPointer rawPrivateKey() const;
911918
BIOPointer derPublicKey() const;
912919

920+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
921+
DataPointer rawSeed() const;
922+
#endif
923+
913924
Result<BIOPointer, bool> writePrivateKey(
914925
const PrivateKeyEncodingConfig& config) const;
915926
Result<BIOPointer, bool> writePublicKey(

doc/api/crypto.md

Lines changed: 42 additions & 5 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
@@ -3678,7 +3696,8 @@ changes:
36783696
-->
36793697

36803698
* `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`,
3681-
`'ed448'`, `'x25519'`, `'x448'`, or `'dh'`.
3699+
`'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35],
3700+
`'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35].
36823701
* `options` {Object}
36833702
* `modulusLength` {number} Key size in bits (RSA, DSA).
36843703
* `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`.
@@ -3767,6 +3786,9 @@ a `Promise` for an `Object` with `publicKey` and `privateKey` properties.
37673786
<!-- YAML
37683787
added: v10.12.0
37693788
changes:
3789+
- version: REPLACEME
3790+
pr-url: https://github.com/nodejs/node/pull/59259
3791+
description: Add support for ML-DSA key pairs.
37703792
- version: v16.10.0
37713793
pr-url: https://github.com/nodejs/node/pull/39927
37723794
description: Add ability to define `RSASSA-PSS-params` sequence parameters
@@ -3792,7 +3814,8 @@ changes:
37923814
-->
37933815

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

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

38213844
If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function
38223845
behaves as if [`keyObject.export()`][] had been called on its result. Otherwise,
@@ -5416,6 +5439,9 @@ Throws an error if FIPS mode is not available.
54165439
<!-- YAML
54175440
added: v12.0.0
54185441
changes:
5442+
- version: REPLACEME
5443+
pr-url: https://github.com/nodejs/node/pull/59259
5444+
description: Add support for ML-DSA signing.
54195445
- version: v18.0.0
54205446
pr-url: https://github.com/nodejs/node/pull/41678
54215447
description: Passing an invalid callback to the `callback` argument
@@ -5445,7 +5471,10 @@ changes:
54455471

54465472
Calculates and returns the signature for `data` using the given private key and
54475473
algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is
5448-
dependent upon the key type (especially Ed25519 and Ed448).
5474+
dependent upon the key type.
5475+
5476+
`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and
5477+
ML-DSA.
54495478

54505479
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
54515480
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
@@ -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
@@ -5561,7 +5593,10 @@ changes:
55615593

55625594
Verifies the given signature for `data` using the given key and algorithm. If
55635595
`algorithm` is `null` or `undefined`, then the algorithm is dependent upon the
5564-
key type (especially Ed25519 and Ed448).
5596+
key type.
5597+
5598+
`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and
5599+
ML-DSA.
55655600

55665601
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
55675602
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
@@ -6150,6 +6185,8 @@ See the [list of SSL OP Flags][] for details.
61506185
</tr>
61516186
</table>
61526187

6188+
[^openssl35]: Requires OpenSSL >= 3.5
6189+
61536190
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
61546191
[CCM mode]: #ccm-mode
61556192
[CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532

0 commit comments

Comments
 (0)