From 69fc44e81969dcc733f029643eec242fe9d5c1c7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 27 Jul 2025 16:01:22 +0200 Subject: [PATCH 01/13] crypto: support ML-DSA KeyObject, sign, and verify --- deps/ncrypto/ncrypto.cc | 49 +++++- deps/ncrypto/ncrypto.h | 7 + doc/api/crypto.md | 34 +++- lib/internal/crypto/keygen.js | 34 ++-- lib/internal/crypto/keys.js | 90 +++++++++- node.gyp | 2 + src/crypto/crypto_keys.cc | 28 ++- src/crypto/crypto_ml_dsa.cc | 73 ++++++++ src/crypto/crypto_ml_dsa.h | 21 +++ src/env_properties.h | 13 +- src/node_crypto.h | 1 + test/fixtures/keys/Makefile | 48 ++++++ test/fixtures/keys/ml_dsa_44_private.pem | 57 ++++++ .../keys/ml_dsa_44_private_priv_only.pem | 56 ++++++ .../keys/ml_dsa_44_private_seed_only.pem | 4 + test/fixtures/keys/ml_dsa_44_public.pem | 30 ++++ test/fixtures/keys/ml_dsa_65_private.pem | 88 ++++++++++ .../keys/ml_dsa_65_private_priv_only.pem | 87 ++++++++++ .../keys/ml_dsa_65_private_seed_only.pem | 4 + test/fixtures/keys/ml_dsa_65_public.pem | 44 +++++ test/fixtures/keys/ml_dsa_87_private.pem | 106 ++++++++++++ .../keys/ml_dsa_87_private_priv_only.pem | 105 +++++++++++ .../keys/ml_dsa_87_private_seed_only.pem | 4 + test/fixtures/keys/ml_dsa_87_public.pem | 57 ++++++ .../test-crypto-pqc-key-objects-ml-dsa.js | 163 ++++++++++++++++++ .../parallel/test-crypto-pqc-keygen-ml-dsa.js | 69 ++++++++ .../test-crypto-pqc-sign-verify-ml-dsa.js | 68 ++++++++ 27 files changed, 1315 insertions(+), 27 deletions(-) create mode 100644 src/crypto/crypto_ml_dsa.cc create mode 100644 src/crypto/crypto_ml_dsa.h create mode 100644 test/fixtures/keys/ml_dsa_44_private.pem create mode 100644 test/fixtures/keys/ml_dsa_44_private_priv_only.pem create mode 100644 test/fixtures/keys/ml_dsa_44_private_seed_only.pem create mode 100644 test/fixtures/keys/ml_dsa_44_public.pem create mode 100644 test/fixtures/keys/ml_dsa_65_private.pem create mode 100644 test/fixtures/keys/ml_dsa_65_private_priv_only.pem create mode 100644 test/fixtures/keys/ml_dsa_65_private_seed_only.pem create mode 100644 test/fixtures/keys/ml_dsa_65_public.pem create mode 100644 test/fixtures/keys/ml_dsa_87_private.pem create mode 100644 test/fixtures/keys/ml_dsa_87_private_priv_only.pem create mode 100644 test/fixtures/keys/ml_dsa_87_private_seed_only.pem create mode 100644 test/fixtures/keys/ml_dsa_87_public.pem create mode 100644 test/parallel/test-crypto-pqc-key-objects-ml-dsa.js create mode 100644 test/parallel/test-crypto-pqc-keygen-ml-dsa.js create mode 100644 test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index cb6b6ab4a6137b..482d7882574766 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1942,7 +1942,16 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; - return EVP_PKEY_id(key); + int type = EVP_PKEY_id(key); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + // https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870 + if (type == -1) { + if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44; + if (EVP_PKEY_is_a(key, "ML-DSA-65")) return EVP_PKEY_ML_DSA_65; + if (EVP_PKEY_is_a(key, "ML-DSA-87")) return EVP_PKEY_ML_DSA_87; + } +#endif + return type; } int EVPKeyPointer::base_id(const EVP_PKEY* key) { @@ -1998,6 +2007,31 @@ DataPointer EVPKeyPointer::rawPublicKey() const { return {}; } +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +DataPointer EVPKeyPointer::rawSeed() const { + if (!pkey_) return {}; + switch (id()) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + break; + default: + unreachable(); + } + + size_t seed_len = 32; + if (auto data = DataPointer::Alloc(seed_len)) { + const Buffer buf = data; + size_t len = data.size(); + if (EVP_PKEY_get_octet_string_param( + get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1) + return {}; + return data; + } + return {}; +} +#endif + DataPointer EVPKeyPointer::rawPrivateKey() const { if (!pkey_) return {}; if (auto data = DataPointer::Alloc(rawPrivateKeySize())) { @@ -2453,7 +2487,18 @@ bool EVPKeyPointer::isRsaVariant() const { bool EVPKeyPointer::isOneShotVariant() const { if (!pkey_) return false; int type = id(); - return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; + switch (type) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: +#endif + return true; + default: + return false; + } } bool EVPKeyPointer::isSigVariant() const { diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 28e836f0bdb989..75d7ae8d4fc12e 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -30,6 +30,9 @@ #if OPENSSL_VERSION_MAJOR >= 3 #define OSSL3_CONST const +#if OPENSSL_VERSION_MINOR >= 5 +#include +#endif #else #define OSSL3_CONST #endif @@ -910,6 +913,10 @@ class EVPKeyPointer final { DataPointer rawPrivateKey() const; BIOPointer derPublicKey() const; +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + DataPointer rawSeed() const; +#endif + Result writePrivateKey( const PrivateKeyEncodingConfig& config) const; Result writePublicKey( diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 62c7b4976c6847..b208a37b9da416 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1916,6 +1916,9 @@ This can be called many times with new data as it is streamed. * `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, - `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. + `'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35], + `'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35]. * `options` {Object} * `modulusLength` {number} Key size in bits (RSA, DSA). * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. @@ -3816,7 +3838,7 @@ changes: * `privateKey` {string | Buffer | KeyObject} Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, -Ed25519, Ed448, X25519, X448, and DH are currently supported. +Ed25519, Ed448, X25519, X448, DH, and ML-DSA[^openssl35] are currently supported. If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function 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. * `type` {string} Must be `'rsa'`, `'rsa-pss'`, `'dsa'`, `'ec'`, `'ed25519'`, - `'ed448'`, `'x25519'`, `'x448'`, or `'dh'`. + `'ed448'`, `'x25519'`, `'x448'`, `'dh'`, `'ml-dsa-44'`[^openssl35], + `'ml-dsa-65'`[^openssl35], or `'ml-dsa-87'`[^openssl35]. * `options` {Object} * `modulusLength` {number} Key size in bits (RSA, DSA). * `publicExponent` {number} Public exponent (RSA). **Default:** `0x10001`. @@ -5470,7 +5471,10 @@ changes: Calculates and returns the signature for `data` using the given private key and algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is -dependent upon the key type (especially Ed25519 and Ed448). +dependent upon the key type. + +`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and +ML-DSA. If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPrivateKey()`][]. If it is an object, the following @@ -5589,7 +5593,10 @@ changes: Verifies the given signature for `data` using the given key and algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is dependent upon the -key type (especially Ed25519 and Ed448). +key type. + +`algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and +ML-DSA. If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPublicKey()`][]. If it is an object, the following From 24670db5e80e0ce518c64cdb18064b7cdb93fd5f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 14:08:58 +0200 Subject: [PATCH 04/13] use primordial --- lib/internal/crypto/keys.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 45c8573224c3b6..f7595e6b7e8474 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -2,6 +2,7 @@ const { ArrayPrototypeSlice, + ArrayPrototypeUnshift, ObjectDefineProperties, ObjectDefineProperty, ObjectSetPrototypeOf, @@ -539,7 +540,7 @@ function encodeLength(length) { const bytes = []; let temp = length; while (temp > 0) { - bytes.unshift(temp & 0xff); + ArrayPrototypeUnshift(bytes, temp & 0xff); temp >>>= 8; } From aaa2d1dca5c0eb71b6351a9be2e73b1c5265a98c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 14:19:43 +0200 Subject: [PATCH 05/13] add benchmarks --- benchmark/crypto/oneshot-sign.js | 14 ++++++++++---- benchmark/crypto/oneshot-verify.js | 10 ++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/benchmark/crypto/oneshot-sign.js b/benchmark/crypto/oneshot-sign.js index 88e67b9dfb1cf9..97372606a10ade 100644 --- a/benchmark/crypto/oneshot-sign.js +++ b/benchmark/crypto/oneshot-sign.js @@ -6,10 +6,15 @@ const fs = require('fs'); const path = require('path'); const fixtures_keydir = path.resolve(__dirname, '../../test/fixtures/keys/'); +function readKey(name) { + return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8'); +} + const keyFixtures = { - ec: fs.readFileSync(`${fixtures_keydir}/ec_p256_private.pem`, 'utf-8'), - rsa: fs.readFileSync(`${fixtures_keydir}/rsa_private_2048.pem`, 'utf-8'), - ed25519: fs.readFileSync(`${fixtures_keydir}/ed25519_private.pem`, 'utf-8'), + 'ec': readKey('ec_p256_private'), + 'rsa': readKey('rsa_private_2048'), + 'ed25519': readKey('ed25519_private'), + 'ml-dsa-44': readKey('ml_dsa_44_private'), }; const data = crypto.randomBytes(256); @@ -18,7 +23,7 @@ let pems; let keyObjects; const bench = common.createBenchmark(main, { - keyType: ['rsa', 'ec', 'ed25519'], + keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'], mode: ['sync', 'async', 'async-parallel'], keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'], n: [1e3], @@ -90,6 +95,7 @@ function main({ n, mode, keyFormat, keyType }) { digest = 'sha256'; break; case 'ed25519': + case 'ml-dsa-44': break; default: throw new Error('not implemented'); diff --git a/benchmark/crypto/oneshot-verify.js b/benchmark/crypto/oneshot-verify.js index 121be28d4d9578..9569d5168f60ce 100644 --- a/benchmark/crypto/oneshot-verify.js +++ b/benchmark/crypto/oneshot-verify.js @@ -18,9 +18,10 @@ function readKeyPair(publicKeyName, privateKeyName) { } const keyFixtures = { - ec: readKeyPair('ec_p256_public', 'ec_p256_private'), - rsa: readKeyPair('rsa_public_2048', 'rsa_private_2048'), - ed25519: readKeyPair('ed25519_public', 'ed25519_private'), + 'ec': readKeyPair('ec_p256_public', 'ec_p256_private'), + 'rsa': readKeyPair('rsa_public_2048', 'rsa_private_2048'), + 'ed25519': readKeyPair('ed25519_public', 'ed25519_private'), + 'ml-dsa-44': readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'), }; const data = crypto.randomBytes(256); @@ -29,7 +30,7 @@ let pems; let keyObjects; const bench = common.createBenchmark(main, { - keyType: ['rsa', 'ec', 'ed25519'], + keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'], mode: ['sync', 'async', 'async-parallel'], keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'], n: [1e3], @@ -104,6 +105,7 @@ function main({ n, mode, keyFormat, keyType }) { digest = 'sha256'; break; case 'ed25519': + case 'ml-dsa-44': break; default: throw new Error('not implemented'); From bba6de77001e3b36f1b2133ca74ca369c7056ad0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 17:14:57 +0200 Subject: [PATCH 06/13] use native handle init for JWK public inputs --- lib/internal/crypto/keys.js | 63 +++++-------------- src/crypto/crypto_keys.cc | 53 +++++++++++++++- src/crypto/crypto_keys.h | 2 + .../test-crypto-pqc-key-objects-ml-dsa.js | 4 +- 4 files changed, 72 insertions(+), 50 deletions(-) diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index f7595e6b7e8474..f8371fa2d322cf 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -2,7 +2,6 @@ const { ArrayPrototypeSlice, - ArrayPrototypeUnshift, ObjectDefineProperties, ObjectDefineProperty, ObjectSetPrototypeOf, @@ -527,26 +526,6 @@ function mlDsaPubLen(alg) { } } -/** - * Encodes length for use in DER - * @param {number} length - * @returns {number[]} - */ -function encodeLength(length) { - if (length < 128) { - return [length]; - } - - const bytes = []; - let temp = length; - while (temp > 0) { - ArrayPrototypeUnshift(bytes, temp & 0xff); - temp >>>= 8; - } - - return [0x80 | bytes.length, ...bytes]; -} - function getKeyObjectHandleFromJwk(key, ctx) { validateObject(key, 'key'); if (EVP_PKEY_ML_DSA_44 || EVP_PKEY_ML_DSA_65 || EVP_PKEY_ML_DSA_87) { @@ -563,39 +542,31 @@ function getKeyObjectHandleFromJwk(key, ctx) { key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']); validateString(key.pub, 'key.pub'); - if (!isPublic) - validateString(key.priv, 'key.priv'); - - let keyData = Buffer.from(key.pub, 'base64url'); - if (keyData.byteLength !== mlDsaPubLen(key.alg)) { - throw new ERR_CRYPTO_INVALID_JWK(); - } + if (isPublic) { + const keyData = Buffer.from(key.pub, 'base64url'); + if (keyData.byteLength !== mlDsaPubLen(key.alg)) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + const handle = new KeyObjectHandle(); - let encoding; - let bytes; - // Uses seed when available for both kKeyTypePublic and kKeyTypePrivate - if (key.priv) { - keyData = Buffer.from(key.priv, 'base64url'); - if (keyData.byteLength !== 32) { + if (!handle.initMlDsaPublicRaw(key.alg, keyData)) { throw new ERR_CRYPTO_INVALID_JWK(); } - bytes = [48, 52, 2, 1, 0, 48, 11, 6, 9, ...oids[key.alg], 4, 34, 128, 32, ...keyData]; - encoding = kKeyEncodingPKCS8; - } else { - bytes = [ - 48, ...encodeLength(keyData.length + 18), - 48, 11, 6, 9, ...oids[key.alg], - 3, ...encodeLength(keyData.length + 1), - 0, ...keyData, - ]; - encoding = kKeyEncodingSPKI; + return handle; } - const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; + validateString(key.priv, 'key.priv'); + const keyData = Buffer.from(key.priv, 'base64url'); + if (keyData.byteLength !== 32) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + + const bytes = [48, 52, 2, 1, 0, 48, 11, 6, 9, ...oids[key.alg], 4, 34, 128, 32, ...keyData]; + const handle = new KeyObjectHandle(); try { - handle.init(keyType, new Uint8Array(bytes), kKeyFormatDER, encoding, null); + handle.init(kKeyTypePrivate, new Uint8Array(bytes), kKeyFormatDER, kKeyEncodingPKCS8, null); } catch { throw new ERR_CRYPTO_INVALID_JWK(); } diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 804806dc0dd0bb..b159e8d4f47cae 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -270,7 +270,7 @@ bool ExportJWKInner(Environment* env, env, key, result.As(), handleRsaPss); } -int GetOKPCurveFromName(const char* name) { +int GetNidFromName(const char* name) { int nid; if (strcmp(name, "Ed25519") == 0) { nid = EVP_PKEY_ED25519; @@ -280,6 +280,14 @@ int GetOKPCurveFromName(const char* name) { nid = EVP_PKEY_X25519; } else if (strcmp(name, "X448") == 0) { nid = EVP_PKEY_X448; +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + } else if (strcmp(name, "ML-DSA-44") == 0) { + nid = EVP_PKEY_ML_DSA_44; + } else if (strcmp(name, "ML-DSA-65") == 0) { + nid = EVP_PKEY_ML_DSA_65; + } else if (strcmp(name, "ML-DSA-87") == 0) { + nid = EVP_PKEY_ML_DSA_87; +#endif } else { nid = NID_undef; } @@ -612,6 +620,7 @@ Local KeyObjectHandle::Initialize(Environment* env) { SetProtoMethod(isolate, templ, "exportJwk", ExportJWK); SetProtoMethod(isolate, templ, "initECRaw", InitECRaw); SetProtoMethod(isolate, templ, "initEDRaw", InitEDRaw); + SetProtoMethod(isolate, templ, "initMlDsaPublicRaw", InitMlDsaPublicRaw); SetProtoMethod(isolate, templ, "initJwk", InitJWK); SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail); SetProtoMethod(isolate, templ, "equals", Equals); @@ -798,7 +807,7 @@ void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo& args) { new_key_fn fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate : EVPKeyPointer::NewRawPublic; - int id = GetOKPCurveFromName(*name); + int id = GetNidFromName(*name); switch (id) { case EVP_PKEY_X25519: @@ -824,6 +833,46 @@ void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(true); } +void KeyObjectHandle::InitMlDsaPublicRaw( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); + + CHECK(args[0]->IsString()); + Utf8Value name(env->isolate(), args[0]); + + ArrayBufferOrViewContents key_data(args[1]); + + MarkPopErrorOnReturn mark_pop_error_on_return; + + int id = GetNidFromName(*name); + + switch (id) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: { + auto pkey = + EVPKeyPointer::NewRawPublic(id, + ncrypto::Buffer{ + .data = key_data.data(), + .len = key_data.size(), + }); + if (!pkey) { + return args.GetReturnValue().Set(false); + } + key->data_ = + KeyObjectData::CreateAsymmetric(kKeyTypePublic, std::move(pkey)); + CHECK(key->data_); + break; + } + default: + UNREACHABLE(); + } + + args.GetReturnValue().Set(true); +} + void KeyObjectHandle::Equals(const FunctionCallbackInfo& args) { KeyObjectHandle* self_handle; KeyObjectHandle* arg_handle; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 794e2699c8163f..b103b2bab17677 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -152,6 +152,8 @@ class KeyObjectHandle : public BaseObject { static void Init(const v8::FunctionCallbackInfo& args); static void InitECRaw(const v8::FunctionCallbackInfo& args); static void InitEDRaw(const v8::FunctionCallbackInfo& args); + static void InitMlDsaPublicRaw( + const v8::FunctionCallbackInfo& args); static void InitJWK(const v8::FunctionCallbackInfo& args); static void GetKeyDetail(const v8::FunctionCallbackInfo& args); static void Equals(const v8::FunctionCallbackInfo& args); diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js index 11b054c3f98f41..37eab463deae47 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js @@ -116,9 +116,9 @@ for (const [asymmetricKeyType, pubLen] of [ { const format = 'jwk'; const jwk = { - priv: '9_uqvxH0WKJFgfLyse1a1des2bwPgsHctl_jCt5AfEo', kty: 'AKP', alg: 'ML-DSA-44', + priv: '9_uqvxH0WKJFgfLyse1a1des2bwPgsHctl_jCt5AfEo', // eslint-disable-next-line @stylistic/js/max-len pub: 'SXghXj9P-DJ5eznNa_zLJxRxpa0mt86WlIid0EVEv1qraLBkC1UKevSZrjtgo1QUEN0oa3tP-HyYj8Onnc1zEnxsSoeC5A-PgywKgYuZP581wGPS-cbA2-5acsg-YUi_9fDkLR5YOTQQ3Iu952K1m8w0QDIBxZjecm32HgkD56CCC6ZyBOwfx9qcNUeO0aImya1igzL2_LRsqomogl9OuduWhtussAavGlAK7ZR4_4lmyjWcdeIc-z--iy42biV5d_tnopfNTJFlycBKinZu3h0lr4-ldl6apGDIyvSdZulhgj_j6jgEX-AgQZgS93ctx680GvROkBL7YI_3iXW3REWzVgS9HLasagEi2h6-RYQ9RzgUTODbei5fNRj3bNSqr8IKTZ08DCsRasN61TGwE3F7meoauw2NYkV51mhTxIafwhLWJrRA4C09Y-afrOtqk6a7Fiy21ObP95TGujXwThuwQSjKcUzTdCbD94ERhleZLqnPYEpb6_Jcc1OBY3kUJvCjoUhXwbW6PhWr533JDEFHoNCkPfhHS7vVCUFx4mQASkPLBud5arFSZU1uDStuiftJXfnQTWaMoJeA1N6rywB3xwLH__lHZQwEh4KnuYVuCeOMU1t8inuHI4EpZ4iTi2LrL0Cl6HadpHv-GENYwuPDVq9qg2Mo75o1X6wpPSN1J5KUDAATyR_0hurg4A1DlVpVWtykP5YWEmx_g5w4MZfEVwH-JJjEhJRxLKajxrjfG4XlnwwxPTznr1k1Mb7getKbLSbiMA3fvAgl1IjBIB8eFaISauFPpLPSpKHCVZrQYPIKSxMVdlXHgwm3CRrkR29GevCM5iSwRHQK6_HWfIQIlJ8H7uVQqXkNMvNmFldnfi3dj-oY6wMhs1ffP4RAsb5UTljvhJc6GoygBL_b0rv4aKcywDF8wa2P6B8gGFl6cVvWBQmWLJ-HL5RR68J_OmvJUm3PD-wwX3YigStd6thdTNlHOZhl4ysn8ulkFY3Rz2jEBV_nO6EXBLdOmxn_yX77qQ9yPcE64uC8iDTFWpQU1gmOF38od96oYD-T-whVl1NLD2bOvFVdd4UmWpb3Ui8AYzKzFBHNczAogQfplFmr8VABsgtWk8hW8csam70NADWK54SZOPQHeiOt1Mb488OZiDpX0FifEnCac78C_5uEiOPa8FAHpgUJ_XeXg83doDsAvrE1ZkrgFDwzT5pUTLyqh9eI1PAQKCoKmAbofcZM65o_qGvmnpN8fGVudOoHb0_Dqu_E2RBbaLcxsIe5jWmGmth4sb_4ANLFCmtt8T8sDmOdcYtQmhpUzg6rjDqeU76yq6fC15bGjT6Qc-EAgrUftFVwLw2UIGAbHmeAyFbSuJiMaUDJeYoZ3zxoID_DRCP9kN3ty-EQMHM9BZgXlH9dJ8ZUCAeH59h4PinM5LQuiS_kvP1iyT1Be6sYglV2dB7W_AziOcrrBiLfjazbUUpwqLm4_Yt_QwYJrAWfYyFOxkKxkT5qi2c1RNGBtiYjv8X_TRJDg0uxX_Hbq0Pc7yYezFQdFYIlRAvJcnc8PiOfhWtQZpCIYKTskg_Y2UfVvjydXYcGuIA2700PzR9ga-1VR7mv9UOLHe2x4ALiOO7Iz6KgOfVYMJ9dIC7f4HY9nrnLdhfKw5dcIf7RDhDqrkPyz8LLTuAuO-hGSwoP35XFkf0FQ8f1Cg5J_k-S3S2dCj8DXIRLcEJ9Qb5zvDofIPSmNKlvwJkqplDLBWlAow' }; @@ -134,7 +134,7 @@ for (const [asymmetricKeyType, pubLen] of [ { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.priv" property must be of type string/ }); assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: Buffer.alloc(33).toString('base64url') } }), { code: 'ERR_CRYPTO_INVALID_JWK' }); - assert.throws(() => createPrivateKey({ format, key: { ...jwk, pub: Buffer.alloc(1313).toString('base64url') } }), + assert.throws(() => createPublicKey({ format, key: { ...jwk, pub: Buffer.alloc(1313).toString('base64url') } }), { code: 'ERR_CRYPTO_INVALID_JWK' }); assert.ok(createPrivateKey({ format, key: jwk })); From a3a2f52087939e6b3c30a2a474bac31cc5438898 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 18:15:06 +0200 Subject: [PATCH 07/13] cpp implementation of init from seed --- deps/ncrypto/ncrypto.cc | 22 +++++++++++++ deps/ncrypto/ncrypto.h | 4 +++ lib/internal/crypto/keys.js | 33 ++++++------------- src/crypto/crypto_keys.cc | 29 +++++++++------- src/crypto/crypto_keys.h | 5 +-- .../test-crypto-pqc-sign-verify-ml-dsa.js | 8 ++++- 6 files changed, 64 insertions(+), 37 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 482d7882574766..4a0dd8b2d8420e 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1897,6 +1897,28 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); } +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +EVPKeyPointer EVPKeyPointer::NewRawSeed( + int id, const Buffer& data) { + if (id == 0) return {}; + + OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_ML_DSA_SEED, + (void*)data.data, // NOLINT(readability/casting) + data.len), + OSSL_PARAM_END}; + + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); + EVP_PKEY* pkey = nullptr; + + if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 || + EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) + return {}; + + return EVPKeyPointer(pkey); +} +#endif + EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) { if (!dh) return {}; auto key = New(); diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 75d7ae8d4fc12e..82af70798f3171 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -820,6 +820,10 @@ class EVPKeyPointer final { const Buffer& data); static EVPKeyPointer NewRawPrivate(int id, const Buffer& data); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + static EVPKeyPointer NewRawSeed(int id, + const Buffer& data); +#endif static EVPKeyPointer NewDH(DHPointer&& dh); static EVPKeyPointer NewRSA(RSAPointer&& rsa); diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index f8371fa2d322cf..581063f486a560 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -512,12 +512,6 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { return types; } -const oids = { - 'ML-DSA-44': [96, 134, 72, 1, 101, 3, 4, 3, 17], - 'ML-DSA-65': [96, 134, 72, 1, 101, 3, 4, 3, 18], - 'ML-DSA-87': [96, 134, 72, 1, 101, 3, 4, 3, 19], -}; - function mlDsaPubLen(alg) { switch (alg) { case 'ML-DSA-44': return 1312; @@ -542,34 +536,27 @@ function getKeyObjectHandleFromJwk(key, ctx) { key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']); validateString(key.pub, 'key.pub'); + let keyData; if (isPublic) { - const keyData = Buffer.from(key.pub, 'base64url'); + keyData = Buffer.from(key.pub, 'base64url'); if (keyData.byteLength !== mlDsaPubLen(key.alg)) { throw new ERR_CRYPTO_INVALID_JWK(); } - const handle = new KeyObjectHandle(); - - if (!handle.initMlDsaPublicRaw(key.alg, keyData)) { + } else { + validateString(key.priv, 'key.priv'); + keyData = Buffer.from(key.priv, 'base64url'); + if (keyData.byteLength !== 32) { throw new ERR_CRYPTO_INVALID_JWK(); } - - return handle; - } - - validateString(key.priv, 'key.priv'); - const keyData = Buffer.from(key.priv, 'base64url'); - if (keyData.byteLength !== 32) { - throw new ERR_CRYPTO_INVALID_JWK(); } - const bytes = [48, 52, 2, 1, 0, 48, 11, 6, 9, ...oids[key.alg], 4, 34, 128, 32, ...keyData]; - const handle = new KeyObjectHandle(); - try { - handle.init(kKeyTypePrivate, new Uint8Array(bytes), kKeyFormatDER, kKeyEncodingPKCS8, null); - } catch { + + const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; + if (!handle.initMlDsaRaw(key.alg, keyData, keyType)) { throw new ERR_CRYPTO_INVALID_JWK(); } + return handle; } diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index b159e8d4f47cae..e069dbaf46f44c 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -620,7 +620,9 @@ Local KeyObjectHandle::Initialize(Environment* env) { SetProtoMethod(isolate, templ, "exportJwk", ExportJWK); SetProtoMethod(isolate, templ, "initECRaw", InitECRaw); SetProtoMethod(isolate, templ, "initEDRaw", InitEDRaw); - SetProtoMethod(isolate, templ, "initMlDsaPublicRaw", InitMlDsaPublicRaw); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + SetProtoMethod(isolate, templ, "initMlDsaRaw", InitMlDsaRaw); +#endif SetProtoMethod(isolate, templ, "initJwk", InitJWK); SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail); SetProtoMethod(isolate, templ, "equals", Equals); @@ -833,8 +835,8 @@ void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(true); } -void KeyObjectHandle::InitMlDsaPublicRaw( - const FunctionCallbackInfo& args) { +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +void KeyObjectHandle::InitMlDsaRaw(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); KeyObjectHandle* key; ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); @@ -843,26 +845,30 @@ void KeyObjectHandle::InitMlDsaPublicRaw( Utf8Value name(env->isolate(), args[0]); ArrayBufferOrViewContents key_data(args[1]); + KeyType type = FromV8Value(args[2]); MarkPopErrorOnReturn mark_pop_error_on_return; + typedef EVPKeyPointer (*new_key_fn)( + int, const ncrypto::Buffer&); + new_key_fn fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed + : EVPKeyPointer::NewRawPublic; + int id = GetNidFromName(*name); switch (id) { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: { - auto pkey = - EVPKeyPointer::NewRawPublic(id, - ncrypto::Buffer{ - .data = key_data.data(), - .len = key_data.size(), - }); + auto pkey = fn(id, + ncrypto::Buffer{ + .data = key_data.data(), + .len = key_data.size(), + }); if (!pkey) { return args.GetReturnValue().Set(false); } - key->data_ = - KeyObjectData::CreateAsymmetric(kKeyTypePublic, std::move(pkey)); + key->data_ = KeyObjectData::CreateAsymmetric(type, std::move(pkey)); CHECK(key->data_); break; } @@ -872,6 +878,7 @@ void KeyObjectHandle::InitMlDsaPublicRaw( args.GetReturnValue().Set(true); } +#endif void KeyObjectHandle::Equals(const FunctionCallbackInfo& args) { KeyObjectHandle* self_handle; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index b103b2bab17677..2a2a38ebe0e3cb 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -152,8 +152,9 @@ class KeyObjectHandle : public BaseObject { static void Init(const v8::FunctionCallbackInfo& args); static void InitECRaw(const v8::FunctionCallbackInfo& args); static void InitEDRaw(const v8::FunctionCallbackInfo& args); - static void InitMlDsaPublicRaw( - const v8::FunctionCallbackInfo& args); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + static void InitMlDsaRaw(const v8::FunctionCallbackInfo& args); +#endif static void InitJWK(const v8::FunctionCallbackInfo& args); static void GetKeyDetail(const v8::FunctionCallbackInfo& args); static void Equals(const v8::FunctionCallbackInfo& args); diff --git a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js index 515cdd5f78ffc6..de3937dbc07486 100644 --- a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js @@ -15,6 +15,7 @@ const { sign, verify, createPublicKey, + createPrivateKey, } = require('crypto'); const fixtures = require('../common/fixtures'); @@ -81,6 +82,7 @@ for (const [asymmetricKeyType, sigLen] of [ alg: 'ML-DSA-44', // eslint-disable-next-line @stylistic/js/max-len pub: 'unH59k4RuutY-pxvu24U5h8YZD2rSVtHU5qRZsoBmBMcRPgmu9VuNOVdteXi1zNIXjnqJg_GAAxepLqA00Vc3lO0bzRIKu39VFD8Lhuk8l0V-cFEJC-zm7UihxiQMMUEmOFxe3x1ixkKZ0jqmqP3rKryx8tSbtcXyfea64QhT6XNje2SoMP6FViBDxLHBQo2dwjRls0k5a-XSQSu2OTOiHLoaWsLe8pQ5FLNfTDqmkrawDEdZyxr3oSWJAsHQxRjcIiVzZuvwxYy1zl2STiP2vy_fTBaPemkleynQzqPg7oPCyXEE8bjnJbrfWkbNNN8438e6tHPIX4l7zTuzz98YPhLjt_d6EBdT4MldsYe-Y4KLyjaGHcAlTkk9oa5RhRwW89T0z_t1DSO3dvfKLUGXh8gd1BD6Fz5MfgpF5NjoafnQEqDjsAAhrCXY4b-Y3yYJEdX4_dp3dRGdHG_rWcPmgX4JG7lCnser4f8QGnDriqiAzJYEXeS8LzUngg_0bx0lqv_KcyU5IaLISFO0xZSU5mmEPvdSoDnyAcV8pV44qhLtAvd29n0ehG259oRihtljTWeiu9V60a1N2tbZVl5mEqSK-6_xZvNYA1TCdzNctvweH24unV7U3wer9XA9Q6kvJWDVJ4oKaQsKMrCSMlteBJMRxWbGK7ddUq6F7GdQw-3j2M-qdJvVKm9UPjY9rc1lPgol25-oJxTu7nxGlbJUH-4m5pevAN6NyZ6lfhbjWTKlxkrEKZvQXs_Yf6cpXEwpI_ZJeriq1UC1XHIpRkDwdOY9MH3an4RdDl2r9vGl_IwlKPNdh_5aF3jLgn7PCit1FNJAwC8fIncAXgAlgcXIpRXdfJk4bBiO89GGccSyDh2EgXYdpG3XvNgGWy7npuSoNTE7WIyblAk13UQuO4sdCbMIuriCdyfE73mvwj15xgb07RZRQtFGlFTmnFcIdZ90zDrWXDbANntv7KCKwNvoTuv64bY3HiGbj-NQ-U9eMylWVpvr4hrXcES8c9K3PqHWADZC0iIOvlzFv4VBoc_wVflcOrL_SIoaNFCNBAZZq-2v5lAgpJTqVOtqJ_HVraoSfcKy5g45p-qULunXj6Jwq21fobQiKubBKKOZwcJFyJD7F4ACKXOrz-HIvSHMCWW_9dVrRuCpJw0s0aVFbRqopDNhu446nqb4_EDYQM1tTHMozPd_jKxRRD0sH75X8ZoToxFSpLBDbtdWcenxj-zBf6IGWfZnmaetjKEBYJWC7QDQx1A91pJVJCEgieCkoIfTqkeQuePpIyu48g2FG3P1zjRF-kumhUTfSjo5qS0YiZQy0E1BMs6M11EvuxXRsHClLHoy5nLYI2Sj4zjVjYyxSHyPRPGGo9hwB34yWxzYNtPPGiqXS_dNCpi_zRZwRY4lCGrQ-hYTEWIK1Dm5OlttvC4_eiQ1dv63NiGkLRJ5kJA3bICN0fzCDY-MBqnd1cWn8YVBijVkgtaoascjL9EywDgJdeHnXK0eeOvUxHHhXJVkNqcibn8O4RQdpVU60TSA-uiu675ytIjcBHC6kTv8A8pmkj_4oypPd-F92YIJC741swkYQoeIHj8rE-ThcMUkF7KqC5VORbZTRp8HsZSqgiJcIPaouuxd1-8Rxrid3fXkE6p8bkrysPYoxWEJgh7ZFsRCPDWX-yTeJwFN0PKFP1j0F6YtlLfK5wv-c4F8ZQHA_-yc_gODicy7KmWDZgbTP07e7gEWzw4MFRrndjbDQ', + priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', } }, { @@ -93,6 +95,7 @@ for (const [asymmetricKeyType, sigLen] of [ alg: 'ML-DSA-65', // eslint-disable-next-line @stylistic/js/max-len pub: 'QksvJn5Y1bO0TXGs_Gpla7JpUNV8YdsciAvPof6rRD8JQquL2619cIq7w1YHj22ZolInH-YsdAkeuUr7m5JkxQqIjg3-2AzV-yy9NmfmDVOevkSTAhnNT67RXbs0VaJkgCufSbzkLudVD-_91GQqVa3mk4aKRgy-wD9PyZpOMLzP-opHXlOVOWZ067galJN1h4gPbb0nvxxPWp7kPN2LDlOzt_tJxzrfvC1PjFQwNSDCm_l-Ju5X2zQtlXyJOTZSLQlCtB2C7jdyoAVwrftUXBFDkisElvgmoKlwBks23fU0tfjhwc0LVWXqhGtFQx8GGBQ-zol3e7P2EXmtIClf4KbgYq5u7Lwu848qwaItyTt7EmM2IjxVth64wHlVQruy3GXnIurcaGb_qWg764qZmteoPl5uAWwuTDX292Sa071S7GfsHFxue5lydxIYvpVUu6dyfwuExEubCovYMfz_LJd5zNTKMMatdbBJg-Qd6JPuXznqc1UYC3CccEXCLTOgg_auB6EUdG0b_cy-5bkEOHm7Wi4SDipGNig_ShzUkkot5qSqPZnd2I9IqqToi_0ep2nYLBB3ny3teW21Qpccoom3aGPt5Zl7fpzhg7Q8zsJ4sQ2SuHRCzgQ1uxYlFx21VUtHAjnFDSoMOkGyo4gH2wcLR7-z59EPPNl51pljyNefgCnMSkjrBPyz1wiET-uqi23f8Bq2TVk1jmUFxOwdfLsU7SIS30WOzvwD_gMDexUFpMlEQyL1-Y36kaTLjEWGCi2tx1FTULttQx5JpryPW6lW5oKw5RMyGpfRliYCiRyQePYqipZGoxOHpvCWhCZIN4meDY7H0RxWWQEpiyCzRQgWkOtMViwao6Jb7wZWbLNMebwLJeQJXWunk-gTEeQaMykVJobwDUiX-E_E7fSybVRTZXherY1jrvZKh8C5Gi5VADg5Vs319uN8-dVILRyOOlvjjxclmsRcn6HEvTvxd9MS7lKm2gI8BXIqhzgnTdqNGwTpmDHPV8hygqJWxWXCltBSSgY6OkGkioMAmXjZjYq_Ya9o6AE7WU_hUdm-wZmQLExwtJWEIBdDxrUxA9L9JL3weNyQtaGItPjXcheZiNBBbJTUxXwIYLnXtT1M0mHzMqGFFWXVKsN_AIdHyv4yDzY9m-tuQRfbQ_2K7r5eDOL1Tj8DZ-s8yXG74MMBqOUvlglJNgNcbuPKLRPbSDoN0E3BYkfeDgiUrXy34a5-vU-PkAWCsgAh539wJUUBxqw90V1Du7eTHFKDJEMSFYwusbPhEX4ZTwoeTHg--8Ysn4HCFWLQ00pfBCteqvMvMflcWwVfTnogcPsJb1bEFVSc3nTzhk6Ln8J-MplyS0Y5mGBEtVko_WlyeFsoDCWj4hqrgU7L-ww8vsCRSQfskH8lodiLzj0xmugiKjWUXbYq98x1zSnB9dmPy5P3UNwwMQdpebtR38N9I-jup4Bzok0-JsaOe7EORZ8ld7kAgDWa4K7BAxjc2eD540Apwxs-VLGFVkXbQgYYeDNG2tW1Xt20-XezJqZVUl6-IZXsqc7DijwNInO3fT5o8ZAcLKUUlzSlEXe8sIlHaxjLoJ-oubRtlKKUbzWOHeyxmYZSxYqQhSQj4sheedGXJEYWJ-Y5DRqB-xpy-cftxL10fdXIUhe1hWFBAoQU3b5xRY8KCytYnfLhsFF4O49xhnax3vuumLpJbCqTXpLureoKg5PvWfnpFPB0P-ZWQN35mBzqbb3ZV6U0rU55DvyXTuiZOK2Z1TxbaAd1OZMmg0cpuzewgueV-Nh_UubIqNto5RXCd7vqgqdXDUKAiWyYegYIkD4wbGMqIjxV8Oo2ggOcSj9UQPS1rD5u0rLckAzsxyty9Q5JsmKa0w8Eh7Jwe4Yob4xPVWWbJfm916avRgzDxXo5gmY7txdGFYHhlolJKdhBU9h6f0gtKEtbiUzhp4IWsqAR8riHQs7lLVEz6P537a4kL1r5FjfDf_yjJDBQmy_kdWMDqaNln-MlKK8eENjUO-qZGy0Ql4bMZtNbHXjfJUuSzapA-RqYfkqSLKgQUOW8NTDKhUk73yqCU3TQqDEKaGAoTsPscyMm7u_8QrvUK8kbc-XnxrWZ0BZJBjdinzh2w-QvjbWQ5mqFp4OMgY94__tIU8vvCUNJiYA1RdyodlfPfH5-avpxOCvBD6C7ZIDyQ-6huGEQEAb6DP8ydWIZQ8xY603DoEKKXkJWcP6CJo3nHFEdj_vcEbDQ-WESDpcQFa1fRIiGuALj-sEWcjGdSHyE8QATOcuWl4TLVzRPKAf4tCXx1zyvhJbXQu0jf0yfzVpOhPun4n-xqK4SxPBCeuJOkQ2VG9jDXWH4pnjbAcrqjveJqVti7huMXTLGuqU2uoihBw6mGqu_WSlOP2-XTEyRyvxbv2t-z9V6GPt1V9ceBukA0oGwtJqgD-q7NXFK8zhw7desI5PZMXf3nuVgbJ3xdvAlzkmm5f9RoqQS6_hqwPQEcclq1MEZ3yML5hc99TDtZWy9gGkhR0Hs3QJxxgP7bEqGFP-HjTPnJsrGaT6TjKP7qCxJlcFKLUr5AU_kxMULeUysWWtSGJ9mpxBvsyW1Juo', + priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', } }, { @@ -105,11 +108,14 @@ for (const [asymmetricKeyType, sigLen] of [ alg: 'ML-DSA-87', // eslint-disable-next-line @stylistic/js/max-len pub: '5F_8jMc9uIXcZi5ioYzY44AylxF_pWWIFKmFtf8dt7Roz8gruSnx2Gt37RT1rhamU2h3LOUZEkEBBeBFaXWukf22Q7US8STV5gvWi4x-Mf4Bx7DcZa5HBQHMVlpuHfz8_RJWVDPEr-3VEYIeLpYQxFJ14oNt7jXO1p1--mcv0eQxi-9etuiX6LRRqiAt7QQrKq73envj9pkUbaIpqL2z_6SWRFln51IXv7yQSPmVZEPYcx-DPrMN4Q2slv_-fPZeoERcPjHoYB4TO-ahAHZP4xluJncmRB8xdR-_mm9YgGRPTnJ15X3isPEF5NsFXVDdHJyTT931NbjeKLDHTARJ8iLNLtC7j7x3XM7oyUBmW0D3EvT34AdQ6eHkzZz_JdGUXD6bylPM1PEu7nWBhW69aPJoRZVuPnvrdh8P51vdMb_i-gGBEzl7OHvVnWKmi4r3-iRauTLmn3eOLO79ITBPu4CZ6hPY6lfBgTGXovda4lEHW1Ha04-FNmnp1fmKNlUJiUGZOhWUhg-6cf5TDuXCn1jyl4r2iMy3Wlg4o1nBEumOJahYOsjawfhh_Vjir7pd5aUuAgkE9bQrwIdONb788-YRloR2jzbgCPBHEhd86-YnYHOB5W6q7hYcFym43lHb3kdNSMxoJJ6icWK4eZPmDITtbMZCPLNnbZ61CyyrWjoEnvExOB1iP6b7y8nbHnzAJeoEGLna0sxszU6V-izsJP7spwMYp1Fxa3IT9j7b9lpjM4NX-Dj5TsBxgiwkhRJIiFEHs9HE6SRnjHYU6hrwOBBGGfKuNylAvs-mninLtf9sPiCke-Sk90usNMEzwApqcGrMxv_T2OT71pqZcE4Sg8hQ2MWNHldTzZWHuDxMNGy5pYE3IT7BCDTGat_iu1xQGo7y7K3Rtnej3xpt64br8HIsT1Aw4g-QGN1bb8U-6iT9kre1tAJf6umW0-SP1MZQ2C261-r5NmOWmFEvJiU9LvaEfIUY6FZcyaVJXG__V83nMjiCxUp9tHCrLa-P_Sv3lPp8aS2ef71TLuzB14gOLKCzIWEovii0qfHRUfrJeAiwvZi3tDphKprIZYEr_qxvR0YCd4QLUqOwh_kWynztwPdo6ivRnqIRVfhLSgTEAArSrgWHFU1WC8Ckd6T5MpqJhN0x6x8qBePZGHAdYwz8qa9h7wiNLFWBrLRj5DmQLl1CVxnpVrjW33MFso4P8n060N4ghdKSSZsZozkNQ5b7O6yajYy-rSp6QpD8msb8oEX5imFKRaOcviQ2D4TRT45HJxKs63Tb9FtT1JoORzfkdv_E1bL3zSR6oYbTt2Stnpz-7kVqc8KR2N45EkFKxDkRw3IXOte0cq81xoU87S_ntf4KiVZaszuqb2XN2SgxnXBl4EDnpehPmqkD92SAlLrQcTaxaSe47G28K-8MwoVt4eeVkj4UEsSfJN7rbCH2yKl2XJx5huDaS0xn2ODQyNRmgk-5I9hXMUiZDNLvEzx4zuyrcu2d0oXFo3ZoUtVFNCB__TQCf2x27ej9GjLXLDAEi7qnl9Xfb94n0IfeVyGte3-j6NP3DWv8OrLiUjNTaLv6Fay1yzfUaU6LI86-Jd6ckloiGhg7kE0_hd-ZKakZxU1vh0Vzc6DW7MFAPky75iCZlDXoBpZjTNGo5HR-mCW_ozblu60U9zZA8bn-voANuu_hYwxh-uY1sHTFZOqp2xicnnMChz_GTm1Je8XCkICYegeiHUryEHA6T6B_L9gW8S_R4ptMD0Sv6b1KHqqKeubwKltCWPUsr2En9iYypnz06DEL5Wp8KMhrLid2AMPpLI0j1CWGJExXHpBWjfIC8vbYH4YKVl-euRo8eDcuKosb5hxUGM9Jvy1siVXUpIKpkZt2YLP5pEBP_EVOoHPh5LJomrLMpORr1wBKbEkfom7npX1g817bK4IeYmZELI8zXUUtUkx3LgNTckwjx90Vt6oVXpFEICIUDF_LAVMUftzz6JUvbwOZo8iAZqcnVslAmRXeY_ZPp5eEHFfHlsb8VQ73Rd_p8XlFf5R1WuWiUGp2TzJ-VQvj3BTdQfOwSxR9RUk4xjqNabLqTFcQ7As246bHJXH6XVnd4DbEIDPfNa8FaWb_DNEgQAiXGqa6n7l7aFq5_6Kp0XeBBM0sOzJt4fy8JC6U0DEcMnWxKFDtMM7q06LubQYFCEEdQ5b1Qh2LbQZ898tegmeF--EZ4F4hvYebZPV8sM0ZcsKBXyCr585qs00PRxr0S6rReekGRBIvXzMojmid3dxc6DPpdV3x5zxlxaIBxO3i_6axknSSdxnS04_bemWqQ3CLf6mpSqfTIQJT1407GB4QINAAC9Ch3AXUR_n1jr64TGWzbIr8uDcnoVCJlOgmlXpmOwubigAzJattbWRi7k4QYBnA3_4QMjt73n2Co4-F_Qh4boYLpmwWG2SwcIw2PeXGr2LY2zwkPR4bcSyx1Z6UK5trQpWlpQCxgsvV_RvGzpN22RtHoihPH74K0cBIzCz7tK-jqeuWl1A7af7KmQ66fpRBr5ykTLOsa17WblkcIB_jDvqKfEcdxhPWJUwmOo4TIQS-xH8arLOy_NQFG2m14_yxwUemXC-QxLUYi6_FIcqwPBKjCdpQtadRdyftQSKO0SP-GxUvamMZzWI780rXuOBkq5kyYLy9QF9bf_-bL6QLpe1WMCQlOeXZaCPoncgYoT0WZ17jB52Xb2lPWsyXYK54npszkbKJ4OIqfvF8xqRXcVe22VwJuqT9Uy4-4KKQgQ7TXla7Gdm2H7mKl8YXQlsGCT2Ypc8O4t0Sfw7qYAuaDGf752Hbm3fl1bupcB2huIPlIaDP6IRR9XvTYIW2flbwYfhKLmoVKnG85uUi2qtqCjPOIuU3-peT0othfmwKQXaoOqO-V4r6wPL1VHxVFtIYmEdVt0RccUOvpOVR_OAHG9uHOzTmueK5557Qxp0ojtZCHyN-hgoMZJLrvdKkTCxPNo2-mZQbHoVh2FnThZ9JbO49dB8lKXP4_MU5xAnjXMgKXtbfI8w6ZWATE_XWgf2VQMUpGp4wpy44yWQTxHxh_4T9540BGwG0FU0bkgrwA_erseGZnepqdmz5_ScCs84O5Xr5MbYhJLCGGxY6O5GqS-ooB2w0Mt87KbbE4bpYje9CAHH8FX3pDrJyLsyasA3zxmk4OmGpG7Z70ofONJtHRe56R5287vFmuazEEutXn81kNzB-3aJT1ga3vnWZw4CSvFKoWYSA7auLgrHSHFZdITfOrgtmQmGbFhM9kSBdY1UCnpzf65oos3PZWRa2twfUxxLAnPNtrxpRGyvtsapw7ljUagZmuyh3hLCjhAxYmnoE1dbyIWvpCqSlEtVjL1yb_nuLEzgvmZuV02fHxGuWgHTOMVGXpf81Rce3eoBK3lapW1wkzezlk3tcA2bZOtA9qbxdsbVR37kemzQ9K1e3Y0OWhtSj', + priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', } }, ]) { - const publicKey = createPublicKey({ format: 'jwk', key: jwk }); + const privateKey = createPrivateKey({ format: 'jwk', key: jwk }); + const publicKey = createPublicKey({ format: 'jwk', key: { ...jwk, priv: undefined } }); assert.strictEqual(verify(null, data, publicKey, signature), true); + assert.strictEqual(verify(null, data, publicKey, sign(null, data, privateKey)), true); } } From 4678673b9287031016225837c91419c511f9059c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 18:50:51 +0200 Subject: [PATCH 08/13] apply feedback (first of many) --- deps/ncrypto/ncrypto.cc | 7 +++++-- src/crypto/crypto_ml_dsa.cc | 27 +++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 4a0dd8b2d8420e..8dd33fd71fddab 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1909,11 +1909,14 @@ EVPKeyPointer EVPKeyPointer::NewRawSeed( OSSL_PARAM_END}; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); - EVP_PKEY* pkey = nullptr; + if (ctx == nullptr) return {}; + EVP_PKEY* pkey = nullptr; if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 || - EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) + EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { + EVP_PKEY_CTX_free(ctx); return {}; + } return EVPKeyPointer(pkey); } diff --git a/src/crypto/crypto_ml_dsa.cc b/src/crypto/crypto_ml_dsa.cc index 74d0e9fe467f3b..4afd511569a0e4 100644 --- a/src/crypto/crypto_ml_dsa.cc +++ b/src/crypto/crypto_ml_dsa.cc @@ -15,24 +15,27 @@ using v8::Value; namespace crypto { #if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +constexpr const char* GetMlDsaAlgorithmName(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + return "ML-DSA-44"; + case EVP_PKEY_ML_DSA_65: + return "ML-DSA-65"; + case EVP_PKEY_ML_DSA_87: + return "ML-DSA-87"; + default: + return nullptr; + } +} + bool ExportJwkMlDsaKey(Environment* env, const KeyObjectData& key, Local target) { Mutex::ScopedLock lock(key.mutex()); const auto& pkey = key.GetAsymmetricKey(); - const char* alg = ([&] { - switch (pkey.id()) { - case EVP_PKEY_ML_DSA_44: - return "ML-DSA-44"; - case EVP_PKEY_ML_DSA_65: - return "ML-DSA-65"; - case EVP_PKEY_ML_DSA_87: - return "ML-DSA-87"; - default: - UNREACHABLE(); - } - })(); + const char* alg = GetMlDsaAlgorithmName(pkey.id()); + CHECK(alg); static constexpr auto trySetKey = [](Environment* env, DataPointer data, From e15987f887b3030dc216fe3486210049aa81e4cd Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 18:56:47 +0200 Subject: [PATCH 09/13] apply more feedback --- src/crypto/crypto_ml_dsa.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/crypto/crypto_ml_dsa.cc b/src/crypto/crypto_ml_dsa.cc index 4afd511569a0e4..119597c634cc11 100644 --- a/src/crypto/crypto_ml_dsa.cc +++ b/src/crypto/crypto_ml_dsa.cc @@ -28,6 +28,15 @@ constexpr const char* GetMlDsaAlgorithmName(int id) { } } +/** + * Exports an ML-DSA key to JWK format. + * + * The resulting JWK object contains: + * - "kty": "AKP" (Asymmetric Key Pair - required) + * - "alg": "ML-DSA-XX" (Algorithm identifier - required for "AKP") + * - "pub": "" (required) + * - "priv": <"Base64URL-encoded raw seed>" (required for private keys only) + */ bool ExportJwkMlDsaKey(Environment* env, const KeyObjectData& key, Local target) { From 7e614d783a8d9affe933e9f032e692ca2035980a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 20:27:31 +0200 Subject: [PATCH 10/13] fixup unregistered external reference --- src/crypto/crypto_keys.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index e069dbaf46f44c..167018d7d208c6 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -643,6 +643,9 @@ void KeyObjectHandle::RegisterExternalReferences( registry->Register(ExportJWK); registry->Register(InitECRaw); registry->Register(InitEDRaw); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + registry->Register(InitMlDsaRaw); +#endif registry->Register(InitJWK); registry->Register(GetKeyDetail); registry->Register(Equals); From 657b4b217f44626acbdb8dd944696fa8f7ad4115 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 29 Jul 2025 20:38:17 +0200 Subject: [PATCH 11/13] add benchmark for create*Key --- benchmark/crypto/create-keyobject.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/benchmark/crypto/create-keyobject.js b/benchmark/crypto/create-keyobject.js index 1e98ac347d7efc..75988031abf6cc 100644 --- a/benchmark/crypto/create-keyobject.js +++ b/benchmark/crypto/create-keyobject.js @@ -18,13 +18,14 @@ function readKeyPair(publicKeyName, privateKeyName) { } const keyFixtures = { - ec: readKeyPair('ec_p256_public', 'ec_p256_private'), - rsa: readKeyPair('rsa_public_2048', 'rsa_private_2048'), - ed25519: readKeyPair('ed25519_public', 'ed25519_private'), + 'ec': readKeyPair('ec_p256_public', 'ec_p256_private'), + 'rsa': readKeyPair('rsa_public_2048', 'rsa_private_2048'), + 'ed25519': readKeyPair('ed25519_public', 'ed25519_private'), + 'ml-dsa-44': readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'), }; const bench = common.createBenchmark(main, { - keyType: ['rsa', 'ec', 'ed25519'], + keyType: ['rsa', 'ec', 'ed25519', 'ml-dsa-44'], keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private'], n: [1e3], }); From fd954bf5665b9d3c98960eca68d98747524b9fbc Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 1 Aug 2025 11:25:01 +0200 Subject: [PATCH 12/13] resolve lint --- deps/ncrypto/ncrypto.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 8dd33fd71fddab..997a32c51f9ce6 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1904,7 +1904,7 @@ EVPKeyPointer EVPKeyPointer::NewRawSeed( OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string( OSSL_PKEY_PARAM_ML_DSA_SEED, - (void*)data.data, // NOLINT(readability/casting) + const_cast(data.data), data.len), OSSL_PARAM_END}; From 7b102391d1b8fb4abfe9149db65ae3b5675dfb40 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 1 Aug 2025 11:28:36 +0200 Subject: [PATCH 13/13] lint --- deps/ncrypto/ncrypto.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 997a32c51f9ce6..a2bfe874650fd5 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -1902,11 +1902,11 @@ EVPKeyPointer EVPKeyPointer::NewRawSeed( int id, const Buffer& data) { if (id == 0) return {}; - OSSL_PARAM params[] = {OSSL_PARAM_construct_octet_string( - OSSL_PKEY_PARAM_ML_DSA_SEED, - const_cast(data.data), - data.len), - OSSL_PARAM_END}; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED, + const_cast(data.data), + data.len), + OSSL_PARAM_END}; EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); if (ctx == nullptr) return {};