From bf43342b85b249271ffc92acd78afc92d67383f5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 23 Aug 2026 14:07:41 +0200 Subject: [PATCH 1/4] crypto: discover hashes from OpenSSL providers Enumerate usable digests and aliases from activated OpenSSL 3 providers rather than relying only on the legacy digest registry. Normalize provider aliases, omit numeric OIDs and NULL, and validate them against the active default property query. Preserve legacy names and the OpenSSL 1.1.1 and BoringSSL paths. Expose KECCAK-KMAC-128, KECCAK-256, SHA256-192, and other provider digests. Add `functionName` and `customization` options for cSHAKE digests in `createHash()` and `crypto.hash()` with OpenSSL 4.0 or later. Resolve provider-only digest names across hashing, HMAC, KDF, signing, verification, and RSA digest options. Keep ordinary hash construction and one-shot hashing on the original binding arities and direct initialization paths. Use parameterized setup only when cSHAKE options are supplied. Lazily cache successful provider fetches per Environment. Index entries by case-insensitive query, canonical, and alias names. Deduplicate owners by provider and canonical identity. Return borrowed pointers on warm hits. Introduce a process-wide FIPS-state generation that advances only after successful, state-changing `setFips()` calls. Use it to invalidate per-Environment digest caches and refresh `getHashes()` snapshots in the main thread and workers. Keep cache IDs monotonic across invalidation because JavaScript Realms can retain them. Existing hash contexts can finish across a transition. Release provider owners before unloading worker addon DSOs. Document provider-dependent availability and operation-specific restrictions. Add known-answer vectors, option validation, provider resolution, property-query, FIPS transition, worker, snapshot, and cross-API coverage. Refs: https://github.com/nodejs/node/issues/62982 Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 229 +++++++- deps/ncrypto/ncrypto.h | 84 ++- doc/api/crypto.md | 142 ++++- lib/internal/crypto/hash.js | 65 ++- lib/internal/crypto/util.js | 49 +- src/crypto/crypto_hash.cc | 495 +++++++++++++----- src/crypto/crypto_hash.h | 8 +- src/crypto/crypto_rsa.cc | 21 +- src/crypto/crypto_util.cc | 115 ++++ src/crypto/crypto_util.h | 29 + src/env.cc | 12 + src/env.h | 16 +- test/addons/addons.status | 1 + test/addons/openssl-providers/providers.cjs | 17 +- .../test-default-properties-config.js | 129 +++++ .../openssl3-conf/default_properties.cnf | 19 + .../test-crypto-provider-hash-options.js | 387 ++++++++++++++ test/parallel/test-crypto-provider-hashes.js | 258 +++++++++ typings/internalBinding/crypto.d.ts | 11 +- 19 files changed, 1913 insertions(+), 174 deletions(-) create mode 100644 test/addons/openssl-providers/test-default-properties-config.js create mode 100644 test/fixtures/openssl3-conf/default_properties.cnf create mode 100644 test/parallel/test-crypto-provider-hash-options.js create mode 100644 test/parallel/test-crypto-provider-hashes.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index fb7446578f57..18dac63be728 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -14,6 +14,7 @@ #endif #include #include +#include #include #include #include @@ -507,23 +508,43 @@ DataPointer DataPointer::resize(size_t len) { } // ============================================================================ -bool isFipsEnabled() { - ClearErrorOnReturn clear_error_on_return; +namespace { +// This generation only coordinates cache invalidation. It does not make +// OpenSSL default property changes safe to race with crypto operations. +std::atomic fips_state_generation{0}; + +bool isFipsEnabledRaw() { #if OPENSSL_VERSION_MAJOR >= 3 return EVP_default_properties_is_fips_enabled(nullptr) == 1; #else return FIPS_mode() == 1; #endif } +} // namespace + +bool isFipsEnabled() { + ClearErrorOnReturn clear_error_on_return; + return isFipsEnabledRaw(); +} bool setFipsEnabled(bool enable, CryptoErrorList* errors) { - if (isFipsEnabled() == enable) return true; + const bool was_enabled = isFipsEnabled(); + if (was_enabled == enable) return true; ClearErrorOnReturn clearErrorOnReturn(errors); #if OPENSSL_VERSION_MAJOR >= 3 - return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; + const bool success = + EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; #else - return FIPS_mode_set(enable ? 1 : 0) == 1; + const bool success = FIPS_mode_set(enable ? 1 : 0) == 1; #endif + if (isFipsEnabledRaw() != was_enabled) { + fips_state_generation.fetch_add(1, std::memory_order_release); + } + return success; +} + +uint64_t getFipsStateGeneration() { + return fips_state_generation.load(std::memory_order_acquire); } bool testFipsEnabled() { @@ -4406,11 +4427,120 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) { // ============================================================================ +namespace { +constexpr char AsciiToLower(char c) { + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; +} + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +void PushAlgorithmAlias(const char* name, void* arg) { + if (name == nullptr) return; + static_cast*>(arg)->emplace_back(name); +} +#endif +} // namespace + #if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV Cipher::Cipher(DeleteFnPtr cipher) : cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {} #endif +size_t CaseInsensitiveNameHash::operator()( + std::string_view name) const noexcept { + size_t hash = 5381; + for (char c : name) hash = ((hash << 5) + hash) ^ AsciiToLower(c); + return hash; +} + +bool CaseInsensitiveNameEqual::operator()(std::string_view lhs, + std::string_view rhs) const noexcept { + if (lhs.size() != rhs.size()) return false; + for (size_t n = 0; n < lhs.size(); n++) { + if (AsciiToLower(lhs[n]) != AsciiToLower(rhs[n])) return false; + } + return true; +} + +DigestCache::Result DigestCache::lookup(const char* name, + uint64_t generation) const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation) return {}; + const auto it = aliases_.find(name); + if (it == aliases_.end()) return {}; + return lookup(it->second, generation); +#else + static_cast(name); + static_cast(generation); + return {}; +#endif +} + +DigestCache::Result DigestCache::insert(const char* name, + const EVP_MD* digest, + uint64_t generation) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation || name == nullptr || digest == nullptr) { + return {}; + } + + const char* canonical_name = EVP_MD_get0_name(digest); + const OSSL_PROVIDER* provider = EVP_MD_get0_provider(digest); + if (canonical_name == nullptr || provider == nullptr) return {}; + + for (size_t index = 0; index < digests_.size(); index++) { + const EVP_MD* cached = digests_[index].get(); + if (cached == nullptr) continue; + const char* cached_name = EVP_MD_get0_name(cached); + if (EVP_MD_get0_provider(cached) == provider && cached_name != nullptr && + CaseInsensitiveNameEqual()(cached_name, canonical_name)) { + const int32_t id = static_cast(first_id_ + index); + aliases_.insert_or_assign(name, id); + return {cached, id}; + } + } + + if (next_id_ == UINT32_MAX || + EVP_MD_up_ref(const_cast(digest)) != 1) { + return {}; + } + + digests_.emplace_back(const_cast(digest)); + const int32_t id = static_cast(next_id_++); + const size_t index = digests_.size() - 1; + + std::vector aliases; + EVP_MD_names_do_all(digests_[index].get(), PushAlgorithmAlias, &aliases); + for (const std::string& alias : aliases) aliases_.emplace(alias, id); + aliases_.insert_or_assign(name, id); + + return {digests_[index].get(), id}; +#else + static_cast(name); + static_cast(digest); + static_cast(generation); + return {}; +#endif +} + +void DigestCache::reset(uint64_t generation) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ == generation) return; + aliases_.clear(); + digests_.clear(); + first_id_ = next_id_; +#endif + generation_ = generation; +} + +const DigestCache::AliasMap& DigestCache::aliases() const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return aliases_; +#else + static const AliasMap empty; + return empty; +#endif +} + Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) { #if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV if (other.fetched_cipher_ != nullptr) { @@ -6479,11 +6609,19 @@ EVP_MD_CTX* EVPMDCtxPointer::release() { return ctx_.release(); } -bool EVPMDCtxPointer::digestInit(const Digest& digest) { +bool EVPMDCtxPointer::digestInit(const EVP_MD* digest) { if (!ctx_) return false; return EVP_DigestInit_ex(ctx_.get(), digest, nullptr) > 0; } +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0) +bool EVPMDCtxPointer::digestInit(const EVP_MD* digest, + const OSSL_PARAM* params) { + if (!ctx_) return false; + return EVP_DigestInit_ex2(ctx_.get(), digest, params) > 0; +} +#endif + bool EVPMDCtxPointer::digestUpdate(const Buffer& in) { if (!ctx_) return false; return EVP_DigestUpdate(ctx_.get(), in.data, in.len) > 0; @@ -7009,7 +7147,10 @@ DataPointer xofHashDigest(const Buffer& buf, if (ctx.digestInit(md) != 1) { return {}; } - if (ctx.digestUpdate(reinterpret_cast&>(buf)) != 1) { + if (ctx.digestUpdate(Buffer{ + .data = buf.data, + .len = buf.len, + }) != 1) { return {}; } return ctx.digestFinal(output_length); @@ -7145,14 +7286,86 @@ size_t Digest::size() const { return EVP_MD_size(md_); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +Digest::Digest(DeleteFnPtr md) + : md_(md.get()), fetched_md_(std::move(md)) {} +#endif + +Digest::Digest(const Digest& other) : md_(other.md_) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (other.fetched_md_ != nullptr) { + if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) { + fetched_md_.reset(other.fetched_md_.get()); + } else { + md_ = nullptr; + } + } +#endif +} + +Digest& Digest::operator=(const Digest& other) { + if (this == &other) return *this; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (other.fetched_md_ != nullptr) { + if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) { + fetched_md_.reset(other.fetched_md_.get()); + } else { + fetched_md_.reset(); + md_ = nullptr; + return *this; + } + } else { + fetched_md_.reset(); + } +#endif + md_ = other.md_; + return *this; +} + const Digest Digest::MD5 = Digest(EVP_md5()); const Digest Digest::SHA1 = Digest(EVP_sha1()); const Digest Digest::SHA256 = Digest(EVP_sha256()); const Digest Digest::SHA384 = Digest(EVP_sha384()); const Digest Digest::SHA512 = Digest(EVP_sha512()); +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +bool IsSupportedDigest(const EVP_MD* md) { + if (md == nullptr || EVP_MD_is_a(md, "NULL")) return false; + + // OpenSSL currently crashes when ML-DSA-MU finalizes an empty input. Keep it + // unavailable until the provider implementation is fixed. + // https://github.com/openssl/openssl/issues/32445 + if (EVP_MD_is_a(md, "ML-DSA-MU")) return false; + + return true; +} +} // namespace +#endif + const Digest Digest::FromName(const char* name) { - return ncrypto::getDigestByName(name); + const EVP_MD* md = ncrypto::getDigestByName(name); + if (md != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (md == EVP_md_null()) return Digest(); +#endif + return Digest(md); + } + + return Fetch(name); +} + +const Digest Digest::Fetch(const char* name) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + MarkPopErrorOnReturn mark_pop_error_on_return; + DeleteFnPtr fetched( + EVP_MD_fetch(nullptr, name, nullptr)); + if (IsSupportedDigest(fetched.get())) { + return Digest(std::move(fetched)); + } +#endif + + return Digest(); } // ============================================================================ diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 58e32cc18fc7..8d4091c75a98 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -397,9 +398,12 @@ class Digest final { static constexpr size_t MAX_SIZE = EVP_MAX_MD_SIZE; Digest() = default; Digest(const EVP_MD* md) : md_(md) {} - Digest(const Digest&) = default; - Digest& operator=(const Digest&) = default; + Digest(const Digest& other); + Digest& operator=(const Digest& other); inline Digest& operator=(const EVP_MD* md) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + fetched_md_.reset(); +#endif md_ = md; return *this; } @@ -418,9 +422,72 @@ class Digest final { static const Digest SHA512; static const Digest FromName(const char* name); + static const Digest Fetch(const char* name); private: const EVP_MD* md_ = nullptr; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + explicit Digest(DeleteFnPtr md); + DeleteFnPtr fetched_md_; +#endif +}; + +struct CaseInsensitiveNameHash { + using is_transparent = void; + size_t operator()(std::string_view name) const noexcept; +}; + +struct CaseInsensitiveNameEqual { + using is_transparent = void; + bool operator()(std::string_view lhs, std::string_view rhs) const noexcept; +}; + +class DigestCache final { + public: + struct Result { + const EVP_MD* digest = nullptr; + int32_t id = -1; + }; + + using AliasMap = std::unordered_map; + + DigestCache() = default; + NCRYPTO_DISALLOW_COPY_AND_MOVE(DigestCache) + + Result lookup(const char* name, uint64_t generation) const; + inline Result lookup(int32_t id, uint64_t generation) const { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation || id == -1) return {}; + const uint32_t unsigned_id = static_cast(id); + if (unsigned_id < first_id_) return {}; + const size_t index = unsigned_id - first_id_; + if (index >= digests_.size()) return {}; + return {digests_[index].get(), id}; +#else + static_cast(id); + static_cast(generation); + return {}; +#endif + } + Result insert(const char* name, const EVP_MD* digest, uint64_t generation); + void reset(uint64_t generation); + const AliasMap& aliases() const; + + private: + uint64_t generation_ = 0; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + using EVPMDPointer = DeleteFnPtr; + + // IDs are not reused across generations because JavaScript caches them + // independently in each Realm. + uint32_t first_id_ = 0; + uint32_t next_id_ = 0; + std::vector digests_; + AliasMap aliases_; +#endif }; // Computes a fixed-length digest. @@ -1690,7 +1757,16 @@ class EVPMDCtxPointer final { void reset(EVP_MD_CTX* ctx = nullptr); EVP_MD_CTX* release(); - bool digestInit(const Digest& digest); + bool digestInit(const EVP_MD* digest); + inline bool digestInit(const Digest& digest) { + return digestInit(digest.get()); + } +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0) + bool digestInit(const EVP_MD* digest, const OSSL_PARAM* params); + inline bool digestInit(const Digest& digest, const OSSL_PARAM* params) { + return digestInit(digest.get(), params); + } +#endif bool digestUpdate(const Buffer& in); DataPointer digestFinal(size_t length); bool digestFinalInto(Buffer* buf); @@ -1880,6 +1956,8 @@ bool isFipsEnabled(); bool setFipsEnabled(bool enabled, CryptoErrorList* errors); +uint64_t getFipsStateGeneration(); + bool testFipsEnabled(); // ============================================================================ diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 4c61a34a0030..78add10d64fd 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3807,6 +3807,11 @@ and description of each available elliptic curve. * Returns: {string\[]} An array of the names of the supported hash algorithms, such as `'RSA-SHA256'`. Hash algorithms are also called "digest" algorithms. +This is the authoritative Node.js list of hash algorithms available to +[`crypto.createHash()`][] and [`crypto.hash()`][] in the current process. With +OpenSSL 3 or later, the list depends on the loaded providers and the default +property query in effect when the list is first generated. Some listed +algorithms can require API-specific options, such as `outputLength` for XOF +hash functions. + +A listed hash algorithm is not necessarily supported by APIs that combine a +digest with another cryptographic operation, such as HMAC, key derivation, or +signing. Those operations can apply additional restrictions. + ```mjs const { getHashes, @@ -4995,6 +5047,11 @@ added: - v21.7.0 - v20.12.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/65484 + description: Hash algorithms exposed by OpenSSL providers are now + supported. The `functionName` and `customization` options + were added for cSHAKE hash functions. - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/64000 description: The `outputLength` option is now required for XOF @@ -5016,6 +5073,12 @@ changes: into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing the encoded `TypedArray` into this API instead. * `options` {Object|string} + * `customization` {string|ArrayBuffer|Buffer|TypedArray|DataView} For cSHAKE + hash functions, specifies the customization byte string. **Default:** an + empty byte string. + * `functionName` {string|ArrayBuffer|Buffer|TypedArray|DataView} For cSHAKE + hash functions, specifies the NIST function-name byte string. **Default:** + an empty byte string. * `outputEncoding` {string} [Encoding][encoding] used to encode the returned digest. **Default:** `'hex'`. * `outputLength` {number} For XOF hash functions such as 'shake256', @@ -5028,10 +5091,21 @@ the object-based `crypto.createHash()` when hashing a smaller amount of data (<= 5MB) that's readily available. If the data can be big or if it is streamed, it's still recommended to use `crypto.createHash()` instead. -The `algorithm` is dependent on the available algorithms supported by the -version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. -On recent releases of OpenSSL, `openssl list -digest-algorithms` will -display the available digest algorithms. +The available algorithms depend on the version and configuration of OpenSSL on +the platform. Examples are `'sha256'` and `'sha512'`. Use +[`crypto.getHashes()`][] to obtain the list of hash algorithms available to the +Node.js process. + +The `functionName` and `customization` options apply only to cSHAKE-128 and +cSHAKE-256. They are supported only when Node.js is built with OpenSSL 4.0 or +later and the selected provider supports the corresponding digest parameters. +Strings are encoded as UTF-8, and neither strings nor byte values may contain +NUL bytes. Both options default to an empty byte string. For OpenSSL's built-in +providers, `functionName` is case-sensitive and must be `''`, `'TupleHash'`, +`'ParallelHash'`, or `'KMAC'`. Other providers can impose different +restrictions. With both options empty, cSHAKE produces the same output as the +corresponding SHAKE function for the same output length. `cshake-128` and +`cshake-256` default to output lengths of 32 and 64 bytes, respectively. If `options` is a string, then it specifies the `outputEncoding`. @@ -5103,6 +5177,10 @@ changes: HKDF is a simple key derivation function defined in RFC 5869. The given `ikm`, `salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. +The available digest algorithms depend on the version and configuration of +OpenSSL. HKDF uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or HKDF. The supplied `callback` function is called with two arguments: `err` and `derivedKey`. If an error occurs while deriving the key, `err` will be set; @@ -5162,6 +5240,10 @@ changes: Provides a synchronous HKDF key derivation function as defined in RFC 5869. The given `ikm`, `salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. +The available digest algorithms depend on the version and configuration of +OpenSSL. HKDF uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or HKDF. The successfully generated `derivedKey` will be returned as an {ArrayBuffer}. @@ -5271,8 +5353,10 @@ pbkdf2('secret', 'salt', 100000, 64, 'sha512', (err, derivedKey) => { }); ``` -An array of supported digest functions can be retrieved using -[`crypto.getHashes()`][]. +The available digest algorithms depend on the version and configuration of +OpenSSL. PBKDF2 uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or PBKDF2. This API uses libuv's threadpool, which can have surprising and negative performance implications for some applications; see the @@ -5344,8 +5428,10 @@ const key = pbkdf2Sync('secret', 'salt', 100000, 64, 'sha512'); console.log(key.toString('hex')); // '3745e48...08d59ae' ``` -An array of supported digest functions can be retrieved using -[`crypto.getHashes()`][]. +The available digest algorithms depend on the version and configuration of +OpenSSL. PBKDF2 uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or PBKDF2. ### `crypto.privateDecrypt(privateKey, buffer)` @@ -5404,6 +5490,10 @@ changes: Decrypts `buffer` with `privateKey`. `buffer` was previously encrypted using the corresponding public key, for example using [`crypto.publicEncrypt()`][]. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the active RSA implementation can impose additional restrictions on digests +used for OAEP or MGF1. + If `privateKey` is not a [`KeyObject`][], this function behaves as if `privateKey` had been passed to [`crypto.createPrivateKey()`][]. If it is an object, the `padding` property can be passed. Otherwise, this function uses @@ -5561,6 +5651,10 @@ Encrypts the content of `buffer` with `key` and returns a new [`Buffer`][] with encrypted content. The returned data can be decrypted using the corresponding private key, for example using [`crypto.privateDecrypt()`][]. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the active RSA implementation can impose additional restrictions on digests +used for OAEP or MGF1. + If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPublicKey()`][]. If it is an object, the `padding` property can be passed. Otherwise, this function uses @@ -6347,6 +6441,10 @@ dependent upon the key type. `algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and ML-DSA. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the key type and signature scheme determine whether a listed digest can be +used for signing. + If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPrivateKey()`][]. When `key` is a string, `ArrayBuffer`, [`Buffer`][], `TypedArray`, or `DataView`, it must contain PEM-encoded key @@ -6489,6 +6587,10 @@ key type. `algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and ML-DSA. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the key type and signature scheme determine whether a listed digest can be +used for verification. + If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPublicKey()`][]. When `key` is a string, `ArrayBuffer`, [`Buffer`][], `TypedArray`, or `DataView`, it must contain PEM-encoded key diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 2445a3f74885..57f6c9ea51d4 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -6,6 +6,7 @@ const { StringPrototypeToLowerCase, Symbol, TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeIncludes, } = primordials; const { @@ -30,6 +31,8 @@ const { kHandle, getCachedHashId, getHashCache, + getArrayBufferOrView, + getBufferSourceBytes, getOptionalByteLength, } = require('internal/crypto/util'); @@ -78,6 +81,17 @@ const emitHmacDigestDeprecation = getDeprecationWarningEmitter( 'Calling Hmac.digest() more than once is deprecated.', ); +function normalizeCShakeParameter(value, name) { + if (value === undefined) return undefined; + + value = getArrayBufferOrView(value, name); + if (TypedArrayPrototypeIncludes(getBufferSourceBytes(value), 0)) { + throw new ERR_INVALID_ARG_VALUE( + name, value, 'must not contain NUL bytes'); + } + return value; +} + function Hash(algorithm, options) { if (!new.target) return new Hash(algorithm, options); @@ -91,10 +105,36 @@ function Hash(algorithm, options) { // Coerce -0 to +0. xofLen += 0; } + let functionName; + let customization; + if (!isCopy && options !== undefined && options !== null) { + const functionNameOption = options.functionName; + if (functionNameOption !== undefined) { + functionName = normalizeCShakeParameter( + functionNameOption, 'options.functionName'); + } + const customizationOption = options.customization; + if (customizationOption !== undefined) { + customization = normalizeCShakeParameter( + customizationOption, 'options.customization'); + } + } // Lookup the cached ID from JS land because it's faster than decoding // the string in C++ land. const algorithmId = isCopy ? -1 : getCachedHashId(algorithm); - this[kHandle] = new _Hash(algorithm, xofLen, algorithmId, getHashCache()); + if (functionName === undefined && customization === undefined) { + this[kHandle] = new _Hash( + algorithm, xofLen, algorithmId, getHashCache()); + } else { + this[kHandle] = new _Hash( + algorithm, + xofLen, + algorithmId, + getHashCache(), + functionName, + customization, + ); + } this[kState] = { [kFinalized]: false, }; @@ -277,8 +317,14 @@ function hash(algorithm, input, options) { if (typeof input !== 'string' && !isArrayBufferView(input)) { throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input); } + if (options === undefined) { + return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(), + input, 'hex', encodingsMap.hex, undefined); + } let outputEncoding; let outputLength; + let functionName; + let customization; if (typeof options === 'string') { outputEncoding = options; @@ -286,6 +332,16 @@ function hash(algorithm, input, options) { validateObject(options, 'options'); outputLength = options.outputLength; outputEncoding = options.outputEncoding; + const functionNameOption = options.functionName; + if (functionNameOption !== undefined) { + functionName = normalizeCShakeParameter( + functionNameOption, 'options.functionName'); + } + const customizationOption = options.customization; + if (customizationOption !== undefined) { + customization = normalizeCShakeParameter( + customizationOption, 'options.customization'); + } } outputEncoding ??= 'hex'; @@ -312,8 +368,13 @@ function hash(algorithm, input, options) { outputLength += 0; } + if (functionName === undefined && customization === undefined) { + return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(), + input, normalized, encodingsMap[normalized], outputLength); + } return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(), - input, normalized, encodingsMap[normalized], outputLength); + input, normalized, encodingsMap[normalized], outputLength, + functionName, customization); } module.exports = { diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 3c52feebea68..8791e53ace78 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -5,6 +5,7 @@ const { ArrayBufferPrototypeGetByteLength, ArrayPrototypeIncludes, ArrayPrototypePush, + ArrayPrototypeSlice, BigInt, DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, @@ -48,6 +49,7 @@ const { kKeyVariantAES_OCB_128: hasAesOcbMode, Argon2Job, getFipsCrypto, + getFipsCryptoGeneration, KmacJob, } = internalBinding('crypto'); @@ -118,24 +120,57 @@ function toBuf(val, encoding) { } let _hashCache; +if (isBuildingSnapshot()) { + addSerializeCallback(() => { _hashCache = undefined; }); +} + function getHashCache() { - if (_hashCache === undefined) { - _hashCache = getCachedAliases(); - if (isBuildingSnapshot()) { - // For dynamic linking, clear the map. - addSerializeCallback(() => { _hashCache = undefined; }); - } + while (_hashCache === undefined) { + const generation = getFipsCryptoGeneration(); + const cache = getCachedAliases(); + if (generation !== getFipsCryptoGeneration()) continue; + _hashCache = cache; } return _hashCache; } +function cachedArrayByFipsGeneration(fn, onRefresh) { + let result; + let generation; + if (isBuildingSnapshot()) { + addSerializeCallback(() => { + result = undefined; + generation = undefined; + }); + } + + return () => { + while (true) { + const current = getFipsCryptoGeneration(); + if (result === undefined || generation !== current) { + const next = fn(); + if (current !== getFipsCryptoGeneration()) continue; + result = next; + generation = current; + if (onRefresh !== undefined) onRefresh(); + } + return ArrayPrototypeSlice(result); + } + }; +} + function getCachedHashId(algorithm) { const result = getHashCache()[algorithm]; return result === undefined ? -1 : result; } const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers())); -const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes())); +const getHashes = cachedArrayByFipsGeneration( + () => filterDuplicateStrings(_getHashes()), + () => { + _hashCache = undefined; + }); + const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); const emitOpenSSLEngineDeprecation = getDeprecationWarningEmitter( diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 2ed356120c48..dc9b54adeb71 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -79,59 +79,97 @@ constexpr BoringSSLDigest kBoringSSLDigests[] = { }; #endif -#if OPENSSL_VERSION_MAJOR >= 3 -void PushAliases(const char* name, void* data) { - static_cast*>(data)->push_back(name); +void ResetHashCache(Environment* env, + uint64_t generation, + Local algorithm_cache = Local()) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + ncrypto::DigestCache* cache = env->provider_digest_cache.get(); + CHECK_NOT_NULL(cache); + if (!algorithm_cache.IsEmpty()) { + Isolate* isolate = env->isolate(); + Local context = isolate->GetCurrentContext(); + for (const auto& entry : cache->aliases()) { + algorithm_cache + ->Set(context, + OneByteString(isolate, entry.first), + Int32::New(isolate, -1)) + .Check(); + } + } + cache->reset(generation); +#endif + env->supported_hash_algorithms.clear(); + env->hash_cache_generation = generation; +} + +bool SynchronizeHashCache(Environment* env, + Local algorithm_cache = Local()) { + const uint64_t generation = ncrypto::getFipsStateGeneration(); + if (env->hash_cache_generation == generation) return false; + + ResetHashCache(env, generation, algorithm_cache); + return true; } -EVP_MD* GetCachedMDByID(Environment* env, size_t id) { - CHECK_LT(id, env->evp_md_cache.size()); - EVP_MD* result = env->evp_md_cache[id].get(); - CHECK_NOT_NULL(result); - return result; +#if NCRYPTO_USE_OPENSSL3_PROVIDER +const EVP_MD* GetCachedMDByID(Environment* env, + int32_t id, + Local algorithm_cache = Local()) { + if (SynchronizeHashCache(env, algorithm_cache) || + env->provider_digest_cache == nullptr) { + return nullptr; + } + return env->provider_digest_cache->lookup(id, env->hash_cache_generation) + .digest; } struct MaybeCachedMD { - EVP_MD* explicit_md = nullptr; - const EVP_MD* implicit_md = nullptr; + const EVP_MD* cached_md = nullptr; + ncrypto::Digest digest; int32_t cache_id = -1; }; -MaybeCachedMD FetchAndMaybeCacheMD(Environment* env, const char* search_name) { - const EVP_MD* implicit_md = ncrypto::getDigestByName(search_name); - if (!implicit_md) return {nullptr, nullptr, -1}; - - const char* real_name = EVP_MD_get0_name(implicit_md); - if (!real_name) return {nullptr, implicit_md, -1}; - - auto it = env->alias_to_md_id_map.find(real_name); - if (it != env->alias_to_md_id_map.end()) { - size_t id = it->second; - return {GetCachedMDByID(env, id), implicit_md, static_cast(id)}; +MaybeCachedMD FetchAndMaybeCacheMD( + Environment* env, + const char* search_name, + Local algorithm_cache = Local(), + const char* fetch_name = nullptr) { + SynchronizeHashCache(env, algorithm_cache); + ncrypto::DigestCache* cache = env->provider_digest_cache.get(); + CHECK_NOT_NULL(cache); + const uint64_t generation = env->hash_cache_generation; + const EVP_MD* legacy = nullptr; + + if (auto cached = cache->lookup(search_name, generation); + cached.digest != nullptr) { + return {cached.digest, cached.digest, cached.id}; } - // EVP_*_fetch() does not support alias names, so we need to pass it the - // real/original algorithm name. - // We use EVP_*_fetch() as a filter here because it will only return an - // instance if the algorithm is supported by the public OpenSSL APIs (some - // algorithms are used internally by OpenSSL and are also passed to this - // callback). - EVP_MD* explicit_md = EVP_MD_fetch(nullptr, real_name, nullptr); - if (!explicit_md) return {nullptr, implicit_md, -1}; + if (fetch_name == nullptr) { + legacy = ncrypto::getDigestByName(search_name); + if (legacy != nullptr) { + if (legacy == EVP_md_null()) return {}; + fetch_name = EVP_MD_get0_name(legacy); + if (fetch_name == nullptr) return {nullptr, legacy, -1}; + } else { + fetch_name = search_name; + } + } - // Cache the EVP_MD* fetched. - env->evp_md_cache.emplace_back(explicit_md); - size_t id = env->evp_md_cache.size() - 1; + const ncrypto::Digest digest = ncrypto::Digest::Fetch(fetch_name); + if (!digest) { + return legacy == nullptr ? MaybeCachedMD{} + : MaybeCachedMD{nullptr, legacy, -1}; + } - // Add all the aliases to the map to speed up next lookup. - std::vector aliases; - EVP_MD_names_do_all(explicit_md, PushAliases, &aliases); - for (const auto& alias : aliases) { - env->alias_to_md_id_map.emplace(alias, id); + if (generation == ncrypto::getFipsStateGeneration()) { + auto cached = cache->insert(search_name, digest.get(), generation); + if (cached.digest != nullptr) { + return {cached.digest, cached.digest, cached.id}; + } } - env->alias_to_md_id_map.emplace(search_name, id); - return {explicit_md, implicit_md, static_cast(id)}; + return {nullptr, digest, -1}; } void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md, @@ -140,12 +178,57 @@ void SaveSupportedHashAlgorithmsAndCacheMD(const EVP_MD* md, void* arg) { if (!from) return; Environment* env = static_cast(arg); - auto result = FetchAndMaybeCacheMD(env, from); - if (result.explicit_md) { + const ncrypto::Digest legacy = ncrypto::Digest::FromName(from); + const char* canonical_name = legacy ? EVP_MD_get0_name(legacy) : nullptr; + if (canonical_name == nullptr) return; + + auto result = FetchAndMaybeCacheMD(env, from, {}, canonical_name); + if (result.cached_md || result.digest) { env->supported_hash_algorithms.push_back(from); } } +struct ProviderHashNameContext { + Environment* env; +}; + +void SaveSupportedProviderHashName(const char* name, void* arg) { + if (name == nullptr) return; + + const std::string_view name_view(name); + const bool is_dotted_decimal = + name_view.find('.') != std::string_view::npos && + std::all_of(name_view.begin(), name_view.end(), [](unsigned char c) { + return (c >= '0' && c <= '9') || c == '.'; + }); + if (is_dotted_decimal) return; + + std::string normalized_name(name_view); + std::transform(normalized_name.begin(), + normalized_name.end(), + normalized_name.begin(), + [](unsigned char c) { + if (c >= 'A' && c <= 'Z') { + return static_cast(c + ('a' - 'A')); + } + return static_cast(c); + }); + + auto* context = static_cast(arg); + auto result = FetchAndMaybeCacheMD( + context->env, normalized_name.c_str(), {}, normalized_name.c_str()); + if (result.cached_md || result.digest) { + context->env->supported_hash_algorithms.push_back(normalized_name); + } +} + +void SaveSupportedProviderHashAlgorithms(EVP_MD* md, void* arg) { + ProviderHashNameContext context = { + .env = static_cast(arg), + }; + EVP_MD_names_do_all(md, SaveSupportedProviderHashName, &context); +} + #else void SaveSupportedHashAlgorithms(const EVP_MD* md, const char* from, @@ -155,25 +238,34 @@ void SaveSupportedHashAlgorithms(const EVP_MD* md, Environment* env = static_cast(arg); env->supported_hash_algorithms.push_back(from); } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // NCRYPTO_USE_OPENSSL3_PROVIDER const std::vector& GetSupportedHashAlgorithms(Environment* env) { - if (env->supported_hash_algorithms.empty()) { - MarkPopErrorOnReturn mark_pop_error_on_return; + while (true) { + SynchronizeHashCache(env); + const uint64_t generation = env->hash_cache_generation; + if (env->supported_hash_algorithms.empty()) { + MarkPopErrorOnReturn mark_pop_error_on_return; #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); + for (const auto& digest : kBoringSSLDigests) { + static_cast(digest.get); + env->supported_hash_algorithms.emplace_back(digest.name); + } +#elif NCRYPTO_USE_OPENSSL3_PROVIDER + // 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); + EVP_MD_do_all_provided(nullptr, SaveSupportedProviderHashAlgorithms, env); #else - EVP_MD_do_all_sorted(SaveSupportedHashAlgorithms, env); + EVP_MD_do_all_sorted(SaveSupportedHashAlgorithms, env); #endif + } + const uint64_t current_generation = ncrypto::getFipsStateGeneration(); + if (generation == current_generation) { + return env->supported_hash_algorithms; + } + ResetHashCache(env, current_generation); } - return env->supported_hash_algorithms; } void Hash::GetHashes(const FunctionCallbackInfo& args) { @@ -191,18 +283,19 @@ void Hash::GetCachedAliases(const FunctionCallbackInfo& args) { Isolate* isolate = args.GetIsolate(); Local context = args.GetIsolate()->GetCurrentContext(); Environment* env = Environment::GetCurrent(context); - size_t size = env->alias_to_md_id_map.size(); + SynchronizeHashCache(env); + size_t size = 0; LocalVector names(isolate); LocalVector values(isolate); -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const auto& aliases = env->provider_digest_cache->aliases(); + size = aliases.size(); names.reserve(size); values.reserve(size); - for (auto& [alias, id] : env->alias_to_md_id_map) { + for (const auto& [alias, id] : aliases) { names.push_back(OneByteString(isolate, alias)); - values.push_back(Uint32::New(isolate, id)); + values.push_back(Int32::New(isolate, id)); } -#else - CHECK(env->alias_to_md_id_map.empty()); #endif Local prototype = Null(isolate); Local result = @@ -210,18 +303,24 @@ void Hash::GetCachedAliases(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } -const EVP_MD* GetDigestImplementation(Environment* env, - Local algorithm, - Local cache_id_val, - Local algorithm_cache) { +const EVP_MD* GetDigestImplementation( + Environment* env, + Local algorithm, + Local cache_id_val, + Local algorithm_cache, + std::optional& digest_owner) { CHECK(algorithm->IsString()); CHECK(cache_id_val->IsInt32()); CHECK(algorithm_cache->IsObject()); + DCHECK(!digest_owner.has_value()); -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER + Local cache = algorithm_cache.As(); + const bool cache_invalidated = SynchronizeHashCache(env, cache); int32_t cache_id = cache_id_val.As()->Value(); - if (cache_id != -1) { // Alias already cached, return the cached EVP_MD*. - return GetCachedMDByID(env, cache_id); + if (!cache_invalidated && cache_id != -1) { + // Alias already cached, return the cached EVP_MD*. + if (const EVP_MD* md = GetCachedMDByID(env, cache_id, cache)) return md; } // Only decode the algorithm when we don't have it cached to avoid @@ -229,11 +328,10 @@ const EVP_MD* GetDigestImplementation(Environment* env, Isolate* isolate = env->isolate(); Utf8Value utf8(isolate, algorithm); - auto result = FetchAndMaybeCacheMD(env, *utf8); + auto result = FetchAndMaybeCacheMD(env, *utf8, cache); if (result.cache_id != -1) { - // Add the alias to both C++ side and JS side to speedup the lookup - // next time. - env->alias_to_md_id_map.emplace(*utf8, result.cache_id); + // Add the alias to the JavaScript side to speed up the next lookup. The + // native cache added it while inserting the implementation. if (algorithm_cache.As() ->Set(isolate->GetCurrentContext(), algorithm, @@ -243,7 +341,12 @@ const EVP_MD* GetDigestImplementation(Environment* env, } } - return result.explicit_md ? result.explicit_md : result.implicit_md; + if (result.cached_md != nullptr) return result.cached_md; + if (result.digest) { + digest_owner.emplace(result.digest); + return digest_owner->get(); + } + return nullptr; #else Utf8Value utf8(env->isolate(), algorithm); return ncrypto::getDigestByName(*utf8); @@ -266,7 +369,7 @@ void MarkInvalidXofLength() { // version-independent. #if !OPENSSL_VERSION_PREREQ(3, 4) bool IsShakeDigest(const EVP_MD* md) { -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER return EVP_MD_is_a(md, "SHAKE128") || EVP_MD_is_a(md, "SHAKE256"); #else const char* name = OBJ_nid2sn(EVP_MD_type(md)); @@ -287,21 +390,11 @@ bool ShouldRejectMissingXofLength(const EVP_MD* md, size_t default_length) { #endif } -// crypto.digest(algorithm, algorithmId, algorithmCache, -// input, outputEncoding, outputEncodingId, outputLength) -void Hash::OneShotDigest(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); +void OneShotDigestWithMD(Environment* env, + const FunctionCallbackInfo& args, + const EVP_MD* md, + const CShakeOptions* options) { Isolate* isolate = env->isolate(); - CHECK_EQ(args.Length(), 7); - CHECK(args[0]->IsString()); // algorithm - CHECK(args[1]->IsInt32()); // algorithmId - CHECK(args[2]->IsObject()); // algorithmCache - CHECK(args[3]->IsString() || args[3]->IsArrayBufferView()); // input - CHECK(args[4]->IsString()); // outputEncoding - CHECK(args[5]->IsUint32() || args[5]->IsUndefined()); // outputEncodingId - CHECK(args[6]->IsUint32() || args[6]->IsUndefined()); // outputLength - - const EVP_MD* md = GetDigestImplementation(env, args[0], args[1], args[2]); if (md == nullptr) [[unlikely]] { Utf8Value method(isolate, args[0]); std::string message = @@ -335,7 +428,7 @@ void Hash::OneShotDigest(const FunctionCallbackInfo& args) { } } - if (output_length == 0) { + auto return_empty_output = [&]() { if (output_enc == BUFFER) { Local u8; if (Buffer::New(isolate, ArrayBuffer::New(isolate, 0), 0, 0) @@ -345,28 +438,72 @@ void Hash::OneShotDigest(const FunctionCallbackInfo& args) { } else { args.GetReturnValue().Set(String::Empty(isolate)); } + }; + + const bool has_digest_options = options != nullptr && !options->empty(); + if (output_length == 0 && !has_digest_options) { + return return_empty_output(); + } + + if (!has_digest_options) { + DataPointer output = ([&]() -> DataPointer { + if (args[3]->IsString()) { + Utf8Value utf8(isolate, args[3]); + ncrypto::Buffer input = { + .data = reinterpret_cast(utf8.out()), + .len = static_cast(utf8.length()), + }; + return is_xof ? ncrypto::xofHashDigest(input, md, output_length) + : ncrypto::hashDigest(input, md); + } + + ArrayBufferViewContents input(args[3]); + ncrypto::Buffer buffer = { + .data = input.data(), + .len = input.length(), + }; + return is_xof ? ncrypto::xofHashDigest(buffer, md, output_length) + : ncrypto::hashDigest(buffer, md); + })(); + if (!output) [[unlikely]] { + return ThrowCryptoError(env, ERR_get_error()); + } + + Local ret; + if (StringBytes::Encode(env->isolate(), + static_cast(output.get()), + output.size(), + output_enc) + .ToLocal(&ret)) { + args.GetReturnValue().Set(ret); + } return; } - DataPointer output = ([&]() -> DataPointer { + EVPMDCtxPointer ctx = EVPMDCtxPointer::New(); + if (!options->Initialize(&ctx, md)) { + return ThrowCryptoError( + env, ERR_get_error(), "Digest options are not supported"); + } + + const bool updated = [&]() { if (args[3]->IsString()) { - Utf8Value utf8(isolate, args[3]); - ncrypto::Buffer buf = { - .data = reinterpret_cast(utf8.out()), - .len = utf8.length(), - }; - return is_xof ? ncrypto::xofHashDigest(buf, md, output_length) - : ncrypto::hashDigest(buf, md); + Utf8Value input(isolate, args[3]); + return ctx.digestUpdate(ncrypto::Buffer{ + .data = input.out(), + .len = static_cast(input.length()), + }); } ArrayBufferViewContents input(args[3]); - ncrypto::Buffer buf = { - .data = reinterpret_cast(input.data()), + return ctx.digestUpdate(ncrypto::Buffer{ + .data = input.data(), .len = input.length(), - }; - return is_xof ? ncrypto::xofHashDigest(buf, md, output_length) - : ncrypto::hashDigest(buf, md); - })(); + }); + }(); + if (!updated) return ThrowCryptoError(env, ERR_get_error()); + if (output_length == 0) return return_empty_output(); + DataPointer output = ctx.digestFinal(output_length); if (!output) [[unlikely]] { return ThrowCryptoError(env, ERR_get_error()); @@ -382,6 +519,51 @@ void Hash::OneShotDigest(const FunctionCallbackInfo& args) { } } +// crypto.digest(algorithm, algorithmId, algorithmCache, input, outputEncoding, +// outputEncodingId, outputLength[, functionName, customization]) +void Hash::OneShotDigest(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.Length() == 7 || args.Length() == 9); + CHECK(args[0]->IsString()); // algorithm + CHECK(args[1]->IsInt32()); // algorithmId + CHECK(args[2]->IsObject()); // algorithmCache + CHECK(args[3]->IsString() || args[3]->IsArrayBufferView()); // input + CHECK(args[4]->IsString()); // outputEncoding + CHECK(args[5]->IsUint32() || args[5]->IsUndefined()); // outputEncodingId + CHECK(args[6]->IsUint32() || args[6]->IsUndefined()); // outputLength + + if (args.Length() == 7) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const int32_t cache_id = args[1].As()->Value(); + if (cache_id != -1) { + if (const EVP_MD* md = + GetCachedMDByID(env, cache_id, args[2].As())) { + return OneShotDigestWithMD(env, args, md, nullptr); + } + } +#else + Utf8Value utf8(env->isolate(), args[0]); + return OneShotDigestWithMD( + env, args, ncrypto::getDigestByName(*utf8), nullptr); +#endif + } + + if (args.Length() == 9) { + CShakeOptions options; + if (GetCShakeOptions(args, 7, &options).IsNothing()) return; + + std::optional digest_owner; + const EVP_MD* md = + GetDigestImplementation(env, args[0], args[1], args[2], digest_owner); + return OneShotDigestWithMD(env, args, md, &options); + } + + std::optional digest_owner; + const EVP_MD* md = + GetDigestImplementation(env, args[0], args[1], args[2], digest_owner); + OneShotDigestWithMD(env, args, md, nullptr); +} + void Hash::Initialize(Environment* env, Local target) { Isolate* isolate = env->isolate(); Local context = env->context(); @@ -418,9 +600,11 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { #endif } -// new Hash(algorithm, algorithmId, xofLen, algorithmCache) +// new Hash(algorithm, xofLen, algorithmId, algorithmCache[, functionName, +// customization]) void Hash::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + CHECK(args.Length() == 4 || args.Length() == 6); Maybe xof_md_len = Nothing(); if (!args[1]->IsUndefined()) { @@ -428,20 +612,50 @@ void Hash::New(const FunctionCallbackInfo& args) { xof_md_len = Just(args[1].As()->Value()); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // This is the common path after the first lookup. Avoid constructing a + // digest owner when the Environment already owns the cached implementation. + if (args.Length() == 4 && args[0]->IsString()) { + CHECK(args[2]->IsInt32()); + const int32_t cache_id = args[2].As()->Value(); + if (cache_id != -1) { + if (const EVP_MD* md = + GetCachedMDByID(env, cache_id, args[3].As())) { + Hash* hash = new Hash(env, args.This()); + if (!hash->HashInit(md, xof_md_len)) { + return ThrowCryptoError( + env, ERR_get_error(), "Digest method not supported"); + } + return; + } + } + } +#endif + const Hash* orig = nullptr; + std::optional digest_owner; const EVP_MD* md = nullptr; if (args[0]->IsObject()) { ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As()); CHECK_NOT_NULL(orig); md = orig->mdctx_.getDigest(); } else { - md = GetDigestImplementation(env, args[0], args[2], args[3]); + md = GetDigestImplementation(env, args[0], args[2], args[3], digest_owner); } Hash* hash = new Hash(env, args.This()); - if (md == nullptr || !hash->HashInit(md, xof_md_len)) { - return ThrowCryptoError(env, ERR_get_error(), - "Digest method not supported"); + if (args.Length() == 4) { + if (md == nullptr || !hash->HashInit(md, xof_md_len)) { + return ThrowCryptoError( + env, ERR_get_error(), "Digest method not supported"); + } + } else { + CShakeOptions options; + if (GetCShakeOptions(args, 4, &options).IsNothing()) return; + if (md == nullptr || !hash->HashInit(md, options, xof_md_len)) { + return ThrowCryptoError( + env, ERR_get_error(), "Digest method not supported"); + } } if (orig != nullptr && !orig->mdctx_.copyTo(hash->mdctx_)) { @@ -449,17 +663,47 @@ void Hash::New(const FunctionCallbackInfo& args) { } } -bool Hash::HashInit(const EVP_MD* md, Maybe xof_md_len) { +bool Hash::HashInit(const EVP_MD* digest, Maybe xof_md_len) { mdctx_ = EVPMDCtxPointer::New(); - if (!mdctx_.digestInit(md)) [[unlikely]] { + if (!mdctx_.digestInit(digest)) [[unlikely]] { mdctx_.reset(); return false; } md_len_ = mdctx_.getDigestSize(); + if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() && + ShouldRejectMissingXofLength(digest, md_len_)) { + MarkInvalidXofLength(); + mdctx_.reset(); + return false; + } + + if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) { + // This is a little hack to cause createHash to fail when an incorrect + // hashSize option was passed for a non-XOF hash function. + if (!mdctx_.hasXofFlag()) [[unlikely]] { + MarkInvalidXofLength(); + mdctx_.reset(); + return false; + } + md_len_ = xof_md_len.FromJust(); + } + return true; +} + +bool Hash::HashInit(const EVP_MD* digest, + const CShakeOptions& options, + Maybe xof_md_len) { + mdctx_ = EVPMDCtxPointer::New(); + if (!options.Initialize(&mdctx_, digest)) [[unlikely]] { + mdctx_.reset(); + return false; + } + + md_len_ = mdctx_.getDigestSize(); if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() && - ShouldRejectMissingXofLength(md, md_len_)) { + ShouldRejectMissingXofLength(digest, md_len_)) { MarkInvalidXofLength(); mdctx_.reset(); return false; @@ -543,7 +787,10 @@ void Hash::HashDigest(const FunctionCallbackInfo& args) { } HashConfig::HashConfig(HashConfig&& other) noexcept - : in(std::move(other.in)), digest(other.digest), length(other.length) {} + : in(std::move(other.in)), + digest(other.digest), + options(std::move(other.options)), + length(other.length) {} HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { if (&other == this) return *this; @@ -553,6 +800,9 @@ HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { void HashConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TraitTrackInline(in, "in"); + if (options.has_value()) { + tracker->TrackField("options", *options); + } } MaybeLocal HashTraits::EncodeOutput(Environment* env, @@ -570,8 +820,8 @@ Maybe HashTraits::AdditionalConfig( CHECK(args[offset]->IsString()); // Hash algorithm Utf8Value digest(env->isolate(), args[offset]); - params->digest = ncrypto::getDigestByName(*digest); - if (params->digest == nullptr) [[unlikely]] { + params->digest = ncrypto::Digest::FromName(*digest); + if (!params->digest) [[unlikely]] { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", digest); return Nothing(); } @@ -583,7 +833,14 @@ Maybe HashTraits::AdditionalConfig( } params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - unsigned int expected = EVP_MD_size(params->digest); + if (static_cast(args.Length()) > offset + 3) { + params->options.emplace(); + if (GetCShakeOptions(args, offset + 3, &*params->options).IsNothing()) { + return Nothing(); + } + } + + unsigned int expected = EVP_MD_size(params->digest.get()); params->length = expected; if (args[offset + 2]->IsUint32()) [[unlikely]] { // length is expressed in terms of bits @@ -591,7 +848,8 @@ Maybe HashTraits::AdditionalConfig( static_cast(args[offset + 2].As()->Value()) / CHAR_BIT; if (params->length != expected) { - if ((EVP_MD_flags(params->digest) & EVP_MD_FLAG_XOF) == 0) [[unlikely]] { + if ((EVP_MD_flags(params->digest.get()) & EVP_MD_FLAG_XOF) == 0) + [[unlikely]] { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Digest method not supported"); return Nothing(); } @@ -608,8 +866,11 @@ bool HashTraits::DeriveBits(Environment* env, CryptoErrorStore* errors) { auto ctx = EVPMDCtxPointer::New(); - if (!ctx.digestInit(params.digest) || !ctx.digestUpdate(params.in)) - [[unlikely]] { + const bool initialized = + params.options.has_value() + ? params.options->Initialize(&ctx, params.digest.get()) + : ctx.digestInit(params.digest.get()); + if (!initialized || !ctx.digestUpdate(params.in)) [[unlikely]] { return false; } diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 3ae6a16c2579..146e9cf55b52 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -21,7 +21,10 @@ class Hash final : public BaseObject { SET_MEMORY_INFO_NAME(Hash) SET_SELF_SIZE(Hash) - bool HashInit(const EVP_MD* md, v8::Maybe xof_md_len); + bool HashInit(const EVP_MD* digest, v8::Maybe xof_md_len); + bool HashInit(const EVP_MD* digest, + const CShakeOptions& options, + v8::Maybe xof_md_len); bool HashUpdate(const char* data, size_t len); static void GetHashes(const v8::FunctionCallbackInfo& args); @@ -43,7 +46,8 @@ class Hash final : public BaseObject { struct HashConfig final : public MemoryRetainer { ByteSource in; - const EVP_MD* digest; + ncrypto::Digest digest; + std::optional options; unsigned int length; HashConfig() = default; diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index 15479284933d..e80c70c961df 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -38,6 +38,21 @@ using v8::Uint32; using v8::Value; namespace crypto { +namespace { +bool IsRsaPssDigestEncodable(const Digest& digest) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const int nid = EVP_MD_type(digest.get()); + if (nid == NID_undef) return false; + + const ASN1_OBJECT* object = OBJ_nid2obj(nid); + return object != nullptr && OBJ_length(object) > 0; +#else + static_cast(digest); + return true; +#endif +} +} // namespace + EVPKeyCtxPointer RsaKeyGenTraits::Setup(RsaKeyPairGenConfig* params) { auto ctx = EVPKeyCtxPointer::NewFromID( params->params.variant == kKeyVariantRSA_PSS ? EVP_PKEY_RSA_PSS @@ -58,7 +73,8 @@ EVPKeyCtxPointer RsaKeyGenTraits::Setup(RsaKeyPairGenConfig* params) { } if (params->params.variant == kKeyVariantRSA_PSS) { - if (params->params.md && !ctx.setRsaPssKeygenMd(params->params.md)) { + if (params->params.md && (!IsRsaPssDigestEncodable(params->params.md) || + !ctx.setRsaPssKeygenMd(params->params.md))) { return {}; } @@ -71,7 +87,8 @@ EVPKeyCtxPointer RsaKeyGenTraits::Setup(RsaKeyPairGenConfig* params) { mgf1_md = params->params.md; } - if (mgf1_md && !ctx.setRsaPssKeygenMgf1Md(mgf1_md)) { + if (mgf1_md && (!IsRsaPssDigestEncodable(mgf1_md) || + !ctx.setRsaPssKeygenMgf1Md(mgf1_md))) { return {}; } diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 133a5c7f7f1d..ff0026ab7bcf 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -18,6 +18,11 @@ #include "openssl/provider.h" #endif +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0) +#include +#include +#endif + namespace node { using ncrypto::BignumPointer; @@ -76,6 +81,108 @@ size_t MemoryRetainerTraits::SelfSize( namespace crypto { +CShakeOptions::CShakeOptions(CShakeOptions&& other) noexcept + : function_name(std::move(other.function_name)), + customization(std::move(other.customization)), + flags(other.flags) {} + +CShakeOptions& CShakeOptions::operator=(CShakeOptions&& other) noexcept { + if (&other == this) return *this; + this->~CShakeOptions(); + return *new (this) CShakeOptions(std::move(other)); +} + +void CShakeOptions::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("function_name", function_name.size()); + tracker->TrackFieldWithSize("customization", customization.size()); +} + +bool CShakeOptions::Initialize(ncrypto::EVPMDCtxPointer* ctx, + const EVP_MD* digest) const { + if (!ctx || !*ctx || digest == nullptr) return false; + if (empty()) return ctx->digestInit(digest); + +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0) + const bool is_cshake = + EVP_MD_is_a(digest, "CSHAKE-128") || EVP_MD_is_a(digest, "CSHAKE-256"); + if (!is_cshake) return false; + + OSSL_PARAM params[3]; + size_t count = 0; + if (has(kFunctionName)) { + params[count++] = OSSL_PARAM_construct_utf8_string( + OSSL_DIGEST_PARAM_FUNCTION_NAME, + const_cast(function_name.c_str()), + function_name.size()); + } + if (has(kCustomization)) { + params[count++] = OSSL_PARAM_construct_utf8_string( + OSSL_DIGEST_PARAM_CUSTOMIZATION, + const_cast(customization.c_str()), + customization.size()); + } + params[count] = OSSL_PARAM_construct_end(); + return ctx->digestInit(digest, params); +#else + return false; +#endif +} + +namespace { +bool ContainsNullByte(std::string_view value) { + return value.find('\0') != std::string_view::npos; +} + +v8::Maybe GetDigestStringOption( + Environment* env, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + CShakeOptions::Flag flag, + std::string* target, + CShakeOptions* options) { + if (args[offset]->IsUndefined()) return v8::JustVoid(); + CHECK(IsAnyBufferSource(args[offset])); + ArrayBufferOrViewContents value(args[offset]); + if (!value.CheckSizeInt32()) { + THROW_ERR_OUT_OF_RANGE(env, "digest option is too big"); + return v8::Nothing(); + } + target->assign(value.data(), value.size()); + if (ContainsNullByte(*target)) { + THROW_ERR_INVALID_ARG_VALUE(env, + "Digest options must not contain null bytes"); + return v8::Nothing(); + } + options->flags |= flag; + return v8::JustVoid(); +} +} // namespace + +v8::Maybe GetCShakeOptions( + const v8::FunctionCallbackInfo& args, + unsigned int offset, + CShakeOptions* options) { + Environment* env = Environment::GetCurrent(args); + if (GetDigestStringOption(env, + args, + offset, + CShakeOptions::kFunctionName, + &options->function_name, + options) + .IsNothing() || + GetDigestStringOption(env, + args, + offset + 1, + CShakeOptions::kCustomization, + &options->customization, + options) + .IsNothing()) { + return v8::Nothing(); + } + + return v8::JustVoid(); +} + int PasswordCallback(char* buf, int size, int rwflag, void* u) { const ByteSource* passphrase = *static_cast(u); if (passphrase != nullptr) { @@ -231,6 +338,11 @@ void GetFipsCrypto(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(IsFipsEnabled() ? 1 : 0); } +void GetFipsCryptoGeneration(const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(BigInt::NewFromUnsigned( + args.GetIsolate(), ncrypto::getFipsStateGeneration())); +} + void SetFipsCrypto(const FunctionCallbackInfo& args) { Mutex::ScopedLock lock(per_process::cli_options_mutex); Mutex::ScopedLock fips_lock(fips_mutex); @@ -891,6 +1003,8 @@ void Initialize(Environment* env, Local target) { #endif // !OPENSSL_NO_ENGINE SetMethodNoSideEffect(context, target, "getFipsCrypto", GetFipsCrypto); + SetMethodNoSideEffect( + context, target, "getFipsCryptoGeneration", GetFipsCryptoGeneration); SetMethod(context, target, "setFipsCrypto", SetFipsCrypto); SetMethodNoSideEffect(context, target, "testFipsCrypto", TestFipsCrypto); @@ -910,6 +1024,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { #endif // !OPENSSL_NO_ENGINE registry->Register(GetFipsCrypto); + registry->Register(GetFipsCryptoGeneration); registry->Register(SetFipsCrypto); registry->Register(TestFipsCrypto); registry->Register(SecureBuffer); diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index c74a6e7fd507..62ae32d277d9 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -286,6 +286,35 @@ enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync, kCryptoJobWebCrypto }; CryptoJobMode GetCryptoJobMode(v8::Local args); bool IsCryptoJobAsync(CryptoJobMode mode); +struct CShakeOptions final : public MemoryRetainer { + enum Flag : uint8_t { + kFunctionName = 1 << 0, + kCustomization = 1 << 1, + }; + + std::string function_name; + std::string customization; + uint8_t flags = 0; + + CShakeOptions() = default; + CShakeOptions(CShakeOptions&& other) noexcept; + CShakeOptions& operator=(CShakeOptions&& other) noexcept; + + bool empty() const { return flags == 0; } + bool has(Flag flag) const { return (flags & flag) != 0; } + + bool Initialize(ncrypto::EVPMDCtxPointer* ctx, const EVP_MD* digest) const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CShakeOptions) + SET_SELF_SIZE(CShakeOptions) +}; + +v8::Maybe GetCShakeOptions( + const v8::FunctionCallbackInfo& args, + unsigned int offset, + CShakeOptions* options); + v8::MaybeLocal CreateWebCryptoJobError(Environment* env, v8::Local cause); diff --git a/src/env.cc b/src/env.cc index eae89a4b758b..aec3509ca761 100644 --- a/src/env.cc +++ b/src/env.cc @@ -16,6 +16,9 @@ #include "node_snapshotable.h" #include "node_v8_platform-inl.h" #include "node_worker.h" +#if HAVE_OPENSSL +#include "ncrypto.h" +#endif #include "req_wrap-inl.h" #include "stream_base.h" #include "tracing/agent.h" @@ -865,6 +868,10 @@ Environment::Environment(IsolateData* isolate_data, ? AllocateEnvironmentThreadId().id : thread_id.id), thread_name_(thread_name) { +#if HAVE_OPENSSL && NCRYPTO_USE_OPENSSL3_PROVIDER + provider_digest_cache = std::make_unique(); +#endif + if (!is_main_thread()) { // If this is a Worker thread, we can always safely use the parent's // Isolate's code cache because of the shared read-only heap. @@ -1116,6 +1123,11 @@ Environment::~Environment() { // Also, since the main thread usually stops just before the process exits, // this is far less relevant here. if (!is_main_thread()) { +#if HAVE_OPENSSL + // Provider methods can contain callbacks into native addons. Release the + // environment-owned methods before unloading any addon DSOs. + provider_digest_cache.reset(); +#endif // Dereference all addons that were loaded into this environment. for (binding::DLib& addon : loaded_addons_) { addon.Close(); diff --git a/src/env.h b/src/env.h index 3f89184f62fb..b57e3a913f15 100644 --- a/src/env.h +++ b/src/env.h @@ -53,10 +53,6 @@ #include "v8-profiler.h" #include "v8.h" -#if HAVE_OPENSSL -#include -#endif - #include #include #include @@ -72,6 +68,10 @@ #include #include +namespace ncrypto { +class DigestCache; +} // namespace ncrypto + namespace node { namespace shadow_realm { @@ -1091,12 +1091,8 @@ class Environment final : public MemoryRetainer { }; #if HAVE_OPENSSL -#if OPENSSL_VERSION_MAJOR >= 3 - // We declare another alias here to avoid having to include crypto_util.h - using EVPMDPointer = DeleteFnPtr; - std::vector evp_md_cache; -#endif // OPENSSL_VERSION_MAJOR >= 3 - std::unordered_map alias_to_md_id_map; + uint64_t hash_cache_generation = 0; + std::unique_ptr provider_digest_cache; std::vector supported_hash_algorithms; #endif // HAVE_OPENSSL diff --git a/test/addons/addons.status b/test/addons/addons.status index 18b1c2b2157d..60a33ec9acc6 100644 --- a/test/addons/addons.status +++ b/test/addons/addons.status @@ -14,6 +14,7 @@ openssl-binding/test: PASS,FLAKY openssl-binding/test: SKIP openssl-get-ssl-ctx/test: SKIP openssl-providers/test-default-only-config: SKIP +openssl-providers/test-default-properties-config: SKIP openssl-providers/test-legacy-provider-config: SKIP openssl-providers/test-legacy-provider-inactive-config: SKIP openssl-providers/test-legacy-provider-option: SKIP diff --git a/test/addons/openssl-providers/providers.cjs b/test/addons/openssl-providers/providers.cjs index efa1019c62d9..861439c66923 100644 --- a/test/addons/openssl-providers/providers.cjs +++ b/test/addons/openssl-providers/providers.cjs @@ -21,7 +21,11 @@ const { getProviders } = require(`./build/${common.buildType}/binding`); const providers = { 'default': { ciphers: ['des3-wrap'], - hashes: ['sha512-256'], + hashes: [ + 'sha512-256', + ...['keccak-kmac-128', 'keccak-kmac128'] + .filter((name) => getHashes().includes(name)), + ], }, 'legacy': { ciphers: ['blowfish', 'idea'], @@ -47,6 +51,15 @@ function assertArrayIncludes(array, item, desc) { `${desc} [${array}] does not include "${item}"`); } +function createSupportedHash(hash) { + try { + return createHash(hash); + } catch (err) { + if (err?.code !== 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH') throw err; + return createHash(hash, { outputLength: 32 }); + } +} + function testProviderPresent(provider) { debug(`Checking '${provider}' is present`); assertArrayIncludes(getProviders(), provider, 'Loaded providers'); @@ -57,7 +70,7 @@ function testProviderPresent(provider) { for (const hash of providers[provider].hashes || []) { debug(`Checking '${hash}' hash is available`); assertArrayIncludes(getHashes(), hash, 'Available hashes'); - createHash(hash); + createSupportedHash(hash); } } diff --git a/test/addons/openssl-providers/test-default-properties-config.js b/test/addons/openssl-providers/test-default-properties-config.js new file mode 100644 index 000000000000..056e55bf736b --- /dev/null +++ b/test/addons/openssl-providers/test-default-properties-config.js @@ -0,0 +1,129 @@ +'use strict'; + +const common = require('../../common'); +const fixtures = require('../../common/fixtures'); +const providers = require('./providers.cjs'); + +const assert = require('node:assert'); +const { fork } = require('node:child_process'); +const { + createHash, + getHashes, + hash: oneShotHash, + setFips, +} = require('node:crypto'); +const { Worker } = require('node:worker_threads'); +const option = `--openssl-config=${fixtures.path( + 'openssl3-conf', + 'default_properties.cnf', +)}`; + +if (!process.execArgv.includes(option)) { + const cp = fork(__filename, { execArgv: [option] }); + cp.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + })); + return; +} + +assert(providers.getCurrentProviders().includes('default')); +assert(providers.getCurrentProviders().includes('legacy')); +providers.testProviderPresent('default'); + +const hashes = getHashes(); +const input = Buffer.alloc(0); +const md5 = 'd41d8cd98f00b204e9800998ecf8427e'; +assert.strictEqual(createHash('md5').update(input).digest('hex'), md5); +assert.strictEqual(oneShotHash('md5', input), md5); + +setFips(true); +assert.deepStrictEqual(getHashes(), []); +assert.throws( + () => createHash('md5'), + { code: 'ERR_OSSL_EVP_UNSUPPORTED' }, +); +assert.throws( + () => oneShotHash('md5', input), + { code: 'ERR_OSSL_EVP_UNSUPPORTED' }, +); + +setFips(false); +assert.deepStrictEqual(getHashes(), hashes); +assert.strictEqual(createHash('md5').update(input).digest('hex'), md5); +assert.strictEqual(oneShotHash('md5', input), md5); + +for (const hash of ['md4', 'whirlpool']) { + assert(!hashes.includes(hash)); + assert.throws(() => createHash(hash), { code: 'ERR_OSSL_EVP_UNSUPPORTED' }); +} + +const worker = new Worker(` + 'use strict'; + const { + createHash, + getHashes, + hash, + } = require('node:crypto'); + const { parentPort } = require('node:worker_threads'); + + const input = Buffer.alloc(0); + const hashes = getHashes(); + const liveHash = createHash('md5').update(input); + hash('md5', input); + parentPort.postMessage({ phase: 'warm' }); + + function getErrorCode(fn) { + try { + fn(); + } catch (err) { + return err.code; + } + } + + parentPort.on('message', (phase) => { + if (phase === 'fips-on') { + parentPort.postMessage({ + phase, + createHashError: getErrorCode(() => createHash('md5')), + oneShotHashError: getErrorCode(() => hash('md5', input)), + hashes: getHashes(), + liveDigest: liveHash.digest('hex'), + }); + } else { + parentPort.postMessage({ + phase, + createHashDigest: createHash('md5').update(input).digest('hex'), + oneShotHashDigest: hash('md5', input), + hashes: getHashes(), + }); + parentPort.close(); + } + }); +`, { eval: true }); + +worker.once('message', common.mustCall((message) => { + assert.strictEqual(message.phase, 'warm'); + setFips(true); + + worker.once('message', common.mustCall((message) => { + assert.strictEqual(message.phase, 'fips-on'); + assert.strictEqual(message.createHashError, 'ERR_OSSL_EVP_UNSUPPORTED'); + assert.strictEqual(message.oneShotHashError, 'ERR_OSSL_EVP_UNSUPPORTED'); + assert.deepStrictEqual(message.hashes, []); + assert.strictEqual(message.liveDigest, md5); + + setFips(false); + + worker.once('message', common.mustCall((message) => { + assert.strictEqual(message.phase, 'fips-off'); + assert.strictEqual(message.createHashDigest, md5); + assert.strictEqual(message.oneShotHashDigest, md5); + assert.deepStrictEqual(message.hashes, hashes); + })); + worker.postMessage('fips-off'); + })); + worker.postMessage('fips-on'); +})); +worker.on('error', common.mustNotCall()); +worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0))); diff --git a/test/fixtures/openssl3-conf/default_properties.cnf b/test/fixtures/openssl3-conf/default_properties.cnf new file mode 100644 index 000000000000..bb26de636b2c --- /dev/null +++ b/test/fixtures/openssl3-conf/default_properties.cnf @@ -0,0 +1,19 @@ +nodejs_conf = nodejs_init + +[nodejs_init] +providers = provider_sect +alg_section = algorithm_sect + +# Load both providers but select only implementations from the default provider. +[provider_sect] +default = default_sect +legacy = legacy_sect + +[default_sect] +activate = 1 + +[legacy_sect] +activate = 1 + +[algorithm_sect] +default_properties = provider=default diff --git a/test/parallel/test-crypto-provider-hash-options.js b/test/parallel/test-crypto-provider-hash-options.js new file mode 100644 index 000000000000..609d00d7f7b0 --- /dev/null +++ b/test/parallel/test-crypto-provider-hash-options.js @@ -0,0 +1,387 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +if (Number(process.versions.openssl.split('.')[0]) < 4 || + process.features.openssl_is_boringssl) { + common.skip('OpenSSL 4 provider support is required'); +} + +const assert = require('node:assert'); +const { + createHash, + getHashes, + hash, +} = require('node:crypto'); +const { internalBinding } = require('internal/test/binding'); +const { + HashJob, + kCryptoJobSync, + kCryptoJobWebCrypto, +} = internalBinding('crypto'); + +const hashes = getHashes(); +const hashNames = new Map( + hashes.map((name) => [name.toLowerCase(), name]), +); + +let exercised = false; + +function findHash(...names) { + for (const name of names) { + const result = hashNames.get(name); + if (result !== undefined) return result; + } + return undefined; +} + +function testHashJob(args, expected) { + const { 0: err, 1: result } = new HashJob( + kCryptoJobSync, + ...args, + ).run(); + assert.strictEqual(err, undefined); + assert.deepStrictEqual(Buffer.from(result), expected); + + (async () => { + const asyncResult = await new HashJob( + kCryptoJobWebCrypto, + ...args, + ).run(); + assert.deepStrictEqual(Buffer.from(asyncResult), expected); + })().then(common.mustCall()); +} + +const cshakeVectors = [ + { + names: ['cshake-128', 'cshake128'], + shakeNames: ['shake128', 'shake-128'], + outputLength: 32, + input: Buffer.from('00010203', 'hex'), + expected: 'c1c36925b6409a04f1b504fcbca9d82b' + + '4017277cb5ed2b2065fc1d3814d5aaf5', + }, + { + names: ['cshake-256', 'cshake256'], + shakeNames: ['shake256', 'shake-256'], + outputLength: 64, + input: Buffer.from('00010203', 'hex'), + expected: 'd008828e2b80ac9d2218ffee1d070c48' + + 'b8e4c87bff32c9699d5b6896eee0edd1' + + '64020e2be0560858d9c00c037e34a96' + + '937c561a74c412bb4c746469527281c8c', + }, +]; + +for (const vector of cshakeVectors) { + const algorithm = findHash(...vector.names); + if (algorithm === undefined) { + common.printSkipMessage(`${vector.names[0]} is not available`); + continue; + } + + exercised = true; + + const options = { + outputLength: vector.outputLength, + customization: 'Email Signature', + }; + const streaming = createHash(algorithm, options) + .update(vector.input.subarray(0, 2)) + .update(vector.input.subarray(2)) + .digest('hex'); + const partial = createHash(algorithm, options) + .update(vector.input.subarray(0, 2)); + const copyOptionReads = []; + const copied = partial.copy({ + get outputLength() { + copyOptionReads.push('outputLength'); + return vector.outputLength; + }, + get functionName() { + copyOptionReads.push('functionName'); + return undefined; + }, + get customization() { + copyOptionReads.push('customization'); + return undefined; + }, + }) + .update(vector.input.subarray(2)) + .digest('hex'); + + assert.strictEqual(streaming, vector.expected); + assert.strictEqual(copied, vector.expected); + assert.deepStrictEqual(copyOptionReads, ['outputLength']); + assert.strictEqual(hash(algorithm, vector.input, options), vector.expected); + + // BufferSource parameters have the same semantics as their string form. + const bufferOptions = { + ...options, + customization: Buffer.from(options.customization), + }; + assert.strictEqual( + createHash(algorithm, bufferOptions).update(vector.input).digest('hex'), + vector.expected, + ); + assert.strictEqual( + hash(algorithm, vector.input, bufferOptions), + vector.expected, + ); + + // Without function-name and customization parameters, cSHAKE is SHAKE. + const withoutParameters = createHash(algorithm) + .update(vector.input) + .digest('hex'); + assert.strictEqual(hash(algorithm, vector.input), withoutParameters); + + // Explicit undefined parameters have the same semantics as omitted ones. + const undefinedOptions = { + outputLength: vector.outputLength, + functionName: undefined, + customization: undefined, + }; + assert.strictEqual( + createHash(algorithm, undefinedOptions).update(vector.input).digest('hex'), + withoutParameters, + ); + assert.strictEqual( + hash(algorithm, vector.input, undefinedOptions), + withoutParameters, + ); + + // Empty BufferSource parameters are still supplied to OpenSSL, but cSHAKE + // with two empty parameters is equivalent to SHAKE. + const emptyOptions = { + outputLength: vector.outputLength, + functionName: Buffer.alloc(0), + customization: new Uint8Array(0), + }; + assert.strictEqual( + createHash(algorithm, emptyOptions).update(vector.input).digest('hex'), + withoutParameters, + ); + assert.strictEqual( + hash(algorithm, vector.input, emptyOptions), + withoutParameters, + ); + const emptyFunctionNameOptions = { + outputLength: vector.outputLength, + functionName: Buffer.alloc(0), + }; + assert.strictEqual( + createHash(algorithm, emptyFunctionNameOptions) + .update(vector.input) + .digest('hex'), + withoutParameters, + ); + assert.strictEqual( + hash(algorithm, vector.input, emptyFunctionNameOptions), + withoutParameters, + ); + + const createHashOptionReads = []; + assert.strictEqual( + createHash(algorithm, { + get outputLength() { + createHashOptionReads.push('outputLength'); + return vector.outputLength; + }, + get functionName() { + createHashOptionReads.push('functionName'); + return undefined; + }, + get customization() { + createHashOptionReads.push('customization'); + return undefined; + }, + }).update(vector.input).digest('hex'), + withoutParameters, + ); + assert.deepStrictEqual( + createHashOptionReads, + ['outputLength', 'functionName', 'customization'], + ); + + const hashOptionReads = []; + assert.strictEqual( + hash(algorithm, vector.input, { + get outputLength() { + hashOptionReads.push('outputLength'); + return vector.outputLength; + }, + get outputEncoding() { + hashOptionReads.push('outputEncoding'); + return 'hex'; + }, + get functionName() { + hashOptionReads.push('functionName'); + return undefined; + }, + get customization() { + hashOptionReads.push('customization'); + return undefined; + }, + }), + withoutParameters, + ); + assert.deepStrictEqual( + hashOptionReads, + ['outputLength', 'outputEncoding', 'functionName', 'customization'], + ); + + for (const zeroLengthOptions of [ + { outputLength: 0 }, + { + outputLength: 0, + functionName: Buffer.alloc(0), + customization: new Uint8Array(0), + }, + ]) { + assert.deepStrictEqual( + createHash(algorithm, zeroLengthOptions).update(vector.input).digest(), + Buffer.alloc(0), + ); + assert.strictEqual( + hash(algorithm, vector.input, zeroLengthOptions), + '', + ); + } + + const shake = findHash(...vector.shakeNames); + if (shake !== undefined) { + assert.strictEqual( + withoutParameters, + createHash(shake, { outputLength: vector.outputLength }) + .update(vector.input) + .digest('hex'), + ); + } + + const namedOptions = { + outputLength: vector.outputLength, + functionName: 'KMAC', + customization: 'Node.js', + }; + let namedResult; + try { + namedResult = createHash(algorithm, namedOptions) + .update(vector.input) + .digest(); + } catch { + common.printSkipMessage( + `${algorithm} does not support the KMAC function name`, + ); + } + if (namedResult !== undefined) { + assert.deepStrictEqual( + hash(algorithm, vector.input, { + ...namedOptions, + outputEncoding: 'buffer', + }), + namedResult, + ); + assert.deepStrictEqual( + createHash(algorithm, { + ...namedOptions, + functionName: Buffer.from(namedOptions.functionName), + customization: new Uint8Array(Buffer.from(namedOptions.customization)), + }).update(vector.input).digest(), + namedResult, + ); + assert.deepStrictEqual( + hash(algorithm, vector.input, { + ...namedOptions, + functionName: Buffer.from(namedOptions.functionName), + customization: new Uint8Array(Buffer.from(namedOptions.customization)), + outputEncoding: 'buffer', + }), + namedResult, + ); + } + + for (const functionName of ['', 'TupleHash', 'ParallelHash', 'KMAC']) { + const functionOptions = { + outputLength: vector.outputLength, + functionName, + }; + let functionResult; + try { + functionResult = createHash(algorithm, functionOptions) + .update(vector.input) + .digest(); + } catch { + common.printSkipMessage( + `${algorithm} does not support the ${functionName} function name`, + ); + continue; + } + assert.deepStrictEqual( + functionResult, + hash(algorithm, vector.input, { + ...functionOptions, + outputEncoding: 'buffer', + }), + ); + } + + testHashJob([ + algorithm, + vector.input, + vector.outputLength * 8, + undefined, + Buffer.from(options.customization), + ], Buffer.from(vector.expected, 'hex')); + + for (const invalidOptions of [ + { functionName: 1 }, + { customization: {} }, + ]) { + const expected = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => createHash(algorithm, invalidOptions), expected); + assert.throws( + () => hash(algorithm, vector.input, invalidOptions), + expected, + ); + } + + for (const invalidOptions of [ + { functionName: 'KMAC\0' }, + { customization: 'Node\0js' }, + { customization: Buffer.from([0x61, 0x00, 0x62]) }, + ]) { + const expected = { code: 'ERR_INVALID_ARG_VALUE' }; + assert.throws(() => createHash(algorithm, invalidOptions), expected); + assert.throws( + () => hash(algorithm, vector.input, invalidOptions), + expected, + ); + } +} + +if (cshakeVectors.some(({ names }) => findHash(...names) !== undefined)) { + for (const mismatchedOptions of [ + { functionName: 'KMAC' }, + { customization: 'Node.js' }, + { functionName: Buffer.alloc(0) }, + { customization: new Uint8Array(0) }, + ]) { + assert.throws( + () => createHash('sha256', mismatchedOptions), + { message: 'Digest method not supported' }, + ); + assert.throws( + () => hash('sha256', Buffer.from('abc'), mismatchedOptions), + { message: 'Digest options are not supported' }, + ); + } +} + +if (!exercised) { + common.printSkipMessage('cSHAKE is not available'); +} diff --git a/test/parallel/test-crypto-provider-hashes.js b/test/parallel/test-crypto-provider-hashes.js new file mode 100644 index 000000000000..166efaa0d7fd --- /dev/null +++ b/test/parallel/test-crypto-provider-hashes.js @@ -0,0 +1,258 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const assert = require('node:assert'); +const { + createHash, + createHmac, + createSign, + createVerify, + generateKeyPair, + generateKeyPairSync, + getHashes, + hash, + hkdf, + hkdfSync, + pbkdf2, + pbkdf2Sync, + privateDecrypt, + publicEncrypt, + sign, + verify, +} = require('node:crypto'); +const { hasOpenSSL3 } = require('../common/crypto'); + +if (!hasOpenSSL3 || process.features.openssl_is_boringssl) { + common.skip('OpenSSL 3 provider support is required'); +} + +const { internalBinding } = require('internal/test/binding'); +const { + HashJob, + kCryptoJobSync, + kCryptoJobWebCrypto, +} = internalBinding('crypto'); + +const hashes = getHashes(); +const lowercaseHashes = hashes.map((name) => name.toLowerCase()); +const modifiedHashes = getHashes(); +modifiedHashes.length = 0; + +assert.deepStrictEqual(hashes, [...hashes].sort()); +assert.deepStrictEqual(getHashes(), hashes); +assert.strictEqual(new Set(lowercaseHashes).size, hashes.length); +if (lowercaseHashes.includes('sha1')) { + assert(hashes.includes('RSA-SHA1')); +} +assert(!lowercaseHashes.includes('null')); +assert(!lowercaseHashes.includes('ml-dsa-mu')); +assert(!hashes.some((name) => /^\d+(?:\.\d+)+$/.test(name))); + +for (const name of hashes) { + try { + createHash(name); + } catch (err) { + assert.strictEqual(err.code, 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH'); + createHash(name, { outputLength: 32 }); + } +} + +const input = Buffer.alloc(0); +assert.throws( + () => createHash('ml-dsa-mu'), + /Digest method not supported/, +); +assert.throws( + () => hash('ml-dsa-mu', input), + { message: 'Digest method ml-dsa-mu is not supported' }, +); + +const providerVectors = { + 'keccak-kmac-128': { + aliases: ['keccak-kmac-128', 'keccak-kmac128'], + expected: '83aa04c211dc19d16912571ed0a75130' + + 'd36aebd58562dd080c1ea84a8c7d73f7', + options: { outputLength: 32 }, + }, + 'keccak-256': { + aliases: ['keccak-256'], + expected: 'c5d2460186f7233c927e7db2dcc703c0' + + 'e500b653ca82273b7bfad8045d85a470', + }, + 'sha256-192': { + aliases: ['sha2-256/192', 'sha-256/192', 'sha256-192'], + expected: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c', + }, +}; + +function testHashVector({ aliases, expected, options }) { + for (const alias of aliases) { + if (!hashes.includes(alias)) continue; + assert(!hashes.includes(alias.toUpperCase())); + + for (const name of [alias, alias.toUpperCase()]) { + const streaming = createHash(name, options).update(input).digest('hex'); + assert.strictEqual(streaming, expected); + assert.strictEqual(hash(name, input, options), expected); + } + } +} + +// These digests are tested when the active provider advertises them. +for (const name of ['keccak-kmac-128', 'keccak-256', 'sha256-192']) { + const vector = providerVectors[name]; + if (vector.aliases.some((alias) => hashes.includes(alias))) { + testHashVector(vector); + } else { + common.printSkipMessage(`${name} is not available from the active provider`); + } +} + +const keccakKmacName = providerVectors['keccak-kmac-128'].aliases + .find((alias) => hashes.includes(alias)); +if (keccakKmacName !== undefined) { + (async () => { + const { expected } = providerVectors['keccak-kmac-128']; + const { 0: err, 1: syncResult } = new HashJob( + kCryptoJobSync, + keccakKmacName, + input, + 256, + ).run(); + assert.strictEqual(err, undefined); + assert.strictEqual(Buffer.from(syncResult).toString('hex'), expected); + + const result = await new HashJob( + kCryptoJobWebCrypto, + keccakKmacName, + input, + 256, + ).run(); + assert.strictEqual(Buffer.from(result).toString('hex'), expected); + })().then(common.mustCall()); +} + +if (hashes.includes('sha256-192')) { + const operationInput = Buffer.from('abc'); + + assert.strictEqual( + createHmac('sha256-192', 'key').update(operationInput).digest('hex'), + 'd7774e586190fa2d2f4d4be4bc86ccd459a9170d52c38809', + ); + + const hkdfExpected = 'ef23757b94b5e1e46c3f981d87828d7aeb0207733ab5c78' + + 'c60df321c9e8c88e0ad54b4eecfef8c258ccd'; + assert.strictEqual( + Buffer.from(hkdfSync('sha256-192', 'key', 'salt', 'info', 42)) + .toString('hex'), + hkdfExpected, + ); + hkdf( + 'sha256-192', + 'key', + 'salt', + 'info', + 42, + common.mustSucceed((result) => { + assert.strictEqual(Buffer.from(result).toString('hex'), hkdfExpected); + }), + ); + + const pbkdf2Expected = '1fee3dd5ea13d5b563d3cc88fbc6dcf7' + + '3497aeffc3b3e6358ab3d3d1aa2aa0ee'; + assert.strictEqual( + pbkdf2Sync('password', 'salt', 2, 32, 'sha256-192').toString('hex'), + pbkdf2Expected, + ); + pbkdf2( + 'password', + 'salt', + 2, + 32, + 'sha256-192', + common.mustSucceed((result) => { + assert.strictEqual(result.toString('hex'), pbkdf2Expected); + }), + ); + + const { privateKey: ecPrivateKey, publicKey: ecPublicKey } = + generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); + const signature = sign('sha256-192', operationInput, ecPrivateKey); + assert(verify('sha256-192', operationInput, ecPublicKey, signature)); + + const streamingSignature = createSign('sha256-192') + .update(operationInput) + .sign(ecPrivateKey); + const verifier = createVerify('sha256-192'); + verifier.update(operationInput); + assert(verifier.verify(ecPublicKey, streamingSignature)); + + sign( + 'sha256-192', + operationInput, + ecPrivateKey, + common.mustSucceed((asyncSignature) => { + verify( + 'sha256-192', + operationInput, + ecPublicKey, + asyncSignature, + common.mustSucceed((result) => assert(result)), + ); + }), + ); + + const { privateKey: rsaPrivateKey, publicKey: rsaPublicKey } = + generateKeyPairSync('rsa', { modulusLength: 2048 }); + const plaintext = Buffer.from('provider digest'); + + assert.throws( + () => sign('sha256-192', plaintext, rsaPrivateKey), + { code: 'ERR_OSSL_DIGEST_NOT_ALLOWED' }, + ); + assert.deepStrictEqual( + privateDecrypt( + { key: rsaPrivateKey, oaepHash: 'sha256-192' }, + publicEncrypt( + { key: rsaPublicKey, oaepHash: 'sha256-192' }, + plaintext, + ), + ), + plaintext, + ); + + const pssOptions = [ + { + hashAlgorithm: 'sha256-192', + modulusLength: 2048, + }, + { + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha256-192', + modulusLength: 2048, + }, + ]; + const keyGenerationFailed = { message: 'Key generation job failed' }; + + for (const options of pssOptions) { + assert.throws( + () => generateKeyPairSync('rsa-pss', options), + keyGenerationFailed, + ); + generateKeyPair( + 'rsa-pss', + options, + common.mustCall((err, publicKey, privateKey) => { + assert.strictEqual(err?.message, keyGenerationFailed.message); + assert.strictEqual(publicKey, undefined); + assert.strictEqual(privateKey, undefined); + }), + ); + } +} diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index f532cf6a0c75..3e98a60517ba 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -1,6 +1,8 @@ declare namespace InternalCryptoBinding { type Buffer = Uint8Array; - type ByteSource = string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView; + type BufferSource = ArrayBuffer | SharedArrayBuffer | ArrayBufferView; + type OptionalBufferSource = BufferSource | undefined; + type ByteSource = string | BufferSource; type OptionalByteSource = ByteSource | undefined; type JwkKey = Record; type KeyFormatDER = 0; @@ -300,6 +302,8 @@ declare namespace InternalCryptoBinding { algorithm: string, data: ByteSource, outputLength?: number, + functionName?: OptionalBufferSource, + customization?: OptionalBufferSource, ): CryptoJobForMode; } @@ -818,6 +822,8 @@ export interface CryptoBinding { xofLen?: number, algorithmId?: number, algorithmCache?: Record, + functionName?: InternalCryptoBinding.OptionalBufferSource, + customization?: InternalCryptoBinding.OptionalBufferSource, ) => InternalCryptoBinding.HashHandle; Hmac: new () => InternalCryptoBinding.HmacHandle; KeyObjectHandle: new () => InternalCryptoBinding.KeyObjectHandle; @@ -939,6 +945,7 @@ export interface CryptoBinding { getCurves(): string[]; getExtraCACertificates(): string[]; getFipsCrypto(): 0 | 1; + getFipsCryptoGeneration(): bigint; getHashes(): string[]; getKeyObjectSlots(key: object): InternalCryptoBinding.KeyObjectSlots; getOpenSSLSecLevelCrypto(): number | undefined; @@ -953,6 +960,8 @@ export interface CryptoBinding { outputEncoding: string, outputEncodingId?: number, outputLength?: number, + functionName?: InternalCryptoBinding.OptionalBufferSource, + customization?: InternalCryptoBinding.OptionalBufferSource, ): string | InternalCryptoBinding.Buffer; parseX509(data: InternalCryptoBinding.ByteSource): InternalCryptoBinding.X509CertificateHandle; privateDecrypt: InternalCryptoBinding.PublicKeyCipher; From 54489eb9adbb7d8c35e35a20889c482093827be8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 23 Aug 2026 14:07:56 +0200 Subject: [PATCH 2/4] crypto: discover ciphers from OpenSSL providers Enumerate usable ciphers and aliases from activated OpenSSL 3 providers instead of maintaining lists of provider-only algorithms. Skip numeric OID aliases and filter NULL, TLS composite, multiblock, and encrypt-then-MAC implementations that the Cipher APIs cannot use. Preserve the OpenSSL 1.1.1 and BoringSSL paths. Expose CBC-CTS, SM4-GCM, SM4-CCM, SM4-XTS, and additional AES key wrap implementations. Add `ctsMode` (CS1/CS2/CS3) and `xtsStandard` (GB/IEEE) options for selecting provider CTS and SM4-XTS variants. Keep ordinary cipher construction on the original binding and legacy lookup paths. Lazily cache successful provider fetches per Environment for string initialization and `getCipherInfo()`. Index entries by case-insensitive query, canonical, and alias names. Deduplicate owners by provider and canonical identity. Return borrowed pointers on warm hits. Use the shared process-wide FIPS-state generation to invalidate per-Environment cipher caches and refresh `getCiphers()` snapshots in the main thread and workers. Existing cipher contexts retain their implementation and can finish across a transition. Release provider owners before unloading worker addon DSOs. Enforce one-shot updates for CBC-CTS, AES key wrap, SIV/GCM-SIV, and CCM decryption. Reject finalization without required input or CCM tags, and defer authentication failures to `final()`. Document streaming and XTS data-unit constraints. Add known-answer vectors, option validation, provider round trips, cache, worker, snapshot, FIPS transition, and construction benchmark coverage. Fixes: https://github.com/nodejs/node/issues/43040 Fixes: https://github.com/nodejs/node/issues/64866 Refs: https://github.com/nodejs/node/issues/62982 Signed-off-by: Filip Skokan --- benchmark/crypto/create-cipheriv.js | 62 +++ deps/ncrypto/ncrypto.cc | 388 ++++++++++++++---- deps/ncrypto/ncrypto.h | 81 ++-- doc/api/crypto.md | 269 +++++++++--- lib/internal/crypto/cipher.js | 32 +- lib/internal/crypto/util.js | 3 +- src/crypto/crypto_aes.h | 30 +- src/crypto/crypto_chacha20_poly1305.cc | 2 +- src/crypto/crypto_cipher.cc | 96 +++-- src/crypto/crypto_cipher.h | 10 +- src/crypto/crypto_context.cc | 11 +- src/env.cc | 2 + src/env.h | 2 + test/addons/openssl-providers/providers.cjs | 80 +++- test/fixtures/aead-vectors.js | 32 ++ .../snapshot/crypto-provider-cipher-cache.js | 55 +++ test/parallel/test-crypto-aes-wrap.js | 143 +++++++ test/parallel/test-crypto-authenticated.js | 44 +- ...est-crypto-cipherbase-options-fast-path.js | 130 ++++++ test/parallel/test-crypto-cipheriv-cbc-cts.js | 123 ++++++ .../test-crypto-cipheriv-decipheriv.js | 79 ++++ test/parallel/test-crypto-cipheriv-xts.js | 71 ++++ test/parallel/test-crypto-getcipherinfo.js | 69 +++- ...t-crypto-provider-cipher-cache-snapshot.js | 28 ++ .../test-crypto-provider-cipher-cache.js | 180 ++++++++ typings/internalBinding/crypto.d.ts | 2 + 26 files changed, 1799 insertions(+), 225 deletions(-) create mode 100644 benchmark/crypto/create-cipheriv.js create mode 100644 test/fixtures/snapshot/crypto-provider-cipher-cache.js create mode 100644 test/parallel/test-crypto-cipherbase-options-fast-path.js create mode 100644 test/parallel/test-crypto-cipheriv-cbc-cts.js create mode 100644 test/parallel/test-crypto-cipheriv-xts.js create mode 100644 test/parallel/test-crypto-provider-cipher-cache-snapshot.js create mode 100644 test/parallel/test-crypto-provider-cipher-cache.js diff --git a/benchmark/crypto/create-cipheriv.js b/benchmark/crypto/create-cipheriv.js new file mode 100644 index 000000000000..7774e393b403 --- /dev/null +++ b/benchmark/crypto/create-cipheriv.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('node:assert'); +const { + createCipheriv, + createDecipheriv, + getCiphers, +} = require('node:crypto'); + +const configurations = { + 'aes-128-cbc': { keyLength: 16, ivLength: 16 }, + 'aes-128-gcm': { keyLength: 16, ivLength: 12 }, + 'aes-128-cbc-cts': { keyLength: 16, ivLength: 16 }, + 'aes-128-wrap-inv': { keyLength: 16, ivLength: 8 }, + 'aes128-wrap-inv': { + keyLength: 16, + ivLength: 8, + warmupCipher: 'aes-128-wrap-inv', + }, +}; + +const ciphers = ['aes-128-cbc', 'aes-128-gcm']; +const availableCiphers = new Set(getCiphers()); +for (const cipher of [ + 'aes-128-cbc-cts', + 'aes-128-wrap-inv', + 'aes128-wrap-inv', +]) { + if (availableCiphers.has(cipher)) { + ciphers.push(cipher); + } +} + +const bench = common.createBenchmark(main, { + n: [1e5], + cipher: ciphers, + operation: ['encrypt', 'decrypt'], +}); + +function main({ n, cipher, operation }) { + const { + keyLength, + ivLength, + warmupCipher = cipher, + } = configurations[cipher]; + const key = Buffer.alloc(keyLength); + const iv = Buffer.alloc(ivLength); + const results = new Array(n); + const method = operation === 'encrypt' ? createCipheriv : createDecipheriv; + + const warmup = method(warmupCipher, key, iv); + assert.strictEqual(typeof warmup, 'object'); + + bench.start(); + for (let i = 0; i < n; ++i) { + results[i] = method(cipher, key, iv); + } + bench.end(n); + + assert.strictEqual(typeof results[n - 1], 'object'); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 18dac63be728..d334d17c300b 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -537,7 +537,7 @@ bool setFipsEnabled(bool enable, CryptoErrorList* errors) { #else const bool success = FIPS_mode_set(enable ? 1 : 0) == 1; #endif - if (isFipsEnabledRaw() != was_enabled) { + if (success && isFipsEnabledRaw() != was_enabled) { fips_state_generation.fetch_add(1, std::memory_order_release); } return success; @@ -4433,6 +4433,40 @@ constexpr char AsciiToLower(char c) { } #if NCRYPTO_USE_OPENSSL3_PROVIDER +constexpr auto kUnsupportedCipherFlags = + EVP_CIPH_FLAG_CIPHER_WITH_MAC | EVP_CIPH_FLAG_TLS1_1_MULTIBLOCK; + +bool HasUnsupportedCipherFlags(const EVP_CIPHER* cipher) { + return (EVP_CIPHER_get_flags(cipher) & kUnsupportedCipherFlags) != 0; +} + +bool IsSupportedLegacyCipher(const EVP_CIPHER* cipher) { + return cipher != nullptr && cipher != EVP_enc_null() && + !HasUnsupportedCipherFlags(cipher); +} + +bool IsSupportedFetchedCipher(const EVP_CIPHER* cipher) { + if (cipher == nullptr || EVP_CIPHER_is_a(cipher, "NULL") || + HasUnsupportedCipherFlags(cipher)) { + return false; + } + +#ifdef OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC + int encrypt_then_mac = 0; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC, + &encrypt_then_mac), + OSSL_PARAM_construct_end(), + }; + if (EVP_CIPHER_get_params(const_cast(cipher), params) == 1 && + encrypt_then_mac != 0) { + return false; + } +#endif + + return true; +} + void PushAlgorithmAlias(const char* name, void* arg) { if (name == nullptr) return; static_cast*>(arg)->emplace_back(name); @@ -4440,7 +4474,7 @@ void PushAlgorithmAlias(const char* name, void* arg) { #endif } // namespace -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER Cipher::Cipher(DeleteFnPtr cipher) : cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {} #endif @@ -4541,8 +4575,63 @@ const DigestCache::AliasMap& DigestCache::aliases() const { #endif } +const EVP_CIPHER* CipherCache::lookup(const char* name, uint64_t generation) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (generation_ != generation) { + aliases_.clear(); + ciphers_.clear(); + generation_ = generation; + } + + const auto it = aliases_.find(name); + if (it == aliases_.end()) return nullptr; + if (it->second >= ciphers_.size()) return nullptr; + return ciphers_[it->second].get(); +#else + static_cast(name); + static_cast(generation); + return nullptr; +#endif +} + +#if NCRYPTO_USE_OPENSSL3_PROVIDER +const EVP_CIPHER* CipherCache::insert( + const char* name, + DeleteFnPtr&& cipher, + uint64_t generation) { + if (generation_ != generation || cipher == nullptr) return nullptr; + + const char* canonical_name = EVP_CIPHER_get0_name(cipher.get()); + const OSSL_PROVIDER* provider = EVP_CIPHER_get0_provider(cipher.get()); + if (canonical_name != nullptr && provider != nullptr) { + for (size_t id = 0; id < ciphers_.size(); id++) { + const EVP_CIPHER* cached = ciphers_[id].get(); + const char* cached_name = EVP_CIPHER_get0_name(cached); + if (EVP_CIPHER_get0_provider(cached) == provider && + cached_name != nullptr && + CaseInsensitiveNameEqual()(cached_name, canonical_name)) { + aliases_.insert_or_assign(name, id); + return cached; + } + } + } + + ciphers_.emplace_back(std::move(cipher)); + const size_t id = ciphers_.size() - 1; + + std::vector aliases; + EVP_CIPHER_names_do_all(ciphers_[id].get(), PushAlgorithmAlias, &aliases); + for (const std::string& alias : aliases) { + aliases_.emplace(alias, id); + } + aliases_.insert_or_assign(name, id); + + return ciphers_[id].get(); +} +#endif + Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) { -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER if (other.fetched_cipher_ != nullptr) { if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) { fetched_cipher_.reset(other.fetched_cipher_.get()); @@ -4555,7 +4644,7 @@ Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) { Cipher& Cipher::operator=(const Cipher& other) { if (this == &other) return *this; -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER if (other.fetched_cipher_ != nullptr) { if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) { fetched_cipher_.reset(other.fetched_cipher_.get()); @@ -4572,40 +4661,59 @@ Cipher& Cipher::operator=(const Cipher& other) { return *this; } -const Cipher Cipher::FromName(const char* name) { +const Cipher Cipher::FromName(const char* name, CipherCache* cache) { const EVP_CIPHER* cipher = EVP_get_cipherbyname(name); - if (cipher != nullptr) return Cipher(cipher); + if (cipher != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!IsSupportedLegacyCipher(cipher)) return Cipher(); +#endif + return Cipher(cipher); + } + +#if NCRYPTO_USE_OPENSSL3_PROVIDER + // A resolution that overlaps a FIPS transition may use either property + // state. The cache retains the generation observed here, so the first + // resolution begun after the transition clears any stale entries. + const uint64_t generation = getFipsStateGeneration(); + if (cache != nullptr) { + if (const EVP_CIPHER* cached = cache->lookup(name, generation)) { + return Cipher(cached); + } + } -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV MarkPopErrorOnReturn mark_pop_error_on_return; DeleteFnPtr fetched( EVP_CIPHER_fetch(nullptr, name, nullptr)); - if (fetched == nullptr) return Cipher(); + if (!IsSupportedFetchedCipher(fetched.get())) return Cipher(); - const int mode = EVP_CIPHER_mode(fetched.get()); - const bool is_siv_mode = -#if OPENSSL_WITH_AES_SIV - mode == EVP_CIPH_SIV_MODE || -#endif -#if OPENSSL_WITH_AES_GCM_SIV - mode == EVP_CIPH_GCM_SIV_MODE || -#endif - false; - if (is_siv_mode) return Cipher(std::move(fetched)); + if (cache != nullptr && generation == getFipsStateGeneration()) { + if (const EVP_CIPHER* cached = + cache->insert(name, std::move(fetched), generation)) { + return Cipher(cached); + } + } - return Cipher(); + return Cipher(std::move(fetched)); #else + static_cast(cache); return Cipher(); #endif } -const Cipher Cipher::FromNid(int nid) { +const Cipher Cipher::FromNid(int nid, CipherCache* cache) { const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid); - if (cipher != nullptr) return Cipher(cipher); + if (cipher != nullptr) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + if (!IsSupportedLegacyCipher(cipher)) return Cipher(); +#endif + return Cipher(cipher); + } -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER const char* name = OBJ_nid2sn(nid); - if (name != nullptr) return FromName(name); + if (name != nullptr) return FromName(name, cache); +#else + static_cast(cache); #endif return Cipher(); @@ -4615,27 +4723,79 @@ const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) { return Cipher(GetCipherCtxCipher(ctx.get())); } -const Cipher Cipher::EMPTY = Cipher(); -const Cipher Cipher::AES_128_CBC = Cipher::FromNid(NID_aes_128_cbc); -const Cipher Cipher::AES_192_CBC = Cipher::FromNid(NID_aes_192_cbc); -const Cipher Cipher::AES_256_CBC = Cipher::FromNid(NID_aes_256_cbc); -const Cipher Cipher::AES_128_CTR = Cipher::FromNid(NID_aes_128_ctr); -const Cipher Cipher::AES_192_CTR = Cipher::FromNid(NID_aes_192_ctr); -const Cipher Cipher::AES_256_CTR = Cipher::FromNid(NID_aes_256_ctr); -const Cipher Cipher::AES_128_GCM = Cipher::FromNid(NID_aes_128_gcm); -const Cipher Cipher::AES_192_GCM = Cipher::FromNid(NID_aes_192_gcm); -const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm); -const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap); -const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap); -const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap); +namespace { +template +const Cipher& GetPredefinedCipher() { + static const Cipher cipher = Cipher::FromNid(nid); + return cipher; +} +} // namespace + +const Cipher& Cipher::AES_128_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_CBC() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_CTR() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_GCM() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_128_KW() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_KW() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_KW() { + return GetPredefinedCipher(); +} #ifndef OPENSSL_IS_BORINGSSL -const Cipher Cipher::AES_128_OCB = Cipher::FromNid(NID_aes_128_ocb); -const Cipher Cipher::AES_192_OCB = Cipher::FromNid(NID_aes_192_ocb); -const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb); +const Cipher& Cipher::AES_128_OCB() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_192_OCB() { + return GetPredefinedCipher(); +} + +const Cipher& Cipher::AES_256_OCB() { + return GetPredefinedCipher(); +} #endif -const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); +const Cipher& Cipher::CHACHA20_POLY1305() { + return GetPredefinedCipher(); +} bool Cipher::isGcmMode() const { if (!cipher_) return false; @@ -4657,6 +4817,15 @@ bool Cipher::isCcmMode() const { return getMode() == EVP_CIPH_CCM_MODE; } +bool Cipher::isCtsMode() const { + if (!cipher_) return false; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return (EVP_CIPHER_get_flags(cipher_) & EVP_CIPH_FLAG_CTS) != 0; +#else + return false; +#endif +} + bool Cipher::isOcbMode() const { if (!cipher_) return false; return getMode() == EVP_CIPH_OCB_MODE; @@ -4761,7 +4930,7 @@ const char* Cipher::getName() const { const char* name = OBJ_nid2sn(nid); if (name != nullptr) return name; } -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER return EVP_CIPHER_get0_name(cipher_); #else return {}; @@ -4858,11 +5027,57 @@ bool CipherCtxPointer::setAeadTagLength(size_t length) { ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr); } +#if NCRYPTO_USE_OPENSSL3_PROVIDER +namespace { +// OSSL_CIPHER_PARAM_XTS_STANDARD is not defined by OpenSSL 3.0. Use its +// parameter name directly so custom 3.0 providers can advertise it too. +constexpr char kCipherParamXtsStandard[] = "xts_standard"; + +bool SetCipherCtxStringParam(EVP_CIPHER_CTX* ctx, + const char* key, + const char* value) { + if (ctx == nullptr || value == nullptr) return false; + + const OSSL_PARAM* settable = EVP_CIPHER_CTX_settable_params(ctx); + const OSSL_PARAM* descriptor = + settable == nullptr ? nullptr : OSSL_PARAM_locate_const(settable, key); + if (descriptor == nullptr || + descriptor->data_type != OSSL_PARAM_UTF8_STRING) { + return false; + } + + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string(key, const_cast(value), 0), + OSSL_PARAM_END, + }; + return EVP_CIPHER_CTX_set_params(ctx, params) == 1; +} +} // namespace +#endif + +bool CipherCtxPointer::setCtsMode(const char* mode) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return SetCipherCtxStringParam(ctx_.get(), OSSL_CIPHER_PARAM_CTS_MODE, mode); +#else + static_cast(mode); + return false; +#endif +} + bool CipherCtxPointer::setPadding(bool padding) { if (!ctx_) return false; return EVP_CIPHER_CTX_set_padding(ctx_.get(), padding); } +bool CipherCtxPointer::setXtsStandard(const char* standard) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + return SetCipherCtxStringParam(ctx_.get(), kCipherParamXtsStandard, standard); +#else + static_cast(standard); + return false; +#endif +} + int CipherCtxPointer::getBlockSize() const { if (!ctx_) return 0; return EVP_CIPHER_CTX_block_size(ctx_.get()); @@ -4888,6 +5103,16 @@ bool CipherCtxPointer::isCcmMode() const { return getMode() == EVP_CIPH_CCM_MODE; } +bool CipherCtxPointer::isCtsMode() const { + if (!ctx_) return false; + return Cipher::FromCtx(*this).isCtsMode(); +} + +bool CipherCtxPointer::isXtsMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_XTS_MODE; +} + bool CipherCtxPointer::isWrapMode() const { if (!ctx_) return false; return getMode() == EVP_CIPH_WRAP_MODE; @@ -6359,23 +6584,7 @@ struct CipherCallbackContext { void operator()(const char* name) { cb(name); } }; -#if OPENSSL_WITH_AES_SIV -constexpr const char* kProviderOnlyAesSivCiphers[] = { - "aes-128-siv", - "aes-192-siv", - "aes-256-siv", -}; -#endif - -#if OPENSSL_WITH_AES_GCM_SIV -constexpr const char* kProviderOnlyAesGcmSivCiphers[] = { - "aes-128-gcm-siv", - "aes-192-gcm-siv", - "aes-256-gcm-siv", -}; -#endif - -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER template fetched( + fetch_type(nullptr, real_name, nullptr)); + if (!IsSupportedFetchedCipher(fetched.get())) return; - free_type(fetched); auto& cb = *(static_cast(arg)); cb(from); } + +void array_push_back_provider_name(const char* name, void* arg) { + if (name == nullptr) return; + + const std::string_view name_view(name); + const bool is_dotted_decimal = + name_view.find('.') != std::string_view::npos && + std::all_of(name_view.begin(), name_view.end(), [](unsigned char c) { + return (c >= '0' && c <= '9') || c == '.'; + }); + if (is_dotted_decimal) return; + + std::string normalized_name(name_view); + std::transform(normalized_name.begin(), + normalized_name.end(), + normalized_name.begin(), + [](unsigned char c) { + if (c >= 'A' && c <= 'Z') { + return static_cast(c + ('a' - 'A')); + } + return static_cast(c); + }); + auto& cb = *(static_cast(arg)); + cb(normalized_name.c_str()); +} + +void array_push_back_provider(EVP_CIPHER* cipher, void* arg) { + const char* name = EVP_CIPHER_get0_name(cipher); + if (name == nullptr) return; + + DeleteFnPtr fetched( + EVP_CIPHER_fetch(nullptr, name, nullptr)); + if (!IsSupportedFetchedCipher(fetched.get())) return; + + EVP_CIPHER_names_do_all(fetched.get(), array_push_back_provider_name, arg); +} #else template void array_push_back(const TypeName* evp_ref, @@ -6431,7 +6676,7 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { } #else EVP_CIPHER_do_all_sorted( -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_OPENSSL3_PROVIDER array_push_back, #endif &context); -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV - auto maybe_push_provider_only_cipher = [&](const char* name) { - EVP_CIPHER* cipher = EVP_CIPHER_fetch(nullptr, name, nullptr); - if (cipher == nullptr) return; - EVP_CIPHER_free(cipher); - context.cb(name); - }; -#endif -#if OPENSSL_WITH_AES_SIV - for (const char* name : kProviderOnlyAesSivCiphers) { - maybe_push_provider_only_cipher(name); - } -#endif -#if OPENSSL_WITH_AES_GCM_SIV - for (const char* name : kProviderOnlyAesGcmSivCiphers) { - maybe_push_provider_only_cipher(name); - } +#if NCRYPTO_USE_OPENSSL3_PROVIDER + EVP_CIPHER_do_all_provided(nullptr, array_push_back_provider, &context); #endif #endif } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 8d4091c75a98..8c09ac5f165d 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -20,6 +20,8 @@ #include #include #include +#include +#include #if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \ !defined(OPENSSL_NO_ENGINE) #include @@ -498,6 +500,32 @@ DataPointer xofHashDigest(const Buffer& data, const EVP_MD* md, size_t length); +class CipherCache final { + public: + CipherCache() = default; + NCRYPTO_DISALLOW_COPY_AND_MOVE(CipherCache) + + const EVP_CIPHER* lookup(const char* name, uint64_t generation); +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const EVP_CIPHER* insert(const char* name, + DeleteFnPtr&& cipher, + uint64_t generation); +#endif + + private: +#if NCRYPTO_USE_OPENSSL3_PROVIDER + using EVPCipherPointer = DeleteFnPtr; + + uint64_t generation_ = 0; + std::vector ciphers_; + std::unordered_map + aliases_; +#endif +}; + class Cipher final { public: static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH; @@ -519,7 +547,7 @@ class Cipher final { Cipher(const Cipher& other); Cipher& operator=(const Cipher& other); inline Cipher& operator=(const EVP_CIPHER* cipher) { -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER fetched_cipher_.reset(); #endif cipher_ = cipher; @@ -543,6 +571,7 @@ class Cipher final { bool isWrapMode() const; bool isCtrMode() const; bool isCcmMode() const; + bool isCtsMode() const; bool isOcbMode() const; bool isSivMode() const; bool isGcmSivMode() const; @@ -556,8 +585,8 @@ class Cipher final { unsigned char* key, unsigned char* iv) const; - static const Cipher FromName(const char* name); - static const Cipher FromNid(int nid); + static const Cipher FromName(const char* name, CipherCache* cache = nullptr); + static const Cipher FromNid(int nid, CipherCache* cache = nullptr); static const Cipher FromCtx(const CipherCtxPointer& ctx); using CipherNameCallback = std::function; @@ -566,28 +595,24 @@ class Cipher final { // is able to do so. static void ForEach(CipherNameCallback callback); - // Utilities to get various ciphers by type. If the underlying - // implementation does not support the requested cipher, then - // the result will be an empty Cipher object whose bool operator - // will return false. - - static const Cipher EMPTY; - static const Cipher AES_128_CBC; - static const Cipher AES_192_CBC; - static const Cipher AES_256_CBC; - static const Cipher AES_128_CTR; - static const Cipher AES_192_CTR; - static const Cipher AES_256_CTR; - static const Cipher AES_128_GCM; - static const Cipher AES_192_GCM; - static const Cipher AES_256_GCM; - static const Cipher AES_128_KW; - static const Cipher AES_192_KW; - static const Cipher AES_256_KW; - static const Cipher AES_128_OCB; - static const Cipher AES_192_OCB; - static const Cipher AES_256_OCB; - static const Cipher CHACHA20_POLY1305; + // Lazily resolves common ciphers. If the underlying implementation does not + // support the requested cipher, the returned Cipher will be empty. + static const Cipher& AES_128_CBC(); + static const Cipher& AES_192_CBC(); + static const Cipher& AES_256_CBC(); + static const Cipher& AES_128_CTR(); + static const Cipher& AES_192_CTR(); + static const Cipher& AES_256_CTR(); + static const Cipher& AES_128_GCM(); + static const Cipher& AES_192_GCM(); + static const Cipher& AES_256_GCM(); + static const Cipher& AES_128_KW(); + static const Cipher& AES_192_KW(); + static const Cipher& AES_256_KW(); + static const Cipher& AES_128_OCB(); + static const Cipher& AES_192_OCB(); + static const Cipher& AES_256_OCB(); + static const Cipher& CHACHA20_POLY1305(); struct CipherParams { int padding; @@ -617,7 +642,7 @@ class Cipher final { private: const EVP_CIPHER* cipher_ = nullptr; -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +#if NCRYPTO_USE_OPENSSL3_PROVIDER explicit Cipher(DeleteFnPtr cipher); DeleteFnPtr fetched_cipher_; #endif @@ -1007,7 +1032,9 @@ class CipherCtxPointer final { bool setIvLength(size_t length); bool setAeadTag(const Buffer& tag); bool setAeadTagLength(size_t length); + bool setCtsMode(const char* mode); bool setPadding(bool padding); + bool setXtsStandard(const char* standard); bool init(const Cipher& cipher, bool encrypt, const unsigned char* key = nullptr, @@ -1020,6 +1047,8 @@ class CipherCtxPointer final { bool isGcmMode() const; bool isOcbMode() const; bool isCcmMode() const; + bool isCtsMode() const; + bool isXtsMode() const; bool isWrapMode() const; bool isSivMode() const; bool isGcmSivMode() const; diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 78add10d64fd..3555414444b3 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -633,6 +633,10 @@ The [`crypto.createCipheriv()`][] method is used to create `Cipheriv` instances. `Cipheriv` objects are not to be created directly using the `new` keyword. +The selected algorithm may impose additional restrictions on streaming and +calls to [`cipher.update()`][]. See [CCM mode][], [CBC-CTS mode][], [XTS mode][], +[AES key wrap modes][], and [SIV and GCM-SIV modes][]. + Example: Using `Cipheriv` objects as streams: ```mjs @@ -905,14 +909,15 @@ added: v0.7.1 * `autoPadding` {boolean} **Default:** `true` * Returns: {Cipheriv} The same `Cipheriv` instance for method chaining. -When using block encryption algorithms, the `Cipheriv` class will automatically -add padding to the input data to the appropriate block size. To disable the -default padding call `cipher.setAutoPadding(false)`. +When using block ciphers that use standard block padding, the `Cipheriv` class +will automatically add padding to the input data to the appropriate block size. +To disable the default padding call `cipher.setAutoPadding(false)`. -When `autoPadding` is `false`, the length of the entire input data must be a -multiple of the cipher's block size or [`cipher.final()`][] will throw an error. -Disabling automatic padding is useful for non-standard padding, for instance -using `0x0` instead of PKCS padding. +For block ciphers that use standard block padding, when `autoPadding` is +`false`, the length of the entire input data must be a multiple of the cipher's +block size or [`cipher.final()`][] will throw an error. Disabling automatic +padding is useful for non-standard padding, for instance using `0x0` instead of +PKCS padding. The `cipher.setAutoPadding()` method must be called before [`cipher.final()`][]. @@ -946,9 +951,12 @@ is specified, a string using the specified encoding is returned. If no When `outputEncoding` is specified, it must use the same encoding as previous calls to `cipher.update()`. -The `cipher.update()` method can be called multiple times with new data until -[`cipher.final()`][] is called. Calling `cipher.update()` after -[`cipher.final()`][] will result in an error being thrown. +For most algorithms, `cipher.update()` can be called multiple times with new +data until [`cipher.final()`][] is called. Some algorithms restrict calls to +`cipher.update()`. For example, [CCM mode][], [CBC-CTS mode][], [XTS mode][], +[AES key wrap modes][], and [SIV and GCM-SIV modes][] require the whole message +in a single call. Calling `cipher.update()` after [`cipher.final()`][] will +result in an error being thrown. ## Class: `Decipheriv` @@ -970,6 +978,10 @@ The [`crypto.createDecipheriv()`][] method is used to create `Decipheriv` instances. `Decipheriv` objects are not to be created directly using the `new` keyword. +The selected algorithm may impose additional restrictions on streaming and +calls to [`decipher.update()`][]. See [CCM mode][], [CBC-CTS mode][], +[XTS mode][], [AES key wrap modes][], and [SIV and GCM-SIV modes][]. + Example: Using `Decipheriv` objects as streams: ```mjs @@ -1267,8 +1279,8 @@ When data has been encrypted without standard block padding, calling `decipher.setAutoPadding(false)` will disable automatic padding to prevent [`decipher.final()`][] from checking for and removing padding. -Turning auto padding off will only work if the input data's length is a -multiple of the ciphers block size. +For block ciphers that use standard block padding, disabling it requires the +input data's length to be a multiple of the cipher's block size. The `decipher.setAutoPadding()` method must be called before [`decipher.final()`][]. @@ -1291,19 +1303,23 @@ changes: Updates the decipher with `data`. If the `inputEncoding` argument is given, the `data` argument is a string using the specified encoding. If the `inputEncoding` -argument is not given, `data` must be a [`Buffer`][]. If `data` is a -[`Buffer`][] then `inputEncoding` is ignored. +argument is not given, `data` must be a [`Buffer`][], `TypedArray`, or +`DataView`. If `data` is a [`Buffer`][], `TypedArray`, or `DataView`, then +`inputEncoding` is ignored. -The `outputEncoding` specifies the output format of the enciphered +The `outputEncoding` specifies the output format of the deciphered data. If the `outputEncoding` is specified, a string using the specified encoding is returned. If no `outputEncoding` is provided, a [`Buffer`][] is returned. When `outputEncoding` is specified, it must use the same encoding as previous calls to `decipher.update()`. -The `decipher.update()` method can be called multiple times with new data until -[`decipher.final()`][] is called. Calling `decipher.update()` after -[`decipher.final()`][] will result in an error being thrown. +For most algorithms, `decipher.update()` can be called multiple times with new +data until [`decipher.final()`][] is called. Some algorithms restrict calls to +`decipher.update()`. For example, [CCM mode][], [CBC-CTS mode][], [XTS mode][], +[AES key wrap modes][], and [SIV and GCM-SIV modes][] require the whole message +in a single call. Calling `decipher.update()` after [`decipher.final()`][] will +result in an error being thrown. Even if the underlying cipher implements authentication, the authenticity and integrity of the plaintext returned from this function may be uncertain at this @@ -3566,6 +3582,12 @@ operations. The specific constants currently defined are described in