diff --git a/doc/api/crypto.md b/doc/api/crypto.md index c17899bb4f608d..e4f0a733046ede 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -4009,8 +4009,7 @@ changes: * `publicKey` {string | Buffer | KeyObject} * `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. +Generates a new asymmetric key pair of the given `type`. If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function behaves as if [`keyObject.export()`][] had been called on its result. Otherwise, @@ -4131,8 +4130,7 @@ changes: * `publicKey` {string | Buffer | KeyObject} * `privateKey` {string | Buffer | KeyObject} -Generates a new asymmetric key pair of the given `type`. RSA, RSA-PSS, DSA, EC, -Ed25519, Ed448, X25519, X448, DH, and ML-DSA[^openssl35] are currently supported. +Generates a new asymmetric key pair of the given `type`. If a `publicKeyEncoding` or `privateKeyEncoding` was specified, this function behaves as if [`keyObject.export()`][] had been called on its result. Otherwise, diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 0541a5f52da042..590781ba780d36 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -25,9 +25,6 @@ const { kKeyEncodingPKCS8, kKeyEncodingSPKI, kKeyEncodingSEC1, - EVP_PKEY_ML_DSA_44, - EVP_PKEY_ML_DSA_65, - EVP_PKEY_ML_DSA_87, } = internalBinding('crypto'); const { @@ -542,40 +539,79 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { return types; } -function mlDsaPubLen(alg) { +function akpPubLen(alg) { switch (alg) { case 'ML-DSA-44': return 1312; case 'ML-DSA-65': return 1952; case 'ML-DSA-87': return 2592; + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHAKE-128f': return 32; + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHAKE-192f': return 48; + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-256f': return 64; + } +} + +function akpPrivLen(alg) { + switch (alg) { + case 'ML-DSA-44': + case 'ML-DSA-65': + case 'ML-DSA-87': return 32; + case 'SLH-DSA-SHA2-128s': + case 'SLH-DSA-SHAKE-128s': + case 'SLH-DSA-SHA2-128f': + case 'SLH-DSA-SHAKE-128f': return 64; + case 'SLH-DSA-SHA2-192s': + case 'SLH-DSA-SHAKE-192s': + case 'SLH-DSA-SHA2-192f': + case 'SLH-DSA-SHAKE-192f': return 96; + case 'SLH-DSA-SHA2-256s': + case 'SLH-DSA-SHAKE-256s': + case 'SLH-DSA-SHA2-256f': + case 'SLH-DSA-SHAKE-256f': return 128; } } function getKeyObjectHandleFromJwk(key, ctx) { validateObject(key, 'key'); - if (EVP_PKEY_ML_DSA_44 || EVP_PKEY_ML_DSA_65 || EVP_PKEY_ML_DSA_87) { + if (KeyObjectHandle.prototype.initPqcRaw) { validateOneOf( key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']); } else { validateOneOf( key.kty, 'key.kty', ['RSA', 'EC', 'OKP']); } + const isPublic = ctx === kConsumePublic || ctx === kCreatePublic; if (key.kty === 'AKP') { validateOneOf( - key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']); + key.alg, 'key.alg', [ + 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', + 'SLH-DSA-SHA2-128s', 'SLH-DSA-SHAKE-128s', 'SLH-DSA-SHA2-128f', + 'SLH-DSA-SHAKE-128f', 'SLH-DSA-SHA2-192s', 'SLH-DSA-SHAKE-192s', + 'SLH-DSA-SHA2-192f', 'SLH-DSA-SHAKE-192f', 'SLH-DSA-SHA2-256s', + 'SLH-DSA-SHAKE-256s', 'SLH-DSA-SHA2-256f', 'SLH-DSA-SHAKE-256f', + ]); validateString(key.pub, 'key.pub'); let keyData; if (isPublic) { keyData = Buffer.from(key.pub, 'base64url'); - if (keyData.byteLength !== mlDsaPubLen(key.alg)) { + if (keyData.byteLength !== akpPubLen(key.alg)) { throw new ERR_CRYPTO_INVALID_JWK(); } } else { validateString(key.priv, 'key.priv'); keyData = Buffer.from(key.priv, 'base64url'); - if (keyData.byteLength !== 32) { + if (keyData.byteLength !== akpPrivLen(key.alg)) { throw new ERR_CRYPTO_INVALID_JWK(); } } diff --git a/node.gyp b/node.gyp index a80260ba173fc9..96dc582bd773e0 100644 --- a/node.gyp +++ b/node.gyp @@ -370,6 +370,7 @@ 'src/crypto/crypto_context.cc', 'src/crypto/crypto_ec.cc', 'src/crypto/crypto_ml_dsa.cc', + 'src/crypto/crypto_slh_dsa.cc', 'src/crypto/crypto_kem.cc', 'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_kmac.cc', @@ -406,6 +407,7 @@ 'src/crypto/crypto_context.h', 'src/crypto/crypto_ec.h', 'src/crypto/crypto_ml_dsa.h', + 'src/crypto/crypto_slh_dsa.h', 'src/crypto/crypto_hkdf.h', 'src/crypto/crypto_pbkdf2.h', 'src/crypto/crypto_sig.h', diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index e805a984322c83..84b9c70d30b131 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -7,6 +7,7 @@ #include "crypto/crypto_ec.h" #include "crypto/crypto_ml_dsa.h" #include "crypto/crypto_rsa.h" +#include "crypto/crypto_slh_dsa.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -184,6 +185,30 @@ bool ExportJWKAsymmetricKey(Environment* env, // Fall through case EVP_PKEY_ML_DSA_87: return ExportJwkMlDsaKey(env, key, target); + case EVP_PKEY_SLH_DSA_SHA2_128F: + // Fall through + case EVP_PKEY_SLH_DSA_SHA2_128S: + // Fall through + case EVP_PKEY_SLH_DSA_SHA2_192F: + // Fall through + case EVP_PKEY_SLH_DSA_SHA2_192S: + // Fall through + case EVP_PKEY_SLH_DSA_SHA2_256F: + // Fall through + case EVP_PKEY_SLH_DSA_SHA2_256S: + // Fall through + case EVP_PKEY_SLH_DSA_SHAKE_128F: + // Fall through + case EVP_PKEY_SLH_DSA_SHAKE_128S: + // Fall through + case EVP_PKEY_SLH_DSA_SHAKE_192F: + // Fall through + case EVP_PKEY_SLH_DSA_SHAKE_192S: + // Fall through + case EVP_PKEY_SLH_DSA_SHAKE_256F: + // Fall through + case EVP_PKEY_SLH_DSA_SHAKE_256S: + return ExportJwkSlhDsaKey(env, key, target); #endif } THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env); @@ -293,6 +318,30 @@ int GetNidFromName(const char* name) { nid = EVP_PKEY_ML_KEM_768; } else if (strcmp(name, "ML-KEM-1024") == 0) { nid = EVP_PKEY_ML_KEM_1024; + } else if (strcmp(name, "SLH-DSA-SHA2-128f") == 0) { + nid = EVP_PKEY_SLH_DSA_SHA2_128F; + } else if (strcmp(name, "SLH-DSA-SHA2-128s") == 0) { + nid = EVP_PKEY_SLH_DSA_SHA2_128S; + } else if (strcmp(name, "SLH-DSA-SHA2-192f") == 0) { + nid = EVP_PKEY_SLH_DSA_SHA2_192F; + } else if (strcmp(name, "SLH-DSA-SHA2-192s") == 0) { + nid = EVP_PKEY_SLH_DSA_SHA2_192S; + } else if (strcmp(name, "SLH-DSA-SHA2-256f") == 0) { + nid = EVP_PKEY_SLH_DSA_SHA2_256F; + } else if (strcmp(name, "SLH-DSA-SHA2-256s") == 0) { + nid = EVP_PKEY_SLH_DSA_SHA2_256S; + } else if (strcmp(name, "SLH-DSA-SHAKE-128f") == 0) { + nid = EVP_PKEY_SLH_DSA_SHAKE_128F; + } else if (strcmp(name, "SLH-DSA-SHAKE-128s") == 0) { + nid = EVP_PKEY_SLH_DSA_SHAKE_128S; + } else if (strcmp(name, "SLH-DSA-SHAKE-192f") == 0) { + nid = EVP_PKEY_SLH_DSA_SHAKE_192F; + } else if (strcmp(name, "SLH-DSA-SHAKE-192s") == 0) { + nid = EVP_PKEY_SLH_DSA_SHAKE_192S; + } else if (strcmp(name, "SLH-DSA-SHAKE-256f") == 0) { + nid = EVP_PKEY_SLH_DSA_SHAKE_256F; + } else if (strcmp(name, "SLH-DSA-SHAKE-256s") == 0) { + nid = EVP_PKEY_SLH_DSA_SHAKE_256S; #endif } else { nid = NID_undef; @@ -862,34 +911,53 @@ void KeyObjectHandle::InitPqcRaw(const FunctionCallbackInfo& args) { typedef EVPKeyPointer (*new_key_fn)( int, const ncrypto::Buffer&); - new_key_fn fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed - : EVPKeyPointer::NewRawPublic; int id = GetNidFromName(*name); + typedef EVPKeyPointer (*new_key_fn)( + int, const ncrypto::Buffer&); + new_key_fn fn; + switch (id) { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: case EVP_PKEY_ML_KEM_512: case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: { - 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(type, std::move(pkey)); - CHECK(key->data_); + case EVP_PKEY_ML_KEM_1024: + fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed + : EVPKeyPointer::NewRawPublic; + break; + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: + fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate + : EVPKeyPointer::NewRawPublic; break; - } default: UNREACHABLE(); } + 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(type, std::move(pkey)); + CHECK(key->data_); + args.GetReturnValue().Set(true); } #endif diff --git a/src/crypto/crypto_ml_dsa.cc b/src/crypto/crypto_ml_dsa.cc index 65f7053cc1fa1d..6f556537030d87 100644 --- a/src/crypto/crypto_ml_dsa.cc +++ b/src/crypto/crypto_ml_dsa.cc @@ -35,7 +35,7 @@ constexpr const char* GetMlDsaAlgorithmName(int id) { * - "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) + * - "priv": "" (required for private keys) */ bool ExportJwkMlDsaKey(Environment* env, const KeyObjectData& key, diff --git a/src/crypto/crypto_slh_dsa.cc b/src/crypto/crypto_slh_dsa.cc new file mode 100644 index 00000000000000..47a1333348b604 --- /dev/null +++ b/src/crypto/crypto_slh_dsa.cc @@ -0,0 +1,97 @@ +#include "crypto/crypto_slh_dsa.h" +#include "crypto/crypto_util.h" +#include "env-inl.h" +#include "string_bytes.h" +#include "v8.h" + +namespace node { + +using ncrypto::DataPointer; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +namespace crypto { + +#if OPENSSL_WITH_PQC +constexpr const char* GetSlhDsaAlgorithmName(int id) { + switch (id) { + case EVP_PKEY_SLH_DSA_SHA2_128F: + return "SLH-DSA-SHA2-128f"; + case EVP_PKEY_SLH_DSA_SHA2_128S: + return "SLH-DSA-SHA2-128s"; + case EVP_PKEY_SLH_DSA_SHA2_192F: + return "SLH-DSA-SHA2-192f"; + case EVP_PKEY_SLH_DSA_SHA2_192S: + return "SLH-DSA-SHA2-192s"; + case EVP_PKEY_SLH_DSA_SHA2_256F: + return "SLH-DSA-SHA2-256f"; + case EVP_PKEY_SLH_DSA_SHA2_256S: + return "SLH-DSA-SHA2-256s"; + case EVP_PKEY_SLH_DSA_SHAKE_128F: + return "SLH-DSA-SHAKE-128f"; + case EVP_PKEY_SLH_DSA_SHAKE_128S: + return "SLH-DSA-SHAKE-128s"; + case EVP_PKEY_SLH_DSA_SHAKE_192F: + return "SLH-DSA-SHAKE-192f"; + case EVP_PKEY_SLH_DSA_SHAKE_192S: + return "SLH-DSA-SHAKE-192s"; + case EVP_PKEY_SLH_DSA_SHAKE_256F: + return "SLH-DSA-SHAKE-256f"; + case EVP_PKEY_SLH_DSA_SHAKE_256S: + return "SLH-DSA-SHAKE-256s"; + default: + return nullptr; + } +} + +/** + * Exports an SLH-DSA key to JWK format. + * + * The resulting JWK object contains: + * - "kty": "AKP" (Asymmetric Key Pair - required) + * - "alg": "SLH-DSA-XX-XX" (Algorithm identifier - required for "AKP") + * - "pub": "" (required) + * - "priv": "" (required for private keys) + */ +bool ExportJwkSlhDsaKey(Environment* env, + const KeyObjectData& key, + Local target) { + Mutex::ScopedLock lock(key.mutex()); + const auto& pkey = key.GetAsymmetricKey(); + + const char* alg = GetSlhDsaAlgorithmName(pkey.id()); + CHECK(alg); + + static constexpr auto trySetKey = [](Environment* env, + DataPointer data, + Local target, + Local key) { + Local encoded; + if (!data) return false; + const ncrypto::Buffer out = data; + return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) + .ToLocal(&encoded) && + target->Set(env->context(), key, encoded).IsJust(); + }; + + if (key.GetKeyType() == kKeyTypePrivate) { + if (!trySetKey(env, pkey.rawPrivateKey(), target, env->jwk_priv_string())) { + return false; + } + } + + return !( + target->Set(env->context(), env->jwk_kty_string(), env->jwk_akp_string()) + .IsNothing() || + target + ->Set(env->context(), + env->jwk_alg_string(), + OneByteString(env->isolate(), alg)) + .IsNothing() || + !trySetKey(env, pkey.rawPublicKey(), target, env->jwk_pub_string())); +} +#endif +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_slh_dsa.h b/src/crypto/crypto_slh_dsa.h new file mode 100644 index 00000000000000..3ad55cba9dbe65 --- /dev/null +++ b/src/crypto/crypto_slh_dsa.h @@ -0,0 +1,21 @@ +#ifndef SRC_CRYPTO_CRYPTO_SLH_DSA_H_ +#define SRC_CRYPTO_CRYPTO_SLH_DSA_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "env.h" +#include "v8.h" + +namespace node { +namespace crypto { +#if OPENSSL_WITH_PQC +bool ExportJwkSlhDsaKey(Environment* env, + const KeyObjectData& key, + v8::Local target); +#endif +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_SLH_DSA_H_ diff --git a/src/node_crypto.h b/src/node_crypto.h index e5e29544b57a81..660842f932b81a 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -52,6 +52,7 @@ #include "crypto/crypto_rsa.h" #include "crypto/crypto_scrypt.h" #include "crypto/crypto_sig.h" +#include "crypto/crypto_slh_dsa.h" #include "crypto/crypto_spkac.h" #include "crypto/crypto_timing.h" #include "crypto/crypto_tls.h" diff --git a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js index 1b612de8b2e582..c54b3cac164440 100644 --- a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js @@ -18,16 +18,34 @@ function getKeyFileName(type, suffix) { return `${type.replaceAll('-', '_')}_${suffix}.pem`; } -for (const asymmetricKeyType of [ - 'slh-dsa-sha2-128f', 'slh-dsa-sha2-128s', 'slh-dsa-sha2-192f', 'slh-dsa-sha2-192s', - 'slh-dsa-sha2-256f', 'slh-dsa-sha2-256s', 'slh-dsa-shake-128f', 'slh-dsa-shake-128s', - 'slh-dsa-shake-192f', 'slh-dsa-shake-192s', 'slh-dsa-shake-256f', 'slh-dsa-shake-256s', +for (const [asymmetricKeyType, pubLen] of [ + ['slh-dsa-sha2-128f', 32], ['slh-dsa-sha2-128s', 32], ['slh-dsa-sha2-192f', 48], ['slh-dsa-sha2-192s', 48], + ['slh-dsa-sha2-256f', 64], ['slh-dsa-sha2-256s', 64], ['slh-dsa-shake-128f', 32], ['slh-dsa-shake-128s', 32], + ['slh-dsa-shake-192f', 48], ['slh-dsa-shake-192s', 48], ['slh-dsa-shake-256f', 64], ['slh-dsa-shake-256s', 64], ]) { const keys = { public: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'public'), 'ascii'), private: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private'), 'ascii'), }; + function assertJwk(jwk) { + assert.strictEqual(jwk.kty, 'AKP'); + assert.strictEqual(jwk.alg, asymmetricKeyType.toUpperCase().slice(0, -1) + asymmetricKeyType.slice(-1)); + assert.ok(jwk.pub); + assert.strictEqual(Buffer.from(jwk.pub, 'base64url').byteLength, pubLen); + } + + function assertPublicJwk(jwk) { + assertJwk(jwk); + assert.ok(!jwk.priv); + } + + function assertPrivateJwk(jwk) { + assertJwk(jwk); + assert.ok(jwk.priv); + assert.strictEqual(Buffer.from(jwk.priv, 'base64url').byteLength, pubLen * 2); + } + function assertKey(key) { assert.deepStrictEqual(key.asymmetricKeyDetails, {}); assert.strictEqual(key.asymmetricKeyType, asymmetricKeyType); @@ -40,8 +58,9 @@ for (const asymmetricKeyType of [ assert.strictEqual(key.type, 'public'); assert.strictEqual(key.export({ format: 'pem', type: 'spki' }), keys.public); key.export({ format: 'der', type: 'spki' }); - assert.throws(() => key.export({ format: 'jwk' }), - { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + const jwk = key.export({ format: 'jwk' }); + assertPublicJwk(jwk); + assert.strictEqual(key.equals(createPublicKey({ format: 'jwk', key: jwk })), true); } function assertPrivateKey(key) { @@ -50,8 +69,10 @@ for (const asymmetricKeyType of [ assertPublicKey(createPublicKey(key)); key.export({ format: 'der', type: 'pkcs8' }); assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); - assert.throws(() => key.export({ format: 'jwk' }), - { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + const jwk = key.export({ format: 'jwk' }); + assertPrivateJwk(jwk); + assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); + assert.ok(createPublicKey({ format: 'jwk', key: jwk })); } if (!hasOpenSSL(3, 5)) {