diff --git a/benchmark/crypto/create-keyobject.js b/benchmark/crypto/create-keyobject.js index 30f8213175df69..7cd6db2d567ad6 100644 --- a/benchmark/crypto/create-keyobject.js +++ b/benchmark/crypto/create-keyobject.js @@ -26,6 +26,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const bench = common.createBenchmark(main, { diff --git a/benchmark/crypto/kem.js b/benchmark/crypto/kem.js index ffdcac6d7fcb0d..a544fc2124afe9 100644 --- a/benchmark/crypto/kem.js +++ b/benchmark/crypto/kem.js @@ -24,6 +24,9 @@ if (hasOpenSSL(3, 5)) { keyFixtures['ml-kem-512'] = readKeyPair('ml_kem_512_public', 'ml_kem_512_private'); keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private'); keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private_seed_only'); + keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private_seed_only'); } if (hasOpenSSL(3, 2)) { keyFixtures['p-256'] = readKeyPair('ec_p256_public', 'ec_p256_private'); diff --git a/benchmark/crypto/oneshot-sign.js b/benchmark/crypto/oneshot-sign.js index d0abc7b5412e60..72e3726d9a5349 100644 --- a/benchmark/crypto/oneshot-sign.js +++ b/benchmark/crypto/oneshot-sign.js @@ -19,6 +19,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); diff --git a/benchmark/crypto/oneshot-verify.js b/benchmark/crypto/oneshot-verify.js index c6a24f52126eb2..8b397b02dbf285 100644 --- a/benchmark/crypto/oneshot-verify.js +++ b/benchmark/crypto/oneshot-verify.js @@ -26,6 +26,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index b7a0c96ee2ea60..38378b730aca66 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -7,6 +7,11 @@ #include #include #include +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +#include +#include +#include +#endif #include #include #include @@ -15,7 +20,7 @@ #include #include #include -#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 #include #endif #endif @@ -29,9 +34,13 @@ constexpr static PQCMapping pqc_mappings[] = { {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, + +#if OPENSSL_WITH_PQC_ML_KEM_512 + {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, +#endif +#if OPENSSL_WITH_PQC_SLH_DSA {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, @@ -44,6 +53,7 @@ constexpr static PQCMapping pqc_mappings[] = { {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, +#endif }; #endif @@ -67,6 +77,28 @@ using NetscapeSPKIPointer = DeleteFnPtr; static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; + +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +struct BoringSSLCipher { + const EVP_CIPHER* (*get)(); + const char* name; +}; + +constexpr BoringSSLCipher kBoringSSLCiphers[] = { + {EVP_aes_128_cbc, "aes-128-cbc"}, {EVP_aes_128_ctr, "aes-128-ctr"}, + {EVP_aes_128_ecb, "aes-128-ecb"}, {EVP_aes_128_gcm, "aes-128-gcm"}, + {EVP_aes_128_ofb, "aes-128-ofb"}, {EVP_aes_192_cbc, "aes-192-cbc"}, + {EVP_aes_192_ctr, "aes-192-ctr"}, {EVP_aes_192_ecb, "aes-192-ecb"}, + {EVP_aes_192_gcm, "aes-192-gcm"}, {EVP_aes_192_ofb, "aes-192-ofb"}, + {EVP_aes_256_cbc, "aes-256-cbc"}, {EVP_aes_256_ctr, "aes-256-ctr"}, + {EVP_aes_256_ecb, "aes-256-ecb"}, {EVP_aes_256_gcm, "aes-256-gcm"}, + {EVP_aes_256_ofb, "aes-256-ofb"}, {EVP_des_cbc, "des-cbc"}, + {EVP_des_ecb, "des-ecb"}, {EVP_des_ede, "des-ede"}, + {EVP_des_ede3_cbc, "des-ede3-cbc"}, {EVP_des_ede_cbc, "des-ede-cbc"}, + {EVP_rc2_cbc, "rc2-cbc"}, {EVP_rc4, "rc4"}, +}; + +#endif } // namespace // ============================================================================ @@ -1928,8 +1960,7 @@ DataPointer pbkdf2(const Digest& md, return {}; } -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 DataPointer argon2(const Buffer& pass, const Buffer& salt, uint32_t lanes, @@ -2022,7 +2053,6 @@ DataPointer argon2(const Buffer& pass, return {}; } #endif -#endif // ============================================================================ @@ -2070,27 +2100,99 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( } #if OPENSSL_WITH_PQC -EVPKeyPointer EVPKeyPointer::NewRawSeed( - int id, const Buffer& data) { - if (id == 0) return {}; +namespace { +constexpr size_t kPqcMlDsaSeedSize = 32; +constexpr size_t kPqcMlKemSeedSize = 64; + +size_t GetPqcSeedSize(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + return kPqcMlDsaSeedSize; +#if OPENSSL_WITH_PQC_ML_KEM_512 + case EVP_PKEY_ML_KEM_512: +#endif + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + return kPqcMlKemSeedSize; + default: + unreachable(); + } +} +#if OPENSSL_WITH_BORINGSSL_PQC +const EVP_PKEY_ALG* GetPqcSeedAlg(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + return EVP_pkey_ml_dsa_44(); + case EVP_PKEY_ML_DSA_65: + return EVP_pkey_ml_dsa_65(); + case EVP_PKEY_ML_DSA_87: + return EVP_pkey_ml_dsa_87(); + case EVP_PKEY_ML_KEM_768: + return EVP_pkey_ml_kem_768(); + case EVP_PKEY_ML_KEM_1024: + return EVP_pkey_ml_kem_1024(); + default: + unreachable(); + } +} +#else +const char* GetPqcSeedParamName(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + return OSSL_PKEY_PARAM_ML_DSA_SEED; + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + return OSSL_PKEY_PARAM_ML_KEM_SEED; + default: + unreachable(); + } +} +#endif + +EVPKeyPointer NewPqcKeyFromSeed(int id, + const Buffer& data) { +#if OPENSSL_WITH_BORINGSSL_PQC + return EVPKeyPointer( + EVP_PKEY_from_private_seed(GetPqcSeedAlg(id), data.data, data.len)); +#else OSSL_PARAM params[] = { - OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED, + OSSL_PARAM_construct_octet_string(GetPqcSeedParamName(id), const_cast(data.data), data.len), OSSL_PARAM_END}; - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); - if (ctx == nullptr) return {}; + auto ctx = EVPKeyCtxPointer::NewFromID(id); + if (!ctx) 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_CTX_free(ctx); + if (EVP_PKEY_fromdata_init(ctx.get()) <= 0 || + EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { return {}; } - return EVPKeyPointer(pkey); +#endif +} + +bool GetPqcSeed(EVP_PKEY* pkey, int id, const Buffer& out) { + size_t len = out.len; +#if OPENSSL_WITH_BORINGSSL_PQC + return EVP_PKEY_get_private_seed(pkey, out.data, &len) == 1; +#else + return EVP_PKEY_get_octet_string_param( + pkey, GetPqcSeedParamName(id), out.data, out.len, &len) == 1; +#endif +} +} // namespace + +EVPKeyPointer EVPKeyPointer::NewRawSeed( + int id, const Buffer& data) { + return NewPqcKeyFromSeed(id, data); } #endif @@ -2140,7 +2242,7 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); -#if OPENSSL_WITH_PQC +#if OPENSSL_WITH_OPENSSL_PQC // EVP_PKEY_id returns -1 when EVP_PKEY_* is only implemented in a provider // which is the case for all post-quantum NIST algorithms // one suggested way would be to use a chain of `EVP_PKEY_is_a` @@ -2218,34 +2320,11 @@ DataPointer EVPKeyPointer::rawPublicKey() const { DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; - // Determine seed length and parameter name based on key type - size_t seed_len; - const char* param_name; - - switch (id()) { - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - seed_len = 32; // ML-DSA uses 32-byte seeds - param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; - break; - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - seed_len = 64; // ML-KEM uses 64-byte seeds - param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; - break; - default: - unreachable(); - } + const size_t seed_len = GetPqcSeedSize(id()); if (auto data = DataPointer::Alloc(seed_len)) { const Buffer buf = data; - size_t len = data.size(); - - if (EVP_PKEY_get_octet_string_param( - get(), param_name, buf.data, len, &seed_len) != 1) - return {}; + if (!GetPqcSeed(get(), id(), buf)) return {}; return data; } return {}; @@ -2287,6 +2366,7 @@ EVPKeyPointer::operator const EC_KEY*() const { } namespace { + EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, const char* name, auto&& parse) { @@ -2714,6 +2794,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: +#if OPENSSL_WITH_PQC_SLH_DSA case EVP_PKEY_SLH_DSA_SHA2_128F: case EVP_PKEY_SLH_DSA_SHA2_128S: case EVP_PKEY_SLH_DSA_SHA2_192F: @@ -2726,6 +2807,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_SLH_DSA_SHAKE_192S: case EVP_PKEY_SLH_DSA_SHAKE_256F: case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif #endif return true; default: @@ -4209,6 +4291,12 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { CipherCallbackContext context; context.cb = std::move(callback); +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK + for (const auto& cipher : kBoringSSLCiphers) { + static_cast(cipher.get); + context.cb(cipher.name); + } +#else EVP_CIPHER_do_all_sorted( #if OPENSSL_VERSION_MAJOR >= 3 array_push_back, #endif &context); +#endif } // ============================================================================ @@ -4369,7 +4458,17 @@ std::optional EVPMDCtxPointer::signInitWithContext( const EVPKeyPointer& key, const Digest& digest, const Buffer& context_string) { -#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING +#ifdef OPENSSL_IS_BORINGSSL + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + if (EVP_PKEY_CTX_set1_signature_context_string( + ctx, context_string.data, context_string.len) <= 0) { + return std::nullopt; + } + return ctx; +#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) EVP_PKEY_CTX* ctx = nullptr; #ifdef OSSL_SIGNATURE_PARAM_INSTANCE @@ -4414,7 +4513,17 @@ std::optional EVPMDCtxPointer::verifyInitWithContext( const EVPKeyPointer& key, const Digest& digest, const Buffer& context_string) { -#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING +#ifdef OPENSSL_IS_BORINGSSL + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + if (EVP_PKEY_CTX_set1_signature_context_string( + ctx, context_string.data, context_string.len) <= 0) { + return std::nullopt; + } + return ctx; +#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) EVP_PKEY_CTX* ctx = nullptr; #ifdef OSSL_SIGNATURE_PARAM_INSTANCE @@ -4580,7 +4689,7 @@ HMACCtxPointer HMACCtxPointer::New() { return HMACCtxPointer(HMAC_CTX_new()); } -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {} EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept @@ -4668,7 +4777,7 @@ EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) { if (!mac) return EVPMacCtxPointer(); return EVPMacCtxPointer(EVP_MAC_CTX_new(mac)); } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KMAC DataPointer hashDigest(const Buffer& buf, const EVP_MD* md) { @@ -4815,8 +4924,8 @@ const Digest Digest::FromName(const char* name) { // ============================================================================ // KEM Implementation -#if OPENSSL_VERSION_MAJOR >= 3 -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM +#if OPENSSL_WITH_KEM_OPERATION_PARAM bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { const char* operation = nullptr; @@ -4824,7 +4933,7 @@ bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { case EVP_PKEY_RSA: operation = OSSL_KEM_PARAM_OPERATION_RSASVE; break; -#if OPENSSL_VERSION_PREREQ(3, 2) +#if OPENSSL_WITH_OPENSSL_DHKEM case EVP_PKEY_EC: case EVP_PKEY_X25519: case EVP_PKEY_X448: @@ -4861,7 +4970,7 @@ std::optional KEM::Encapsulate( return std::nullopt; } -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM_OPERATION_PARAM if (!SetOperationParameter(ctx.get(), public_key)) { return std::nullopt; } @@ -4902,7 +5011,7 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return {}; } -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM_OPERATION_PARAM if (!SetOperationParameter(ctx.get(), private_key)) { return {}; } @@ -4932,6 +5041,6 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return shared_key; } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KEM } // namespace ncrypto diff --git a/deps/ncrypto/ncrypto.gyp b/deps/ncrypto/ncrypto.gyp index cf9b7c6cdb6d2c..1747f3ea0149b9 100644 --- a/deps/ncrypto/ncrypto.gyp +++ b/deps/ncrypto/ncrypto.gyp @@ -1,5 +1,6 @@ { 'variables': { + 'ncrypto_bssl_libdecrepit_missing%': 1, 'ncrypto_sources': [ 'engine.cc', 'ncrypto.cc', @@ -11,8 +12,14 @@ 'target_name': 'ncrypto', 'type': 'static_library', 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + ], 'direct_dependent_settings': { 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + ], }, 'sources': [ '<@(ncrypto_sources)' ], 'conditions': [ diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 1f116169f57a27..b27e2e76c3dcfc 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -22,22 +22,103 @@ #ifndef OPENSSL_NO_ENGINE #include #endif // !OPENSSL_NO_ENGINE + +#ifndef OPENSSL_VERSION_PREREQ +#define OPENSSL_VERSION_PREREQ(maj, min) \ + (OPENSSL_VERSION_NUMBER >= (((maj) << 28) | ((min) << 20))) +#endif + +// BoringSSL declares the EVP_*_do_all* APIs, but their implementation may +// live in libdecrepit. This matches standalone ncrypto's build flag. +#ifndef NCRYPTO_BSSL_LIBDECREPIT_MISSING +#define NCRYPTO_BSSL_LIBDECREPIT_MISSING 0 +#endif + +#if defined(OPENSSL_IS_BORINGSSL) && NCRYPTO_BSSL_LIBDECREPIT_MISSING +#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 1 +#else +#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 0 +#endif + // The FIPS-related functions are only available // when the OpenSSL itself was compiled with FIPS support. -#if defined(OPENSSL_FIPS) && OPENSSL_VERSION_MAJOR < 3 +#if defined(OPENSSL_FIPS) && !OPENSSL_VERSION_PREREQ(3, 0) #include #endif // OPENSSL_FIPS -// Define OPENSSL_WITH_PQC for post-quantum cryptography support -#if OPENSSL_VERSION_NUMBER >= 0x30500000L -#define OPENSSL_WITH_PQC 1 +#if OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_AES_OCB 1 +#else +#define OPENSSL_WITH_AES_OCB 0 +#endif + +#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_ARGON2 1 +#else +#define OPENSSL_WITH_ARGON2 0 +#endif + +#if OPENSSL_VERSION_PREREQ(3, 0) || defined(OPENSSL_IS_BORINGSSL) +#define OPENSSL_WITH_KEM 1 +#else +#define OPENSSL_WITH_KEM 0 +#endif + +#if OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_KMAC 1 +#else +#define OPENSSL_WITH_KMAC 0 +#endif + +#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 1 +#else +#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 0 +#endif + +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_OPENSSL_DHKEM 1 +#else +#define OPENSSL_WITH_OPENSSL_DHKEM 0 +#endif + +#if OPENSSL_WITH_KEM && !defined(OPENSSL_IS_BORINGSSL) && \ + !OPENSSL_VERSION_PREREQ(3, 5) +#define OPENSSL_WITH_KEM_OPERATION_PARAM 1 +#else +#define OPENSSL_WITH_KEM_OPERATION_PARAM 0 +#endif + +// Post-quantum cryptography support. Keep these explicit so code can +// distinguish provider API shape from the available algorithm set. +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 5) +#define OPENSSL_WITH_OPENSSL_PQC 1 +#else +#define OPENSSL_WITH_OPENSSL_PQC 0 +#endif + +#ifdef OPENSSL_IS_BORINGSSL +#define OPENSSL_WITH_BORINGSSL_PQC 1 +#else +#define OPENSSL_WITH_BORINGSSL_PQC 0 +#endif + +#define OPENSSL_WITH_PQC \ + (OPENSSL_WITH_OPENSSL_PQC || OPENSSL_WITH_BORINGSSL_PQC) +#define OPENSSL_WITH_PQC_ML_KEM_512 OPENSSL_WITH_OPENSSL_PQC +#define OPENSSL_WITH_PQC_SLH_DSA OPENSSL_WITH_OPENSSL_PQC + +#if OPENSSL_WITH_OPENSSL_PQC #define EVP_PKEY_ML_KEM_512 NID_ML_KEM_512 #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #include +#elif OPENSSL_WITH_BORINGSSL_PQC +#define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 +#define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_PREREQ(3, 0) #define OSSL3_CONST const #else #define OSSL3_CONST @@ -1474,7 +1555,7 @@ class HMACCtxPointer final { DeleteFnPtr ctx_; }; -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC class EVPMacPointer final { public: EVPMacPointer() = default; @@ -1522,7 +1603,7 @@ class EVPMacCtxPointer final { private: DeleteFnPtr ctx_; }; -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KMAC #ifndef OPENSSL_NO_ENGINE class EnginePointer final { @@ -1635,8 +1716,7 @@ DataPointer pbkdf2(const Digest& md, uint32_t iterations, size_t length); -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID }; DataPointer argon2(const Buffer& pass, @@ -1650,11 +1730,10 @@ DataPointer argon2(const Buffer& pass, const Buffer& ad, Argon2Type type); #endif -#endif // ============================================================================ // KEM (Key Encapsulation Mechanism) -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM class KEM final { public: @@ -1678,13 +1757,13 @@ class KEM final { const Buffer& ciphertext); private: -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM_OPERATION_PARAM static bool SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key); #endif }; -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KEM // ============================================================================ // Version metadata diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 67b5f66e4c3320..663375b9e155d2 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -396,12 +396,11 @@ const kAlgorithmDefinitions = { // Conditionally supported algorithms const conditionalAlgorithms = { - 'AES-KW': !process.features.openssl_is_boringssl, 'AES-OCB': !!hasAesOcbMode, 'Argon2d': !!Argon2Job, 'Argon2i': !!Argon2Job, 'Argon2id': !!Argon2Job, - 'ChaCha20-Poly1305': !process.features.openssl_is_boringssl || + 'ChaCha20-Poly1305': process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), 'cSHAKE128': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'shake128'), diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 18bea7df03880d..cca3a5fb044d92 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -588,14 +588,18 @@ converters.ContextParams = createDictionaryConverter( key: 'context', converter: converters.BufferSource, validator(V, dict) { - let { 0: major, 1: minor } = process.versions.openssl.split('.'); - major = NumberParseInt(major, 10); - minor = NumberParseInt(minor, 10); - if (major > 3 || (major === 3 && minor >= 2)) { + if (process.features.openssl_is_boringssl) { this.validator = undefined; } else { - this.validator = validateZeroLength('ContextParams.context'); - this.validator(V, dict); + let { 0: major, 1: minor } = process.versions.openssl.split('.'); + major = NumberParseInt(major, 10); + minor = NumberParseInt(minor, 10); + if (major > 3 || (major === 3 && minor >= 2)) { + this.validator = undefined; + } else { + this.validator = validateZeroLength('ContextParams.context'); + this.validator(V, dict); + } } }, }, diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index fa619696ffd5b2..815c972837049a 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -181,6 +181,68 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, return WebCryptoCipherStatus::OK; } +#ifdef OPENSSL_IS_BORINGSSL +// AES Key Wrap using BoringSSL's low-level AES_wrap_key / AES_unwrap_key. +// BoringSSL does not expose EVP_aes_*_wrap via the +// EVP_CIPHER registry, so the EVP-based AES_Cipher path is unusable for +// AES-KW. This matches Chromium's WebCrypto AES-KW implementation. +WebCryptoCipherStatus AES_KW_Cipher(Environment* env, + const KeyObjectData& key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out) { + CHECK_EQ(key_data.GetKeyType(), kKeyTypeSecret); + + const unsigned key_bits = + static_cast(key_data.GetSymmetricKeySize()) * 8; + const auto key_bytes = + reinterpret_cast(key_data.GetSymmetricKey()); + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + + AES_KEY aes_key; + if (encrypt) { + // Input must be a multiple of 8 bytes and at least 16 bytes. + if (in.size() < 16 || in.size() % 8 != 0) { + return WebCryptoCipherStatus::FAILED; + } + if (AES_set_encrypt_key(key_bytes, key_bits, &aes_key) != 0) { + return WebCryptoCipherStatus::FAILED; + } + auto buf = DataPointer::Alloc(in.size() + 8); + int len = AES_wrap_key(&aes_key, + nullptr, + static_cast(buf.get()), + in.data(), + in.size()); + if (len < 0 || static_cast(len) != in.size() + 8) { + return WebCryptoCipherStatus::FAILED; + } + *out = ByteSource::Allocated(buf.release()); + } else { + // Input must be a multiple of 8 bytes and at least 24 bytes. + if (in.size() < 24 || in.size() % 8 != 0) { + return WebCryptoCipherStatus::FAILED; + } + if (AES_set_decrypt_key(key_bytes, key_bits, &aes_key) != 0) { + return WebCryptoCipherStatus::FAILED; + } + auto buf = DataPointer::Alloc(in.size() - 8); + int len = AES_unwrap_key(&aes_key, + nullptr, + static_cast(buf.get()), + in.data(), + in.size()); + if (len < 0 || static_cast(len) != in.size() - 8) { + return WebCryptoCipherStatus::FAILED; + } + *out = ByteSource::Allocated(buf.release()); + } + + return WebCryptoCipherStatus::OK; +} +#endif // OPENSSL_IS_BORINGSSL + // The AES_CTR implementation here takes it's inspiration from the chromium // implementation here: // https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/aes_ctr.cc @@ -465,6 +527,19 @@ Maybe AESCipherTraits::AdditionalConfig( } #undef V +#ifdef OPENSSL_IS_BORINGSSL + // On BoringSSL the KW variants have no backing EVP_CIPHER; they use + // low-level AES_wrap_key / AES_unwrap_key instead. + const bool is_kw = params->variant == AESKeyVariant::KW_128 || + params->variant == AESKeyVariant::KW_192 || + params->variant == AESKeyVariant::KW_256; + + if (is_kw) { + UseDefaultIV(params); + return JustVoid(); + } +#endif + if (!params->cipher) { THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); return Nothing(); diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 5627f9020bad54..401e7b2c338a1b 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -22,11 +22,23 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM) \ V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM) \ V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM) \ + VARIANTS_KW(V) + +#ifdef OPENSSL_IS_BORINGSSL +// BoringSSL does not expose EVP_aes_*_wrap via the EVP_CIPHER registry. +// Route AES-KW through low-level AES_wrap_key / AES_unwrap_key instead. +#define VARIANTS_KW(V) \ + V(KW_128, AES_KW_Cipher, static_cast(nullptr)) \ + V(KW_192, AES_KW_Cipher, static_cast(nullptr)) \ + V(KW_256, AES_KW_Cipher, static_cast(nullptr)) +#else +#define VARIANTS_KW(V) \ V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW) \ V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \ V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) +#endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_AES_OCB #define VARIANTS_OCB(V) \ V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \ V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \ diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index 7bb995ca51c0df..d5207f4be57bb2 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -2,8 +2,7 @@ #include "async_wrap-inl.h" #include "threadpoolwork-inl.h" -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 #include namespace node::crypto { @@ -159,4 +158,3 @@ void Argon2::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto #endif -#endif diff --git a/src/crypto/crypto_argon2.h b/src/crypto/crypto_argon2.h index 73e8460d204dd3..354d0a4be6f392 100644 --- a/src/crypto/crypto_argon2.h +++ b/src/crypto/crypto_argon2.h @@ -6,7 +6,7 @@ #include "crypto/crypto_util.h" namespace node::crypto { -#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 // Argon2 is a password-based key derivation algorithm // defined in https://datatracker.ietf.org/doc/html/rfc9106 diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc index 0fd3e0517317ca..43d63fa8c5e409 100644 --- a/src/crypto/crypto_chacha20_poly1305.cc +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -10,6 +10,9 @@ #include "v8.h" #include +#ifdef OPENSSL_IS_BORINGSSL +#include +#endif namespace node { @@ -110,10 +113,15 @@ Maybe ChaCha20Poly1305CipherTraits::AdditionalConfig( params->mode = mode; params->cipher = ncrypto::Cipher::CHACHA20_POLY1305; +#ifndef OPENSSL_IS_BORINGSSL + // On BoringSSL, ChaCha20-Poly1305 is not exposed via the EVP_CIPHER registry + // so FromNid() returns a null Cipher. We use EVP_AEAD directly in DoCipher + // instead. if (!params->cipher) { THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); return Nothing(); } +#endif // IV parameter (required) if (!ValidateIV(env, mode, args[offset], params)) { @@ -144,6 +152,75 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( return WebCryptoCipherStatus::INVALID_KEY_TYPE; } +#ifdef OPENSSL_IS_BORINGSSL + // BoringSSL does not expose ChaCha20-Poly1305 via the EVP_CIPHER registry; + // it is only available through the EVP_AEAD API. Matches Chromium's + // WebCrypto ChaCha20-Poly1305 implementation. + const auto key_bytes = + reinterpret_cast(key_data.GetSymmetricKey()); + const auto ad_bytes = params.additional_data.data(); + const auto ad_len = params.additional_data.size(); + const auto iv_bytes = params.iv.data(); + const auto iv_len = params.iv.size(); + + bssl::ScopedEVP_AEAD_CTX ctx; + if (!EVP_AEAD_CTX_init(ctx.get(), + EVP_aead_chacha20_poly1305(), + key_bytes, + key_data.GetSymmetricKeySize(), + kChaCha20Poly1305TagSize, + nullptr)) { + return WebCryptoCipherStatus::FAILED; + } + + if (cipher_mode == kWebCryptoCipherEncrypt) { + size_t out_len = 0; + const size_t max_out_len = in.size() + kChaCha20Poly1305TagSize; + auto buf = DataPointer::Alloc(max_out_len); + if (!EVP_AEAD_CTX_seal(ctx.get(), + static_cast(buf.get()), + &out_len, + max_out_len, + iv_bytes, + iv_len, + in.data(), + in.size(), + ad_bytes, + ad_len)) { + return WebCryptoCipherStatus::FAILED; + } + buf = buf.resize(out_len); + *out = ByteSource::Allocated(buf.release()); + return WebCryptoCipherStatus::OK; + } + + // Decrypt + if (in.size() < kChaCha20Poly1305TagSize) { + return WebCryptoCipherStatus::FAILED; + } + size_t out_len = 0; + const size_t max_out_len = in.size(); // at most |in_len| bytes written + auto buf = DataPointer::Alloc(max_out_len == 0 ? 1 : max_out_len); + if (!EVP_AEAD_CTX_open(ctx.get(), + static_cast(buf.get()), + &out_len, + max_out_len, + iv_bytes, + iv_len, + in.data(), + in.size(), + ad_bytes, + ad_len)) { + return WebCryptoCipherStatus::FAILED; + } + if (out_len == 0) { + *out = ByteSource(); + } else { + buf = buf.resize(out_len); + *out = ByteSource::Allocated(buf.release()); + } + return WebCryptoCipherStatus::OK; +#else auto ctx = CipherCtxPointer::New(); CHECK(ctx); @@ -242,6 +319,7 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( *out = ByteSource::Allocated(buf.release()); return WebCryptoCipherStatus::OK; +#endif // OPENSSL_IS_BORINGSSL } void ChaCha20Poly1305::Initialize(Environment* env, Local target) { diff --git a/src/crypto/crypto_cipher.cc b/src/crypto/crypto_cipher.cc index 2e9acf86099ee8..dec72c20412e4e 100644 --- a/src/crypto/crypto_cipher.cc +++ b/src/crypto/crypto_cipher.cc @@ -711,7 +711,7 @@ bool CipherBase::Final(std::unique_ptr* out) { static_cast(ctx_.getBlockSize()), BackingStoreInitializationMode::kUninitialized); -#if (OPENSSL_VERSION_NUMBER < 0x30000000L) +#if !OPENSSL_VERSION_PREREQ(3, 0) // OpenSSL v1.x doesn't verify the presence of the auth tag so do // it ourselves, see https://github.com/nodejs/node/issues/45874. if (kind_ == kDecipher && ctx_.isChaCha20Poly1305() && diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 9b76b900049484..c42926bb4ce61f 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,6 +7,10 @@ #include "threadpoolwork-inl.h" #include "v8.h" +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +#include +#endif + #include namespace node { @@ -41,6 +45,24 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); } +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +struct BoringSSLDigest { + const EVP_MD* (*get)(); + const char* name; +}; + +constexpr BoringSSLDigest kBoringSSLDigests[] = { + {EVP_md4, "md4"}, + {EVP_md5, "md5"}, + {EVP_sha1, "sha1"}, + {EVP_sha224, "sha224"}, + {EVP_sha256, "sha256"}, + {EVP_sha384, "sha384"}, + {EVP_sha512, "sha512"}, + {EVP_sha512_256, "sha512-256"}, +}; +#endif + #if OPENSSL_VERSION_MAJOR >= 3 void PushAliases(const char* name, void* data) { static_cast*>(data)->push_back(name); @@ -122,7 +144,12 @@ void SaveSupportedHashAlgorithms(const EVP_MD* md, const std::vector& GetSupportedHashAlgorithms(Environment* env) { if (env->supported_hash_algorithms.empty()) { MarkPopErrorOnReturn mark_pop_error_on_return; -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK + for (const auto& digest : kBoringSSLDigests) { + static_cast(digest.get); + env->supported_hash_algorithms.emplace_back(digest.name); + } +#elif OPENSSL_VERSION_MAJOR >= 3 // Since we'll fetch the EVP_MD*, cache them along the way to speed up // later lookups instead of throwing them away immediately. EVP_MD_do_all_sorted(SaveSupportedHashAlgorithmsAndCacheMD, env); diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index dff69f2e18f947..d30c6aaef6253f 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -1,6 +1,6 @@ #include "crypto/crypto_kem.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM #include "async_wrap-inl.h" #include "base_object-inl.h" diff --git a/src/crypto/crypto_kem.h b/src/crypto/crypto_kem.h index 2b4671cfc7a0ec..e00aa04baa897e 100644 --- a/src/crypto/crypto_kem.h +++ b/src/crypto/crypto_kem.h @@ -10,7 +10,7 @@ #include "memory_tracker.h" #include "node_external_reference.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM namespace node { namespace crypto { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 0ca1d536c16582..aac059696596e4 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -177,7 +177,11 @@ bool ExportJWKAsymmetricKey(Environment* env, const KeyObjectData& key, Local target, bool handleRsaPss) { - switch (key.GetAsymmetricKey().id()) { + const int id = key.GetAsymmetricKey().id(); +#if OPENSSL_WITH_PQC + if (IsPqcKeyId(id)) return ExportJwkPqcKey(env, key, target); +#endif + switch (id) { case EVP_PKEY_RSA_PSS: { if (handleRsaPss) return ExportJWKRsaKey(env, key, target); break; @@ -187,51 +191,10 @@ bool ExportJWKAsymmetricKey(Environment* env, case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target); case EVP_PKEY_ED25519: - // Fall through case EVP_PKEY_ED448: - // Fall through case EVP_PKEY_X25519: - // Fall through case EVP_PKEY_X448: return ExportJWKEdKey(env, key, target); -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - // Fall through - case EVP_PKEY_ML_DSA_65: - // Fall through - case EVP_PKEY_ML_DSA_87: - // Fall through - 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: - // Fall through - case EVP_PKEY_ML_KEM_512: - // Fall through - case EVP_PKEY_ML_KEM_768: - // Fall through - case EVP_PKEY_ML_KEM_1024: - return ExportJwkPqcKey(env, key, target); -#endif } THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env); return false; @@ -306,35 +269,19 @@ int GetNidFromName(const char* name) { const char* name; int nid; } kNameToNid[] = { - {"Ed25519", EVP_PKEY_ED25519}, - {"Ed448", EVP_PKEY_ED448}, - {"X25519", EVP_PKEY_X25519}, - {"X448", EVP_PKEY_X448}, -#if OPENSSL_WITH_PQC - {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, - {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, - {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, - {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, - {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, - {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, - {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, - {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, - {"SLH-DSA-SHA2-192s", EVP_PKEY_SLH_DSA_SHA2_192S}, - {"SLH-DSA-SHA2-256f", EVP_PKEY_SLH_DSA_SHA2_256F}, - {"SLH-DSA-SHA2-256s", EVP_PKEY_SLH_DSA_SHA2_256S}, - {"SLH-DSA-SHAKE-128f", EVP_PKEY_SLH_DSA_SHAKE_128F}, - {"SLH-DSA-SHAKE-128s", EVP_PKEY_SLH_DSA_SHAKE_128S}, - {"SLH-DSA-SHAKE-192f", EVP_PKEY_SLH_DSA_SHAKE_192F}, - {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, - {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, - {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, -#endif + {"Ed25519", EVP_PKEY_ED25519}, + {"Ed448", EVP_PKEY_ED448}, + {"X25519", EVP_PKEY_X25519}, + {"X448", EVP_PKEY_X448}, }; for (const auto& entry : kNameToNid) { if (StringEqualNoCase(name, entry.name)) return entry.nid; } +#if OPENSSL_WITH_PQC + return GetPqcNidFromName(name); +#else return NID_undef; +#endif } bool IsUnavailablePqcKeyType(Environment* env, Local key_type) { @@ -442,35 +389,15 @@ bool KeyObjectData::ToEncodedPublicKey( const auto point = ECKeyPointer::GetPublicKey(ec_key); return ECPointToBuffer(env, group, point, form).ToLocal(out); } - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - 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: - 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: + is_raw_supported = is_raw_supported || IsPqcKeyId(id); #endif - break; - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!is_raw_supported) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } auto raw_data = pkey.rawPublicKey(); if (!raw_data) { @@ -517,29 +444,15 @@ bool KeyObjectData::ToEncodedPrivateKey( } return Buffer::Copy(env, buf.get(), buf.size()).ToLocal(out); } - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - 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: + is_raw_supported = is_raw_supported || IsPqcRawPrivateKeyId(id); #endif - break; - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!is_raw_supported) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } auto raw_data = pkey.rawPrivateKey(); if (!raw_data) { @@ -549,23 +462,13 @@ bool KeyObjectData::ToEncodedPrivateKey( return Buffer::Copy(env, raw_data.get(), raw_data.size()) .ToLocal(out); } else if (config.format == EVPKeyPointer::PKFormatType::RAW_SEED) { +#if OPENSSL_WITH_PQC Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); - switch (pkey.id()) { -#if OPENSSL_WITH_PQC - 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: - break; -#endif - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!IsPqcSeedKeyId(pkey.id())) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } -#if OPENSSL_WITH_PQC auto raw_data = pkey.rawSeed(); if (!raw_data) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); @@ -738,33 +641,17 @@ static KeyObjectData ImportRawKey(Environment* env, fn = target_type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate : EVPKeyPointer::NewRawPublic; break; + default: #if OPENSSL_WITH_PQC - 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: - fn = target_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 = target_type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate - : EVPKeyPointer::NewRawPublic; - break; + if (IsPqcKeyId(id)) { + if (target_type == kKeyTypePrivate) { + fn = IsPqcSeedKeyId(id) ? EVPKeyPointer::NewRawSeed + : EVPKeyPointer::NewRawPrivate; + } else { + fn = EVPKeyPointer::NewRawPublic; + } + } #endif - default: break; } @@ -1377,45 +1264,12 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { case EVP_PKEY_X448: return env()->crypto_x448_string(); #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - return env()->crypto_ml_dsa_44_string(); - case EVP_PKEY_ML_DSA_65: - return env()->crypto_ml_dsa_65_string(); - case EVP_PKEY_ML_DSA_87: - return env()->crypto_ml_dsa_87_string(); - case EVP_PKEY_ML_KEM_512: - return env()->crypto_ml_kem_512_string(); - case EVP_PKEY_ML_KEM_768: - return env()->crypto_ml_kem_768_string(); - case EVP_PKEY_ML_KEM_1024: - return env()->crypto_ml_kem_1024_string(); - case EVP_PKEY_SLH_DSA_SHA2_128F: - return env()->crypto_slh_dsa_sha2_128f_string(); - case EVP_PKEY_SLH_DSA_SHA2_128S: - return env()->crypto_slh_dsa_sha2_128s_string(); - case EVP_PKEY_SLH_DSA_SHA2_192F: - return env()->crypto_slh_dsa_sha2_192f_string(); - case EVP_PKEY_SLH_DSA_SHA2_192S: - return env()->crypto_slh_dsa_sha2_192s_string(); - case EVP_PKEY_SLH_DSA_SHA2_256F: - return env()->crypto_slh_dsa_sha2_256f_string(); - case EVP_PKEY_SLH_DSA_SHA2_256S: - return env()->crypto_slh_dsa_sha2_256s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_128F: - return env()->crypto_slh_dsa_shake_128f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_128S: - return env()->crypto_slh_dsa_shake_128s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_192F: - return env()->crypto_slh_dsa_shake_192f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_192S: - return env()->crypto_slh_dsa_shake_192s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_256F: - return env()->crypto_slh_dsa_shake_256f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_256S: - return env()->crypto_slh_dsa_shake_256s_string(); -#endif + default: + return GetPqcAsymmetricKeyType(env(), data_.GetAsymmetricKey().id()); +#else default: return Undefined(env()->isolate()); +#endif } } @@ -1524,34 +1378,14 @@ void KeyObjectHandle::RawPublicKey( Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - 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: - 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: + is_raw_supported = is_raw_supported || IsPqcKeyId(id); #endif - break; - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!is_raw_supported) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } auto raw_data = pkey.rawPublicKey(); @@ -1577,28 +1411,14 @@ void KeyObjectHandle::RawPrivateKey( Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - 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: + is_raw_supported = is_raw_supported || IsPqcRawPrivateKeyId(id); #endif - break; - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!is_raw_supported) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } auto raw_data = pkey.rawPrivateKey(); @@ -1687,24 +1507,14 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); +#if OPENSSL_WITH_PQC Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { -#if OPENSSL_WITH_PQC - 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: - break; -#endif - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!IsPqcSeedKeyId(pkey.id())) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } -#if OPENSSL_WITH_PQC auto raw_data = pkey.rawSeed(); if (!raw_data) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); @@ -1713,6 +1523,8 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { args.GetReturnValue().Set( Buffer::Copy(env, raw_data.get(), raw_data.size()) .FromMaybe(Local())); +#else + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); #endif } @@ -2180,9 +1992,12 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_44); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_65); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_87); +#if OPENSSL_WITH_PQC_ML_KEM_512 NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_512); +#endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_768); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_1024); +#if OPENSSL_WITH_PQC_SLH_DSA NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_192F); @@ -2195,6 +2010,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_192S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_256F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_256S); +#endif #endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index fd431ffc1b47b7..ed4a8e9d526983 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -3,7 +3,7 @@ #include "node_internals.h" #include "threadpoolwork-inl.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC #include #include #include "crypto/crypto_keys.h" @@ -220,4 +220,4 @@ void Kmac::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto -#endif +#endif // OPENSSL_WITH_KMAC diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 9ee6192ee3dd17..5a8c9e5039f22b 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -10,8 +10,7 @@ namespace node::crypto { -// KMAC (Keccak Message Authentication Code) is available since OpenSSL 3.0. -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC enum class KmacVariant { KMAC128, KMAC256 }; @@ -72,7 +71,7 @@ namespace Kmac { void Initialize(Environment* env, v8::Local target) {} void RegisterExternalReferences(ExternalReferenceRegistry* registry) {} } // namespace Kmac -#endif +#endif // OPENSSL_WITH_KMAC } // namespace node::crypto diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc index bf40052fb6ea1e..8d4af1e7801180 100644 --- a/src/crypto/crypto_pqc.cc +++ b/src/crypto/crypto_pqc.cc @@ -17,32 +17,105 @@ namespace crypto { #if OPENSSL_WITH_PQC namespace { +using PqcKeyTypeGetter = Local (Environment::*)() const; + +enum PqcAlgorithmFlag { + kPqcRawPrivate = 1 << 0, + kPqcRawSeed = 1 << 1, + kPqcSignature = 1 << 2, +}; + struct PqcAlgorithm { int id; const char* name; - bool - use_seed; // true: rawSeed/NewRawSeed, false: rawPrivateKey/NewRawPrivate + PqcKeyTypeGetter key_type; + int flags; }; +// ML-DSA and ML-KEM carry private material as a seed. SLH-DSA uses the +// expanded private key and is only exposed by OpenSSL. +constexpr int kPqcMlDsaFlags = kPqcRawSeed | kPqcSignature; +constexpr int kPqcMlKemFlags = kPqcRawSeed; +constexpr int kPqcSlhDsaFlags = kPqcRawPrivate | kPqcSignature; + constexpr PqcAlgorithm kPqcAlgorithms[] = { - {EVP_PKEY_ML_DSA_44, "ML-DSA-44", true}, - {EVP_PKEY_ML_DSA_65, "ML-DSA-65", true}, - {EVP_PKEY_ML_DSA_87, "ML-DSA-87", true}, - {EVP_PKEY_ML_KEM_512, "ML-KEM-512", true}, - {EVP_PKEY_ML_KEM_768, "ML-KEM-768", true}, - {EVP_PKEY_ML_KEM_1024, "ML-KEM-1024", true}, - {EVP_PKEY_SLH_DSA_SHA2_128F, "SLH-DSA-SHA2-128f", false}, - {EVP_PKEY_SLH_DSA_SHA2_128S, "SLH-DSA-SHA2-128s", false}, - {EVP_PKEY_SLH_DSA_SHA2_192F, "SLH-DSA-SHA2-192f", false}, - {EVP_PKEY_SLH_DSA_SHA2_192S, "SLH-DSA-SHA2-192s", false}, - {EVP_PKEY_SLH_DSA_SHA2_256F, "SLH-DSA-SHA2-256f", false}, - {EVP_PKEY_SLH_DSA_SHA2_256S, "SLH-DSA-SHA2-256s", false}, - {EVP_PKEY_SLH_DSA_SHAKE_128F, "SLH-DSA-SHAKE-128f", false}, - {EVP_PKEY_SLH_DSA_SHAKE_128S, "SLH-DSA-SHAKE-128s", false}, - {EVP_PKEY_SLH_DSA_SHAKE_192F, "SLH-DSA-SHAKE-192f", false}, - {EVP_PKEY_SLH_DSA_SHAKE_192S, "SLH-DSA-SHAKE-192s", false}, - {EVP_PKEY_SLH_DSA_SHAKE_256F, "SLH-DSA-SHAKE-256f", false}, - {EVP_PKEY_SLH_DSA_SHAKE_256S, "SLH-DSA-SHAKE-256s", false}, + {EVP_PKEY_ML_DSA_44, + "ML-DSA-44", + &Environment::crypto_ml_dsa_44_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_DSA_65, + "ML-DSA-65", + &Environment::crypto_ml_dsa_65_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_DSA_87, + "ML-DSA-87", + &Environment::crypto_ml_dsa_87_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_KEM_768, + "ML-KEM-768", + &Environment::crypto_ml_kem_768_string, + kPqcMlKemFlags}, + {EVP_PKEY_ML_KEM_1024, + "ML-KEM-1024", + &Environment::crypto_ml_kem_1024_string, + kPqcMlKemFlags}, + +#if OPENSSL_WITH_PQC_ML_KEM_512 + {EVP_PKEY_ML_KEM_512, + "ML-KEM-512", + &Environment::crypto_ml_kem_512_string, + kPqcMlKemFlags}, +#endif +#if OPENSSL_WITH_PQC_SLH_DSA + {EVP_PKEY_SLH_DSA_SHA2_128F, + "SLH-DSA-SHA2-128f", + &Environment::crypto_slh_dsa_sha2_128f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_128S, + "SLH-DSA-SHA2-128s", + &Environment::crypto_slh_dsa_sha2_128s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_192F, + "SLH-DSA-SHA2-192f", + &Environment::crypto_slh_dsa_sha2_192f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_192S, + "SLH-DSA-SHA2-192s", + &Environment::crypto_slh_dsa_sha2_192s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_256F, + "SLH-DSA-SHA2-256f", + &Environment::crypto_slh_dsa_sha2_256f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_256S, + "SLH-DSA-SHA2-256s", + &Environment::crypto_slh_dsa_sha2_256s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_128F, + "SLH-DSA-SHAKE-128f", + &Environment::crypto_slh_dsa_shake_128f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_128S, + "SLH-DSA-SHAKE-128s", + &Environment::crypto_slh_dsa_shake_128s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_192F, + "SLH-DSA-SHAKE-192f", + &Environment::crypto_slh_dsa_shake_192f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_192S, + "SLH-DSA-SHAKE-192s", + &Environment::crypto_slh_dsa_shake_192s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_256F, + "SLH-DSA-SHAKE-256f", + &Environment::crypto_slh_dsa_shake_256f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_256S, + "SLH-DSA-SHAKE-256s", + &Environment::crypto_slh_dsa_shake_256s_string, + kPqcSlhDsaFlags}, +#endif }; const PqcAlgorithm* FindPqcAlgorithmById(int id) { @@ -59,6 +132,10 @@ const PqcAlgorithm* FindPqcAlgorithmByName(const char* name) { return nullptr; } +bool HasPqcAlgorithmFlag(const PqcAlgorithm* alg, PqcAlgorithmFlag flag) { + return alg != nullptr && (alg->flags & flag) != 0; +} + bool TrySetEncodedKey(Environment* env, DataPointer data, Local target, @@ -82,9 +159,9 @@ bool ExportJwkPqcKey(Environment* env, CHECK(alg); if (key.GetKeyType() == kKeyTypePrivate) { - DataPointer priv_data = - alg->use_seed ? pkey.rawSeed() : pkey.rawPrivateKey(); - if (alg->use_seed && !priv_data) { + const bool uses_seed = HasPqcAlgorithmFlag(alg, kPqcRawSeed); + DataPointer priv_data = uses_seed ? pkey.rawSeed() : pkey.rawPrivateKey(); + if (uses_seed && !priv_data) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "key does not have an available seed"); return false; @@ -144,8 +221,9 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { .data = priv.data(), .len = priv.size(), }; - pkey = alg->use_seed ? EVPKeyPointer::NewRawSeed(alg->id, buf) - : EVPKeyPointer::NewRawPrivate(alg->id, buf); + pkey = HasPqcAlgorithmFlag(alg, kPqcRawSeed) + ? EVPKeyPointer::NewRawSeed(alg->id, buf) + : EVPKeyPointer::NewRawPrivate(alg->id, buf); } else { ByteSource pub = ByteSource::FromEncodedString(env, pub_value.As()); pkey = @@ -176,14 +254,38 @@ KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { return KeyObjectData::CreateAsymmetric(type, std::move(pkey)); } +bool IsPqcKeyId(int id) { + return FindPqcAlgorithmById(id) != nullptr; +} + bool IsPqcRawPrivateKeyId(int id) { const PqcAlgorithm* alg = FindPqcAlgorithmById(id); - return alg != nullptr && !alg->use_seed; + return HasPqcAlgorithmFlag(alg, kPqcRawPrivate); } bool IsPqcSeedKeyId(int id) { const PqcAlgorithm* alg = FindPqcAlgorithmById(id); - return alg != nullptr && alg->use_seed; + return HasPqcAlgorithmFlag(alg, kPqcRawSeed); +} + +bool IsPqcSignatureKeyId(int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + return HasPqcAlgorithmFlag(alg, kPqcSignature); +} + +int GetPqcNidFromName(const char* name) { + for (const auto& alg : kPqcAlgorithms) { + if (StringEqualNoCase(name, alg.name)) return alg.id; + } + return NID_undef; +} + +Local GetPqcAsymmetricKeyType(Environment* env, int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + if (alg == nullptr) return v8::Undefined(env->isolate()); + + Local key_type = (env->*(alg->key_type))(); + return key_type.As(); } #endif } // namespace crypto diff --git a/src/crypto/crypto_pqc.h b/src/crypto/crypto_pqc.h index 156066097bbfb9..14f919d94c6f8a 100644 --- a/src/crypto/crypto_pqc.h +++ b/src/crypto/crypto_pqc.h @@ -18,10 +18,19 @@ KeyObjectData ImportJWKPqcKey(Environment* env, v8::Local jwk); // Returns true for PQC algorithms that support raw private key export/import. bool IsPqcRawPrivateKeyId(int id); +// Returns true if the given EVP_PKEY id is a PQC algorithm known to Node. +bool IsPqcKeyId(int id); // Returns true for PQC algorithms that carry the private key as a seed // (ML-DSA, ML-KEM). Returns false for algorithms that use the expanded // private key (SLH-DSA), or for non-PQC ids. bool IsPqcSeedKeyId(int id); +// Returns true for PQC signature algorithms (ML-DSA, SLH-DSA). Returns false +// for ML-KEM or for non-PQC ids. +bool IsPqcSignatureKeyId(int id); +// Returns the EVP_PKEY id for the given PQC algorithm name, or NID_undef. +int GetPqcNidFromName(const char* name); +// Returns the JS asymmetricKeyType string for a PQC id, or undefined. +v8::Local GetPqcAsymmetricKeyType(Environment* env, int id); #endif } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index bd3c9f538c5de5..d8a4fe395a5f47 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -3,6 +3,7 @@ #include "base_object-inl.h" #include "crypto/crypto_ec.h" #include "crypto/crypto_keys.h" +#include "crypto/crypto_pqc.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -237,34 +238,16 @@ bool UseP1363Encoding(const EVPKeyPointer& key, const DSASigEnc dsa_encoding) { } bool SupportsContextString(const EVPKeyPointer& key) { -#if OPENSSL_VERSION_NUMBER < 0x3020000fL - return false; -#else - switch (key.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: + if (!OPENSSL_WITH_SIGNATURE_CONTEXT_STRING) return false; + + const int id = key.id(); #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - 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: + if (IsPqcSignatureKeyId(id)) return true; #endif - return true; - default: - return false; - } +#ifndef OPENSSL_IS_BORINGSSL + if (id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448) return true; #endif + return false; } } // namespace diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 53d6142917dc58..b9d037fb72352b 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -139,7 +139,7 @@ void InitCryptoOnce() { OPENSSL_init_ssl(0, settings); -#if OPENSSL_WITH_PQC +#if OPENSSL_WITH_OPENSSL_PQC // Configure all loaded providers to prefer seed-only format for ML-KEM and // ML-DSA private keys in PKCS#8 export, falling back to priv-only when a // seed is not available. The provider encoder reads these parameters at diff --git a/src/node_crypto.cc b/src/node_crypto.cc index c0869f40e0410d..91d80e0dd379ba 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -61,21 +61,24 @@ namespace crypto { V(Verify) \ V(X509Certificate) -#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 #define ARGON2_NAMESPACE_LIST(V) V(Argon2) #else #define ARGON2_NAMESPACE_LIST(V) -#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2 +#endif // OPENSSL_WITH_ARGON2 -// KEM and KMAC functionality requires OpenSSL 3.0.0 or later -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM #define KEM_NAMESPACE_LIST(V) V(KEM) -#define KMAC_NAMESPACE_LIST(V) V(Kmac) #else #define KEM_NAMESPACE_LIST(V) -#define KMAC_NAMESPACE_LIST(V) #endif +#if OPENSSL_WITH_KMAC +#define KMAC_NAMESPACE_LIST(V) V(Kmac) +#else +#define KMAC_NAMESPACE_LIST(V) +#endif // OPENSSL_WITH_KMAC + #define TURBOSHAKE_NAMESPACE_LIST(V) V(TurboShake) #ifdef OPENSSL_NO_SCRYPT diff --git a/src/node_crypto.h b/src/node_crypto.h index 80657431a791db..ecc2b8c6a358c8 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -40,8 +40,10 @@ #include "crypto/crypto_hash.h" #include "crypto/crypto_hkdf.h" #include "crypto/crypto_hmac.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM #include "crypto/crypto_kem.h" +#endif +#if OPENSSL_WITH_KMAC #include "crypto/crypto_kmac.h" #endif #include "crypto/crypto_keygen.h" diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index def378b70fef92..c1e2fde9c3874b 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -102,6 +102,8 @@ all: \ ml_dsa_44_private.pem \ ml_dsa_44_private_seed_only.pem \ ml_dsa_44_private_priv_only.pem \ + ml_dsa_44_private_encrypted.pem \ + ml_dsa_44_private_encrypted.der \ ml_dsa_44_public.pem \ ml_dsa_65_private.pem \ ml_dsa_65_private_seed_only.pem \ @@ -124,6 +126,8 @@ all: \ ml_kem_768_private.pem \ ml_kem_768_private_seed_only.pem \ ml_kem_768_private_priv_only.pem \ + ml_kem_768_private_encrypted.pem \ + ml_kem_768_private_encrypted.der \ ml_kem_768_public.pem \ ml_kem_1024_private.pem \ ml_kem_1024_private_seed_only.pem \ @@ -1028,6 +1032,12 @@ ml_dsa_44_private_priv_only.pem: ml_dsa_44_private.pem ml_dsa_44_public.pem: ml_dsa_44_private.pem openssl pkey -in ml_dsa_44_private.pem -pubout -out ml_dsa_44_public.pem +ml_dsa_44_private_encrypted.pem: ml_dsa_44_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-dsa.output_formats=seed-only -in ml_dsa_44_private_seed_only.pem -passout 'pass:password' -out ml_dsa_44_private_encrypted.pem + +ml_dsa_44_private_encrypted.der: ml_dsa_44_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-dsa.output_formats=seed-only -in ml_dsa_44_private_seed_only.pem -passout 'pass:password' -outform DER -out ml_dsa_44_private_encrypted.der + ml_dsa_65_private.pem: openssl genpkey -algorithm ml-dsa-65 -out ml_dsa_65_private.pem @@ -1076,6 +1086,12 @@ ml_kem_768_private_priv_only.pem: ml_kem_768_private.pem ml_kem_768_public.pem: ml_kem_768_private.pem openssl pkey -in ml_kem_768_private.pem -pubout -out ml_kem_768_public.pem +ml_kem_768_private_encrypted.pem: ml_kem_768_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-kem.output_formats=seed-only -in ml_kem_768_private_seed_only.pem -passout 'pass:password' -out ml_kem_768_private_encrypted.pem + +ml_kem_768_private_encrypted.der: ml_kem_768_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-kem.output_formats=seed-only -in ml_kem_768_private_seed_only.pem -passout 'pass:password' -outform DER -out ml_kem_768_private_encrypted.der + ml_kem_1024_private.pem: openssl genpkey -algorithm ml-kem-1024 -out ml_kem_1024_private.pem diff --git a/test/fixtures/keys/ml_dsa_44_private_encrypted.der b/test/fixtures/keys/ml_dsa_44_private_encrypted.der new file mode 100644 index 00000000000000..2ae136bc7961e5 Binary files /dev/null and b/test/fixtures/keys/ml_dsa_44_private_encrypted.der differ diff --git a/test/fixtures/keys/ml_dsa_44_private_encrypted.pem b/test/fixtures/keys/ml_dsa_44_private_encrypted.pem new file mode 100644 index 00000000000000..e127aa0085bc5f --- /dev/null +++ b/test/fixtures/keys/ml_dsa_44_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBD1YJCeuwCAuw/ktX9I +K9g9AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ7kmeI0FzRNLI2m54 +BMASEgRAWBi1BRsuBBVt2kWVTbz8tQa8K3lV+nNE+iRGlMaOhnF5o5Kx4mQnzE1q +ppIFNbWPGGr+xKHTU6fNfNnMecVXKA== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/keys/ml_kem_768_private_encrypted.der b/test/fixtures/keys/ml_kem_768_private_encrypted.der new file mode 100644 index 00000000000000..4f9097751c2249 Binary files /dev/null and b/test/fixtures/keys/ml_kem_768_private_encrypted.der differ diff --git a/test/fixtures/keys/ml_kem_768_private_encrypted.pem b/test/fixtures/keys/ml_kem_768_private_encrypted.pem new file mode 100644 index 00000000000000..0e3d54e75a3259 --- /dev/null +++ b/test/fixtures/keys/ml_kem_768_private_encrypted.pem @@ -0,0 +1,7 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHDMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBBSwnAxR1nLC5FZtJyu +lumDAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQyBgMhkKPAMK6jaIc +YxhYcgRg3P97VHfT14YDN024txZznhhzC0mWGNpP6f1EV/mP/YttQp2JTXMKID4V +um3QuQes5my0oOIuiRl3gYIz/BDjKkqLagYBQmUcUUlURgaYJ67Yk3BZg6ULjXmq +EdLYqK5D +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 931be98a824032..1850acacdc22e9 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -74,7 +74,7 @@ export const vectors = { [false, { name: 'AES-CBC', length: 25 }], [true, { name: 'AES-GCM', length: 128 }], [false, { name: 'AES-GCM', length: 25 }], - [!boringSSL, { name: 'AES-KW', length: 128 }], + [true, { name: 'AES-KW', length: 128 }], [false, { name: 'AES-KW', length: 25 }], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], @@ -192,7 +192,7 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [!boringSSL, 'AES-KW'], + [true, 'AES-KW'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], @@ -214,18 +214,18 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [!boringSSL, 'AES-KW'], + [true, 'AES-KW'], [true, 'Ed25519'], [true, 'X25519'], ], 'wrapKey': [ [false, 'AES-KW'], - [!boringSSL, 'AES-KW', 'AES-CTR'], - [!boringSSL, 'AES-KW', 'HMAC'], + [true, 'AES-KW', 'AES-CTR'], + [true, 'AES-KW', 'HMAC'], ], 'unwrapKey': [ [false, 'AES-KW'], - [!boringSSL, 'AES-KW', 'AES-CTR'], + [true, 'AES-KW', 'AES-CTR'], ], 'unsupported operation': [ [false, ''], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 0572107e9f492e..2d370b8e21d3d5 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -2,14 +2,13 @@ import * as crypto from 'node:crypto' import { hasOpenSSL } from '../../common/crypto.js' -const pqc = hasOpenSSL(3, 5); +const boringSSL = process.features.openssl_is_boringssl; +const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); -const chacha = crypto.getCiphers().includes('chacha20-poly1305'); const ocb = hasOpenSSL(3); const kmac = hasOpenSSL(3); -const boringSSL = process.features.openssl_is_boringssl; const { subtle } = globalThis.crypto; const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); @@ -75,10 +74,10 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', length: 128 }], [false, 'Argon2d'], [false, 'Argon2i'], @@ -96,10 +95,10 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', length: 128 }], [argon2, 'Argon2d'], [argon2, 'Argon2i'], @@ -117,10 +116,10 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, 'AES-OCB'], [false, 'Argon2d'], [false, 'Argon2i'], @@ -141,7 +140,7 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [false, 'AES-CTR'], @@ -186,9 +185,9 @@ export const vectors = { [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 16777215, memory: 8, passes: 1 }, 32], ], 'encrypt': [ - [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }], - [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }], @@ -200,37 +199,39 @@ export const vectors = { [false, 'AES-OCB'], ], 'encapsulateBits': [ - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], ], 'encapsulateKey': [ - [pqc, 'ML-KEM-512', 'AES-KW'], - [pqc, 'ML-KEM-512', 'AES-GCM'], - [pqc, 'ML-KEM-512', 'AES-CTR'], - [pqc, 'ML-KEM-512', 'AES-CBC'], - [pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'], - [pqc, 'ML-KEM-512', 'HKDF'], - [pqc, 'ML-KEM-512', 'PBKDF2'], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-GCM'], + [pqc, 'ML-KEM-768', 'AES-CTR'], + [pqc, 'ML-KEM-768', 'AES-CBC'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'HKDF'], + [pqc, 'ML-KEM-768', 'PBKDF2'], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], ], 'decapsulateBits': [ - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], ], 'decapsulateKey': [ - [pqc, 'ML-KEM-512', 'AES-KW'], - [pqc, 'ML-KEM-512', 'AES-GCM'], - [pqc, 'ML-KEM-512', 'AES-CTR'], - [pqc, 'ML-KEM-512', 'AES-CBC'], - [pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'], - [pqc, 'ML-KEM-512', 'HKDF'], - [pqc, 'ML-KEM-512', 'PBKDF2'], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-GCM'], + [pqc, 'ML-KEM-768', 'AES-CTR'], + [pqc, 'ML-KEM-768', 'AES-CBC'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'HKDF'], + [pqc, 'ML-KEM-768', 'PBKDF2'], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], ], }; diff --git a/test/parallel/test-crypto-boringssl-evp-list.js b/test/parallel/test-crypto-boringssl-evp-list.js new file mode 100644 index 00000000000000..3f142c24f28a7c --- /dev/null +++ b/test/parallel/test-crypto-boringssl-evp-list.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!process.features.openssl_is_boringssl) + common.skip('BoringSSL-only test'); + +const assert = require('assert'); +const { getCiphers, getHashes } = require('crypto'); + +const ciphers = getCiphers(); +[ + 'aes-128-cbc', + 'aes-256-gcm', + 'des-ede', + 'des-ede-cbc', + 'des-ede3-cbc', + 'rc2-cbc', + 'rc4', +].forEach((cipher) => assert(ciphers.includes(cipher), cipher)); + +const hashes = getHashes(); +[ + 'md4', + 'md5', + 'sha1', + 'sha256', + 'sha512-256', +].forEach((hash) => assert(hashes.includes(hash), hash)); diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js index 38e24a7341713a..f2259194a9e15d 100644 --- a/test/parallel/test-crypto-encap-decap.js +++ b/test/parallel/test-crypto-encap-decap.js @@ -9,7 +9,9 @@ const fixtures = require('../common/fixtures'); const { hasOpenSSL } = require('../common/crypto'); const { promisify } = require('util'); -if (!hasOpenSSL(3)) { +const isBoringSSL = process.features.openssl_is_boringssl; + +if (!hasOpenSSL(3) && !isBoringSSL) { assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' }); return; } @@ -79,25 +81,25 @@ const keys = { raw: true, }, 'ml-kem-512': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5), // BoringSSL does not support ML-KEM-512 publicKey: fixtures.readKey('ml_kem_512_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_512_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 768, raw: true, }, 'ml-kem-768': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5) || isBoringSSL, publicKey: fixtures.readKey('ml_kem_768_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_768_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1088, raw: true, }, 'ml-kem-1024': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5) || isBoringSSL, publicKey: fixtures.readKey('ml_kem_1024_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_1024_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1568, raw: true, @@ -109,7 +111,7 @@ for (const [name, { }] of Object.entries(keys)) { if (!supported) { assert.throws(() => crypto.encapsulate(publicKey), - { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ }); + { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_CRYPTO_OPERATION_FAILED/ }); continue; } @@ -211,7 +213,7 @@ for (const [name, { } else if (name.startsWith('p-')) { wrongPrivateKey = name === 'p-256' ? keys['p-384'].privateKey : keys['p-256'].privateKey; } else if (name.startsWith('ml-')) { - wrongPrivateKey = name === 'ml-kem-512' ? keys['ml-kem-768'].privateKey : keys['ml-kem-512'].privateKey; + wrongPrivateKey = name === 'ml-kem-768' ? keys['ml-kem-1024'].privateKey : keys['ml-kem-768'].privateKey; } else { wrongPrivateKey = keys.x25519.privateKey; } diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index 311659ef004ea2..024d5f6f199ffc 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -170,15 +170,22 @@ if (hasOpenSSL(3, 5)) { // PQC import throws when PQC is not supported if (!hasOpenSSL(3, 5)) { - for (const asymmetricKeyType of [ - 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', - 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', - 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', - ]) { + const unsupported = process.features.openssl_is_boringssl ? + // BoringSSL supports ML-DSA and ML-KEM-{768,1024}, but not ML-KEM-512 or SLH-DSA. + ['ml-kem-512', 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f'] : + [ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', + 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', + ]; + for (const asymmetricKeyType of unsupported) { for (const format of ['raw-public', 'raw-private', 'raw-seed']) { assert.throws(() => crypto.createPublicKey({ key: Buffer.alloc(32), format, asymmetricKeyType, - }), { code: 'ERR_INVALID_ARG_VALUE' }); + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /Invalid asymmetricKeyType|Unsupported key type/ + }); } } } @@ -224,27 +231,27 @@ if (!hasOpenSSL(3, 5)) { }), { code: 'ERR_INVALID_ARG_VALUE' }); } -// ML-KEM: -768 and -512 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5)) { - const mlKem512Pub = crypto.createPublicKey( - fixtures.readKey('ml_kem_512_public.pem', 'ascii')); +// ML-KEM: public keys of different type cannot be imported as the other type +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlKem768Pub = crypto.createPublicKey( fixtures.readKey('ml_kem_768_public.pem', 'ascii')); + const mlKem1024Pub = crypto.createPublicKey( + fixtures.readKey('ml_kem_1024_public.pem', 'ascii')); - const mlKem512RawPub = mlKem512Pub.export({ format: 'raw-public' }); const mlKem768RawPub = mlKem768Pub.export({ format: 'raw-public' }); + const mlKem1024RawPub = mlKem1024Pub.export({ format: 'raw-public' }); assert.throws(() => crypto.createPublicKey({ - key: mlKem512RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', + key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-1024', }), { code: 'ERR_INVALID_ARG_VALUE' }); assert.throws(() => crypto.createPublicKey({ - key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-512', + key: mlKem1024RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', }), { code: 'ERR_INVALID_ARG_VALUE' }); } // ML-DSA: -44 and -65 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlDsa44Pub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsa65Pub = crypto.createPublicKey( @@ -357,10 +364,10 @@ if (hasOpenSSL(3, 5)) { } // raw-private cannot be used for ml-kem and ml-dsa -if (hasOpenSSL(3, 5)) { - for (const type of ['ml-kem-512', 'ml-dsa-44']) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + for (const type of ['ml-kem-768', 'ml-dsa-44']) { const priv = crypto.createPrivateKey( - fixtures.readKey(`${type.replaceAll('-', '_')}_private.pem`, 'ascii')); + fixtures.readKey(`${type.replaceAll('-', '_')}_private_seed_only.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-private' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); assert.throws(() => crypto.createPrivateKey({ @@ -465,9 +472,9 @@ if (hasOpenSSL(3, 5)) { { code: 'ERR_INVALID_ARG_VALUE' }); // PQC raw-seed -> createPublicKey - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlDsaPriv = crypto.createPrivateKey( - fixtures.readKey('ml_dsa_44_private.pem', 'ascii')); + fixtures.readKey('ml_dsa_44_private_seed_only.pem', 'ascii')); const mlDsaPub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsaRawSeed = mlDsaPriv.export({ format: 'raw-seed' }); diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 54449329cb551a..5c3148647324b0 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -26,15 +26,10 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { { for (const length of [128, 192, 256]) { const key = createSecretKey(randomBytes(length >> 3)); - let algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; + const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; if (length === 256) algorithms.push('ChaCha20-Poly1305'); - if (process.features.openssl_is_boringssl) { - algorithms = algorithms.filter((a) => a !== 'AES-KW' && a !== 'ChaCha20-Poly1305'); - common.printSkipMessage('Skipping unsupported AES-KW/ChaCha20-Poly1305 test cases'); - } - for (const algorithm of algorithms) { const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt']; for (const extractable of [true, false]) { @@ -200,7 +195,7 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { } } -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { const { publicKey, privateKey } = generateKeyPairSync(name.toLowerCase()); assert.throws(() => { diff --git a/test/parallel/test-crypto-keygen-raw.js b/test/parallel/test-crypto-keygen-raw.js index 5b7abe3f72d9dd..e55c3f10eed8e3 100644 --- a/test/parallel/test-crypto-keygen-raw.js +++ b/test/parallel/test-crypto-keygen-raw.js @@ -205,7 +205,7 @@ if (!process.features.openssl_is_boringssl) { } // PQC key types -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test raw encoding for ML-DSA key types (raw-public + raw-seed only). { for (const type of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { @@ -232,6 +232,10 @@ if (hasOpenSSL(3, 5)) { // Test raw encoding for ML-KEM key types (raw-public + raw-seed only). { for (const type of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { + if (process.features.openssl_is_boringssl && type === 'ml-kem-512') { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, @@ -246,7 +250,7 @@ if (hasOpenSSL(3, 5)) { // Test error: raw-private with ML-KEM (not supported). { - assert.throws(() => generateKeyPairSync('ml-kem-512', { + assert.throws(() => generateKeyPairSync('ml-kem-768', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); @@ -255,6 +259,10 @@ if (hasOpenSSL(3, 5)) { // Test raw encoding for SLH-DSA key types. { for (const type of ['slh-dsa-sha2-128f', 'slh-dsa-shake-128f']) { + if (process.features.openssl_is_boringssl) { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, @@ -266,11 +274,13 @@ if (hasOpenSSL(3, 5)) { } // Test error: raw-seed with SLH-DSA (not supported). - { + if (!process.features.openssl_is_boringssl) { assert.throws(() => generateKeyPairSync('slh-dsa-sha2-128f', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } else { + common.printSkipMessage('Skipping unsupported slh-dsa test case'); } // Test async generateKeyPair with raw encoding for PQC types. diff --git a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js new file mode 100644 index 00000000000000..b4a1b586d21d10 --- /dev/null +++ b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js @@ -0,0 +1,134 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); + +const assert = require('assert'); +const { + createPrivateKey, + generateKeyPairSync, + getCiphers, +} = require('crypto'); + +const algorithms = new Set([ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', +]); +// BoringSSL does not support ML-KEM-512. +if (process.features.openssl_is_boringssl) { + algorithms.delete('ml-kem-512'); +} + +// Exercise each CBC cipher that PBES2 may use. This covers multiple +// EVP_CIPHER_key_length values (16 / 24 / 32) and, for variable-key +// ciphers like RC2, the optional PBKDF2 keyLength INTEGER branch in +// the EncryptedPrivateKeyInfo parser. +const availableCiphers = new Set(getCiphers()); +const ciphers = [ + 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', + 'des-ede3-cbc', 'rc2-cbc', +].filter((c) => availableCiphers.has(c)); + +const passphrase = 'top secret'; +const wrongPassphraseError = + /bad decrypt|DECRYPTION_FAILED|BAD_DECRYPT|bad password|DECODE[ _]ERROR/i; +// A wrong passphrase usually fails during cipher finalization, but CBC output +// can have valid padding by chance. OpenSSL then parses the bad plaintext as +// PKCS#8 and may report ASN.1 or decoder errors from the same failed import. +function assertWrongPassphrase(fn) { + assert.throws(fn, (err) => wrongPassphraseError.test(err.message) || + err.code?.startsWith('ERR_OSSL_ASN1_') || + err.code === 'ERR_OSSL_UNSUPPORTED'); +} + +for (const asymmetricKeyType of algorithms) { + const { privateKey } = generateKeyPairSync(asymmetricKeyType); + assert.strictEqual(privateKey.asymmetricKeyType, asymmetricKeyType); + + const plainDer = privateKey.export({ type: 'pkcs8', format: 'der' }); + + for (const cipher of ciphers) { + for (const format of ['pem', 'der']) { + const encrypted = privateKey.export({ + type: 'pkcs8', + format, + cipher, + passphrase, + }); + + const imported = createPrivateKey({ + key: encrypted, + format, + type: 'pkcs8', + passphrase, + }); + assert.strictEqual(imported.type, 'private'); + assert.strictEqual(imported.asymmetricKeyType, asymmetricKeyType); + assert.deepStrictEqual( + imported.export({ type: 'pkcs8', format: 'der' }), + plainDer, + ); + + assertWrongPassphrase(() => createPrivateKey({ + key: encrypted, + format, + type: 'pkcs8', + passphrase: 'wrong', + })); + } + } +} + +// Cross-implementation compatibility: load encrypted PKCS#8 fixtures that +// were generated by OpenSSL's `openssl pkcs8` from the seed-only PQC +// PrivateKeyInfo fixtures. The inner seed-only form is portable across +// OpenSSL (>=3.5) and BoringSSL, and the matching JWK fixture provides the +// canonical key material used to derive the expected PKCS#8 bytes. +const fixtures = require('../common/fixtures'); +const fixtureCases = [ + { alg: 'ml-dsa-44', jwkFile: 'ml-dsa-44.json', + encBase: 'ml_dsa_44_private_encrypted' }, + { alg: 'ml-kem-768', jwkFile: 'ml-kem-768.json', + encBase: 'ml_kem_768_private_encrypted' }, +]; + +for (const { alg, jwkFile, encBase } of fixtureCases) { + const jwkKey = createPrivateKey({ + key: JSON.parse(fixtures.readKey(jwkFile, 'utf8')), + format: 'jwk', + }); + assert.strictEqual(jwkKey.asymmetricKeyType, alg); + const expectedDer = jwkKey.export({ type: 'pkcs8', format: 'der' }); + + for (const format of ['pem', 'der']) { + const encryptedFixture = fixtures.readKey( + `${encBase}.${format}`, + format === 'pem' ? 'utf8' : null, + ); + + const imported = createPrivateKey({ + key: encryptedFixture, + format, + type: 'pkcs8', + passphrase: 'password', + }); + assert.strictEqual(imported.asymmetricKeyType, alg); + assert.deepStrictEqual( + imported.export({ type: 'pkcs8', format: 'der' }), + expectedDer, + ); + + assertWrongPassphrase(() => createPrivateKey({ + key: encryptedFixture, + format, + type: 'pkcs8', + passphrase: 'wrong', + })); + } +} 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 f18c555d4653d4..f2a19799c51541 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js @@ -4,10 +4,6 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) { - common.skip('Skipping unsupported ML-DSA key tests'); -} - const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); @@ -104,7 +100,7 @@ for (const [asymmetricKeyType, pubLen] of [ } } - if (!hasOpenSSL(3, 5)) { + if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -119,11 +115,15 @@ for (const [asymmetricKeyType, pubLen] of [ assertPublicKey(publicKey); { - for (const [pem, hasSeed] of [ - [keys.private, true], - [keys.private_seed_only, true], - [keys.private_priv_only, false], + for (const [pem, hasSeed, seedOnly] of [ + [keys.private, true, false], + [keys.private_seed_only, true, true], + [keys.private_priv_only, false, false], ]) { + if (process.features.openssl_is_boringssl && !seedOnly) { + common.printSkipMessage('Skipping unsupported private key format test'); + continue; + } const pubFromPriv = createPublicKey(pem); assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(pem), hasSeed); diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js index 0c344ed100c2da..81353b5115dd36 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js @@ -100,7 +100,7 @@ for (const [asymmetricKeyType, pubLen] of [ } } - if (!hasOpenSSL(3, 5)) { + if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -110,16 +110,28 @@ for (const [asymmetricKeyType, pubLen] of [ code: hasOpenSSL(3) ? 'ERR_OSSL_UNSUPPORTED' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); } + } else if (process.features.openssl_is_boringssl && asymmetricKeyType === 'ml-kem-512') { + // BoringSSL does not support ML-KEM-512. + assert.throws(() => createPublicKey(keys.public), + { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); + for (const pem of [keys.private, keys.private_seed_only, keys.private_priv_only]) { + assert.throws(() => createPrivateKey(pem), + { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); + } } else { const publicKey = createPublicKey(keys.public); assertPublicKey(publicKey); { - for (const [pem, hasSeed] of [ - [keys.private, true], - [keys.private_seed_only, true], - [keys.private_priv_only, false], - ]) { + const entries = process.features.openssl_is_boringssl ? + // BoringSSL only supports the seed-only PKCS#8 private key encoding. + [[keys.private_seed_only, true]] : + [ + [keys.private, true], + [keys.private_seed_only, true], + [keys.private_priv_only, false], + ]; + for (const [pem, hasSeed] of entries) { const pubFromPriv = createPublicKey(pem); assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(pem), hasSeed); diff --git a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js index abad2c15cf01d1..e6534c988c4e2b 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js @@ -11,7 +11,7 @@ const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5)) { +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { for (const asymmetricKeyType of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', diff --git a/test/parallel/test-crypto-pqc-keygen-ml-kem.js b/test/parallel/test-crypto-pqc-keygen-ml-kem.js index ea3ac9f4dde137..620f65c3a8d156 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-kem.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-kem.js @@ -11,7 +11,12 @@ const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5)) { +const algorithms = process.features.openssl_is_boringssl ? + // BoringSSL does not support ML-KEM-512. + ['ml-kem-768', 'ml-kem-1024'] : + ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']; + +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', @@ -19,8 +24,7 @@ if (!hasOpenSSL(3, 5)) { }); } } else { - for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { - + for (const asymmetricKeyType of algorithms) { function assertJwk(jwk) { assert.strictEqual(jwk.kty, 'AKP'); assert.strictEqual(jwk.alg, asymmetricKeyType.toUpperCase()); @@ -67,3 +71,10 @@ if (!hasOpenSSL(3, 5)) { } } } + +if (process.features.openssl_is_boringssl) { + assert.throws(() => generateKeyPair('ml-kem-512', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The argument 'type' must be a supported key type/ + }); +} 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 57d6692ca79b55..535e6a33d5ccb0 100644 --- a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js @@ -6,8 +6,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { @@ -34,7 +34,15 @@ for (const [asymmetricKeyType, sigLen] of [ private_priv_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_priv_only'), 'ascii'), }; - for (const privateKey of [keys.private, keys.private_seed_only, keys.private_priv_only]) { + for (const [privateKey, seedOnly] of [ + [keys.private, false], + [keys.private_seed_only, true], + [keys.private_priv_only, false], + ]) { + if (process.features.openssl_is_boringssl && !seedOnly) { + common.printSkipMessage('Skipping unsupported private key format test'); + continue; + } for (const data of [randomBytes(0), randomBytes(1), randomBytes(32), randomBytes(128), randomBytes(1024)]) { // sync { @@ -44,10 +52,12 @@ for (const [asymmetricKeyType, sigLen] of [ assert.strictEqual(verify(undefined, data, keys.public, Buffer.alloc(sigLen)), false); assert.strictEqual(verify(undefined, data, keys.public, signature), true); assert.strictEqual(verify(undefined, data, privateKey, signature), true); - assert.throws(() => sign('sha256', data, privateKey), { code: 'ERR_OSSL_INVALID_DIGEST' }); + const code = process.features.openssl_is_boringssl ? + 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_OSSL_INVALID_DIGEST'; + assert.throws(() => sign('sha256', data, privateKey), { code }); assert.throws( () => verify('sha256', data, keys.public, Buffer.alloc(sigLen)), - { code: 'ERR_OSSL_INVALID_DIGEST' }); + { code }); } // async @@ -62,8 +72,9 @@ for (const [asymmetricKeyType, sigLen] of [ })); })); - sign('sha256', data, privateKey, common.expectsError(/invalid digest/)); - verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(/invalid digest/)); + const message = process.features.openssl_is_boringssl ? /COMMAND_NOT_SUPPORTED/ : /invalid digest/; + sign('sha256', data, privateKey, common.expectsError(message)); + verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(message)); } } } diff --git a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index a96e709095430f..316d706e7b7948 100644 --- a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js +++ b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js @@ -29,14 +29,11 @@ async function test(algorithmName, keyLength, ivLength, format = 'raw') { const tests = [ test('AES-GCM', 32, 12), + test('ChaCha20-Poly1305', 32, 12, 'raw-secret'), ]; if (hasOpenSSL(3)) { tests.push(test('AES-OCB', 32, 12, 'raw-secret')); } -if (!process.features.openssl_is_boringssl) { - tests.push(test('ChaCha20-Poly1305', 32, 12, 'raw-secret')); -} - Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index e30dbe7887166e..70b35f6cfa3849 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -42,17 +42,13 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'AES-GCM', length: 128 }, usages: ['decrypt', 'encrypt', 'decrypt'], expected: ['encrypt', 'decrypt'] }, - ]; - - if (!process.features.openssl_is_boringssl) { - symmetric.push({ - algorithm: { name: 'AES-KW', length: 128 }, + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + { algorithm: { name: 'ChaCha20-Poly1305' }, + usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], + expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { symmetric.push({ @@ -69,16 +65,6 @@ function assertSameSet(actual, expected, msg) { common.printSkipMessage('AES-OCB and KMAC require OpenSSL >= 3'); } - if (!process.features.openssl_is_boringssl) { - symmetric.push({ - algorithm: { name: 'ChaCha20-Poly1305' }, - usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], - expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL'); - } - for (const { algorithm, usages, expected } of symmetric) { tests.push((async () => { const key = await subtle.generateKey(algorithm, true, usages); @@ -121,7 +107,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { asymmetric.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -136,7 +122,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['decapsulateKey', 'decapsulateBits'], }); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } for (const { algorithm, usages, publicExpected, privateExpected } of asymmetric) { @@ -172,17 +158,10 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'HMAC', hash: 'SHA-256' }, keyData: new Uint8Array(32), usages: ['verify', 'sign', 'verify', 'sign'], expected: ['sign', 'verify'] }, - ]; - - if (!process.features.openssl_is_boringssl) { - rawSymmetric.push({ - algorithm: { name: 'AES-KW' }, keyData: new Uint8Array(16), + { algorithm: { name: 'AES-KW' }, keyData: new Uint8Array(16), usages: ['wrapKey', 'unwrapKey', 'wrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { // KMAC does not support `raw` format, only `raw-secret` and `jwk`. @@ -310,7 +289,7 @@ function assertSameSet(actual, expected, msg) { assert.deepStrictEqual(imported.usages, ['sign']); })()); - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // ML-DSA JWK roundtrip. tests.push((async () => { const { privateKey } = await subtle.generateKey( @@ -336,7 +315,7 @@ function assertSameSet(actual, expected, msg) { ['decapsulateKey', 'decapsulateBits']); })()); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } // Spki import of RSA public key. @@ -356,20 +335,16 @@ function assertSameSet(actual, expected, msg) { })()); // ChaCha20-Poly1305 raw-secret import. - if (!process.features.openssl_is_boringssl) { - tests.push((async () => { - const key = await subtle.importKey( - 'raw-secret', - new Uint8Array(32), - { name: 'ChaCha20-Poly1305' }, - true, - ['decrypt', 'encrypt', 'decrypt', 'encrypt']); - assertSameSet(key.usages, ['encrypt', 'decrypt']); - assert.strictEqual(key.usages.length, 2); - })()); - } else { - common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL'); - } + tests.push((async () => { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(32), + { name: 'ChaCha20-Poly1305' }, + true, + ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + assertSameSet(key.usages, ['encrypt', 'decrypt']); + assert.strictEqual(key.usages.length, 2); + })()); // AES-OCB raw-secret import. if (hasOpenSSL(3)) { @@ -455,17 +430,10 @@ function assertSameSet(actual, expected, msg) { { algorithm: { name: 'AES-GCM', length: 128 }, usages: ['decrypt', 'encrypt', 'decrypt'], expected: ['encrypt', 'decrypt'] }, - ]; - - if (!process.features.openssl_is_boringssl) { - jwkVectors.push({ - algorithm: { name: 'AES-KW', length: 128 }, + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { jwkVectors.push({ @@ -523,7 +491,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { jwkPairVectors.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -538,7 +506,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['decapsulateKey', 'decapsulateBits'], }); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } for (const { algorithm, usages, publicExpected, privateExpected } of jwkPairVectors) { diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 689eaeb38fd66f..d2057d1f782e7f 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -24,12 +24,12 @@ const kDerivedKeyTypes = [ ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; if (!process.features.openssl_is_boringssl) { kDerivedKeyTypes.push( - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], diff --git a/test/parallel/test-webcrypto-encap-decap-ml-kem.js b/test/parallel/test-webcrypto-encap-decap-ml-kem.js index 450ba2cefb0a4f..958a4d240db148 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const crypto = require('crypto'); @@ -253,12 +253,16 @@ async function testDecapsulateBits({ name, publicKeyPem, privateKeyPem, results (async function() { const variations = []; - vectors.forEach((vector) => { + for (const vector of vectors) { + if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + common.printSkipMessage(`Skipping unsupported ${vector.name} test`); + continue; + } variations.push(testEncapsulateKey(vector)); variations.push(testEncapsulateBits(vector)); variations.push(testDecapsulateKey(vector)); variations.push(testDecapsulateBits(vector)); - }); + } await Promise.all(variations); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 0f930a356712ed..723fd26ea5708b 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -5,9 +5,6 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) - common.skip('Skipping unsupported ChaCha20-Poly1305 test case'); - const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 63766a7b377c77..20d46870e430f1 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -96,12 +96,23 @@ async function testImportSpki({ name, publicUsages }, extractable) { } async function testImportPkcs8({ name, privateUsages }, extractable) { - const key = await subtle.importKey( - 'pkcs8', - keyData[name].pkcs8, - { name }, - extractable, - privateUsages); + let key; + try { + key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + } catch (err) { + if (process.features.openssl_is_boringssl) { + assert.strictEqual(err.name, 'DataError'); + assert.strictEqual(err.cause.code, 'ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED'); + common.printSkipMessage('Skipping unsupported private key format test'); + return; + } + throw err; + } assert.strictEqual(key.type, 'private'); assert.strictEqual(key.extractable, extractable); assert.deepStrictEqual(key.usages, privateUsages); @@ -480,14 +491,18 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { }); })().then(common.mustCall()); -(async function() { - for (const { name, privateUsages } of testVectors) { - const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); - const keyObject = createPrivateKey(pem); - const key = keyObject.toCryptoKey({ name }, true, privateUsages); - await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { - assert.strictEqual(err.name, 'OperationError'); - return true; - }); - } -})().then(common.mustCall()); +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index 332d88d93f69d1..a3b1b3fe773090 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -96,12 +96,26 @@ async function testImportSpki({ name, publicUsages }, extractable) { } async function testImportPkcs8({ name, privateUsages }, extractable) { - const key = await subtle.importKey( - 'pkcs8', - keyData[name].pkcs8, - { name }, - extractable, - privateUsages); + let key; + try { + key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + } catch (err) { + if (process.features.openssl_is_boringssl) { + assert.strictEqual(err.name, 'DataError'); + // It should really only be ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED + // but BoringSSL is inconsistent between handling ML-KEM and ML-DSA + // Fixed in https://github.com/google/boringssl/commit/94c4c7f9e0eeeff72ea1ac6abf1aed5bd2a82c0c + assert.match(err.cause.code, /ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED/); + common.printSkipMessage('Skipping unsupported private key format test'); + return; + } + throw err; + } assert.strictEqual(key.type, 'private'); assert.strictEqual(key.extractable, extractable); assert.deepStrictEqual(key.usages, privateUsages); @@ -239,7 +253,7 @@ async function testImportRawPublic({ name, publicUsages }, extractable) { subtle.importKey( 'raw-public', pub, - { name: name === 'ML-KEM-512' ? 'ML-KEM-768' : 'ML-KEM-512' }, + { name: name === 'ML-KEM-768' ? 'ML-KEM-1024' : 'ML-KEM-768' }, extractable, publicUsages), { message: 'Invalid keyData' }); } @@ -415,7 +429,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) privateUsages), // Invalid for a public key { message: /Unsupported key usage/ }); - for (const alg of [undefined, name === 'ML-KEM-512' ? 'ML-KEM-1024' : 'ML-KEM-512']) { + for (const alg of [undefined, name === 'ML-KEM-768' ? 'ML-KEM-1024' : 'ML-KEM-768']) { await assert.rejects( subtle.importKey( 'jwk', @@ -457,6 +471,10 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) (async function() { const tests = []; for (const vector of testVectors) { + if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); + continue; + } for (const extractable of [true, false]) { tests.push(testImportSpki(vector, extractable)); tests.push(testImportPkcs8(vector, extractable)); @@ -472,26 +490,14 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) })().then(common.mustCall()); (async function() { - const alg = 'ML-KEM-512'; + const alg = 'ML-KEM-768'; const pub = Buffer.from(keyData[alg].jwk.pub, 'base64url'); await assert.rejects(subtle.importKey('raw', pub, alg, false, []), { name: 'NotSupportedError', - message: 'Unable to import ML-KEM-512 using raw format', + message: 'Unable to import ML-KEM-768 using raw format', }); })().then(common.mustCall()); -(async function() { - for (const { name, privateUsages } of testVectors) { - const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); - const keyObject = createPrivateKey(pem); - const key = keyObject.toCryptoKey({ name }, true, privateUsages); - await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { - assert.strictEqual(err.name, 'OperationError'); - return true; - }); - } -})().then(common.mustCall()); - // Regression test: JWK `key_ops` validation must recognize ML-KEM operations // (encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits) so that // duplicate entries are rejected @@ -504,3 +510,19 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name: 'DataError', message: /Duplicate key operation/ }); } })().then(common.mustCall()); + +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index e57c34436578ab..d73ffd21e563a5 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -135,6 +135,23 @@ const vectors = { 'deriveBits', ], }, + 'AES-KW': { + algorithm: { length: 256 }, + result: 'CryptoKey', + usages: [ + 'wrapKey', + 'unwrapKey', + ], + }, + 'ChaCha20-Poly1305': { + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }, }; if (!process.features.openssl_is_boringssl) { @@ -152,23 +169,6 @@ if (!process.features.openssl_is_boringssl) { 'deriveBits', ], }; - vectors['AES-KW'] = { - algorithm: { length: 256 }, - result: 'CryptoKey', - usages: [ - 'wrapKey', - 'unwrapKey', - ], - }; - vectors['ChaCha20-Poly1305'] = { - result: 'CryptoKey', - usages: [ - 'encrypt', - 'decrypt', - 'wrapKey', - 'unwrapKey', - ], - }; } else { common.printSkipMessage('Skipping unsupported test cases'); } @@ -196,7 +196,7 @@ if (hasOpenSSL(3)) { } } -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { vectors[name] = { result: 'CryptoKeyPair', @@ -606,17 +606,10 @@ if (hasOpenSSL(3, 5)) { [ 'AES-CBC', 256, ['encrypt', 'decrypt']], [ 'AES-GCM', 128, ['encrypt', 'decrypt']], [ 'AES-GCM', 256, ['encrypt', 'decrypt']], + [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], + [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], ]; - if (!process.features.openssl_is_boringssl) { - kTests.push( - [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], - [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], - ); - } else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); - } - const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); @@ -772,7 +765,7 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); } // Test ML-DSA Key Generation -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test( name, privateUsages, @@ -815,7 +808,7 @@ if (hasOpenSSL(3, 5)) { } // Test ML-KEM Key Generation -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test( name, privateUsages, @@ -850,7 +843,13 @@ if (hasOpenSSL(3, 5)) { assert.strictEqual(publicKey.usages, publicKey.usages); } - const kTests = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']; + const kTests = ['ML-KEM-768', 'ML-KEM-1024']; + + if (!process.features.openssl_is_boringssl) { + kTests.unshift('ML-KEM-512'); + } else { + common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); + } const tests = kTests.map((name) => test(name, ['decapsulateKey', 'decapsulateBits'], diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index 3ea0a961f41b90..d479abe3dcc989 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -59,28 +59,24 @@ await subtle.deriveKey( true, ['encrypt', 'decrypt']); -if (!process.features.openssl_is_boringssl) { - const wrappingKey = await subtle.generateKey( - { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); +const wrappingKey = await subtle.generateKey( + { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); - const keyToWrap = await subtle.generateKey( - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +const keyToWrap = await subtle.generateKey( + { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); - const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); +const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); - await subtle.unwrapKey( - 'raw', wrapped, wrappingKey, 'AES-KW', - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); -} else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); -} +await subtle.unwrapKey( + 'raw', wrapped, wrappingKey, 'AES-KW', + { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); const { privateKey } = await subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); await subtle.getPublicKey(privateKey, ['verify']); -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kemPair = await subtle.generateKey( { name: 'ML-KEM-768' }, false, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 1ed74c2508f438..b11e65ade79185 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const crypto = require('crypto'); diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 26e66d9aa0fa8b..0a6f5cffe7b934 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -173,7 +173,7 @@ if (!process.features.openssl_is_boringssl) { } // Test Sign/Verify ML-DSA -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test(name, data) { const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 8c57111daebca6..49f63e215fadfc 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -39,25 +39,20 @@ const kWrappingData = { }, pair: false }, -}; - -if (!process.features.openssl_is_boringssl) { - kWrappingData['AES-KW'] = { + 'AES-KW': { generate: { length: 128 }, wrap: { }, pair: false - }; - kWrappingData['ChaCha20-Poly1305'] = { + }, + 'ChaCha20-Poly1305': { wrap: { iv: new Uint8Array(12), additionalData: new Uint8Array(16), tagLength: 128 }, pair: false - }; -} else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); -} + } +}; if (hasOpenSSL(3)) { kWrappingData['AES-OCB'] = { @@ -188,34 +183,24 @@ async function generateKeysToWrap() { usages: ['sign', 'verify'], pair: false, }, - ]; - - if (!process.features.openssl_is_boringssl) { - parameters.push({ + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey'], pair: false, - }); - } else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); - } - - if (!process.features.openssl_is_boringssl) { - parameters.push({ + }, + { algorithm: { name: 'ChaCha20-Poly1305' }, usages: ['encrypt', 'decrypt'], pair: false, - }); - } else { - common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case'); - } + }, + ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { parameters.push({ algorithm: { name }, diff --git a/test/pummel/test-webcrypto-derivebits-pbkdf2.js b/test/pummel/test-webcrypto-derivebits-pbkdf2.js index cbe64bff77505c..bfb01ac0c94fe0 100644 --- a/test/pummel/test-webcrypto-derivebits-pbkdf2.js +++ b/test/pummel/test-webcrypto-derivebits-pbkdf2.js @@ -28,17 +28,10 @@ const kDerivedKeyTypes = [ ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; -if (!process.features.openssl_is_boringssl) { - kDerivedKeyTypes.push( - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], - ); -} else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); -} - const kPasswords = { short: '5040737377307264', long: '55736572732073686f756c64207069636b206c6f6' + diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 253877f1a970e0..4b01978511548f 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -45,7 +45,7 @@ if (!hasOpenSSL(3, 2)) { 'import_export/Argon2_importKey.tentative.https.any.js'); } -if (!hasOpenSSL(3, 5)) { +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { skip( 'encap_decap/encap_decap_bits.tentative.https.any.js', 'encap_decap/encap_decap_keys.tentative.https.any.js', @@ -61,6 +61,31 @@ if (!hasOpenSSL(3, 5)) { ['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]); } +if (process.features.openssl_is_boringssl) { + skip( + 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', + 'digest/cshake.tentative.https.any.js', + 'digest/sha3.tentative.https.any.js', + 'generateKey/failures_Ed448.tentative.https.any.js', + 'generateKey/failures_X448.tentative.https.any.js', + 'generateKey/successes_Ed448.tentative.https.any.js', + 'generateKey/successes_X448.tentative.https.any.js', + 'import_export/okp_importKey_Ed448.tentative.https.any.js', + 'import_export/okp_importKey_failures_Ed448.tentative.https.any.js', + 'import_export/okp_importKey_failures_X448.tentative.https.any.js', + 'import_export/okp_importKey_X448.tentative.https.any.js', + 'sign_verify/eddsa_curve448.tentative.https.any.js'); + + skipSubtests( + ['encap_decap/encap_decap_bits.tentative.https.any.js', /ml-kem-512/i], + ['encap_decap/encap_decap_keys.tentative.https.any.js', /ml-kem-512/i], + ['generateKey/failures_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['generateKey/successes_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['import_export/ML-KEM_importKey.tentative.https.any.js', /ml-kem-512/i], + ['supports-modern.tentative.https.any.js', /ml-kem-512/i]); +} + function assertNoOverlap(fileSkips, subtestSkips) { const subtestSkipFiles = new Set(Object.keys(subtestSkips)); const overlap = Object.keys(fileSkips).filter((file) => subtestSkipFiles.has(file)); diff --git a/tools/dep_updaters/update-nixpkgs-pin.sh b/tools/dep_updaters/update-nixpkgs-pin.sh index 97bcd878181c7b..eb5fde1526ab0c 100755 --- a/tools/dep_updaters/update-nixpkgs-pin.sh +++ b/tools/dep_updaters/update-nixpkgs-pin.sh @@ -29,16 +29,16 @@ mv "$TMP_FILE" "$NIXPKGS_PIN_FILE" nix-instantiate -I "nixpkgs=$NIXPKGS_PIN_FILE" --eval --strict --json -E " let pkgs = import {}; + opensslAttrs = builtins.filter + (n: builtins.match \"openssl_[0-9]+(_[0-9]+)?\" n != null) + (builtins.attrNames pkgs); + extraMatrixAttrs = [ \"boringssl\" ]; attrs = builtins.filter (n: let t = builtins.tryEval pkgs.\${n}; in t.success && (builtins.tryEval t.value.version).success ) - ( - builtins.filter - (n: builtins.match \"openssl_[0-9]+(_[0-9]+)?\" n != null) - (builtins.attrNames pkgs) - ); + (opensslAttrs ++ extraMatrixAttrs); in { inherit attrs; @@ -54,7 +54,7 @@ nix-instantiate -I "nixpkgs=$NIXPKGS_PIN_FILE" --eval --strict --json -E " { inherit (pkgs) - \(.attrs | join("\n ")) + \(.attrs | sort | join("\n ")) ; }"' > "$OPENSSL_MATRIX_FILE" diff --git a/tools/nix/openssl-matrix.nix b/tools/nix/openssl-matrix.nix index 3f9476acd7f7e0..36978c5d4efcb0 100644 --- a/tools/nix/openssl-matrix.nix +++ b/tools/nix/openssl-matrix.nix @@ -6,6 +6,7 @@ { inherit (pkgs) + boringssl openssl_1_1 openssl_3 openssl_3_5