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

Lines changed: 1479 additions & 44 deletions

‎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
constkeyFixtures={
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
constbench=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
constpath=require('path');
77
constfixtures_keydir=path.resolve(__dirname,'../../test/fixtures/keys/');
88

9+
functionreadKey(name){
10+
returnfs.readFileSync(`${fixtures_keydir}/${name}.pem`,'utf8');
11+
}
12+
913
constkeyFixtures={
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
constdata=crypto.randomBytes(256);
@@ -18,7 +23,7 @@ let pems;
1823
letkeyObjects;
1924

2025
constbench=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
thrownewError('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
constkeyFixtures={
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
constdata=crypto.randomBytes(256);
@@ -29,7 +30,7 @@ let pems;
2930
letkeyObjects;
3031

3132
constbench=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
thrownewError('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<constunsignedchar>& 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<unsignedchar*>(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+
returnEVPKeyPointer(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
intEVPKeyPointer::id(constEVP_PKEY* key) {
19441969
if (key == nullptr) return0;
1945-
returnEVP_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")) returnEVP_PKEY_ML_DSA_44;
1975+
if (EVP_PKEY_is_a(key, "ML-DSA-65")) returnEVP_PKEY_ML_DSA_65;
1976+
if (EVP_PKEY_is_a(key, "ML-DSA-87")) returnEVP_PKEY_ML_DSA_87;
1977+
}
1978+
#endif
1979+
return type;
19461980
}
19471981

19481982
intEVPKeyPointer::base_id(constEVP_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+
caseEVP_PKEY_ML_DSA_44:
2040+
caseEVP_PKEY_ML_DSA_65:
2041+
caseEVP_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<unsignedchar> 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
boolEVPKeyPointer::isOneShotVariant() const {
24542513
if (!pkey_) returnfalse;
24552514
int type = id();
2456-
return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448;
2515+
switch (type) {
2516+
caseEVP_PKEY_ED25519:
2517+
caseEVP_PKEY_ED448:
2518+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
2519+
caseEVP_PKEY_ML_DSA_44:
2520+
caseEVP_PKEY_ML_DSA_65:
2521+
caseEVP_PKEY_ML_DSA_87:
2522+
#endif
2523+
returntrue;
2524+
default:
2525+
returnfalse;
2526+
}
24572527
}
24582528

24592529
boolEVPKeyPointer::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
#defineOSSL3_CONSTconst
33+
#if OPENSSL_VERSION_MINOR >= 5
34+
#include<openssl/core_names.h>
35+
#endif
3336
#else
3437
#defineOSSL3_CONST
3538
#endif
@@ -817,6 +820,10 @@ class EVPKeyPointer final {
817820
const Buffer<constunsignedchar>& data);
818821
static EVPKeyPointer NewRawPrivate(int id,
819822
const Buffer<constunsignedchar>& data);
823+
#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5
824+
static EVPKeyPointer NewRawSeed(int id,
825+
const Buffer<constunsignedchar>& 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)