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 fb7446578f57..d334d17c300b 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 (success && 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,13 +4427,211 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) { // ============================================================================ -#if OPENSSL_WITH_AES_SIV || OPENSSL_WITH_AES_GCM_SIV +namespace { +constexpr char AsciiToLower(char c) { + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : 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); +} +#endif +} // namespace + +#if NCRYPTO_USE_OPENSSL3_PROVIDER 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 +} + +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()); @@ -4425,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()); @@ -4442,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(); @@ -4485,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; @@ -4527,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; @@ -4631,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 {}; @@ -4728,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()); @@ -4758,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; @@ -6229,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, @@ -6301,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 } @@ -6479,11 +6839,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 +7377,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 +7516,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..8c09ac5f165d 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -13,12 +13,15 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include #if defined(NCRYPTO_ENGINE_COMPAT) && NCRYPTO_ENGINE_COMPAT && \ !defined(OPENSSL_NO_ENGINE) #include @@ -397,9 +400,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 +424,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. @@ -431,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; @@ -452,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; @@ -476,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; @@ -489,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; @@ -499,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; @@ -550,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 @@ -940,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, @@ -953,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; @@ -1690,7 +1786,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 +1985,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..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 * 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 +5115,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 +5141,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 +5159,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 +5245,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 +5308,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 +5421,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 +5496,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 +5558,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 +5719,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 +6509,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 +6655,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 @@ -6732,6 +6902,79 @@ try { console.log(receivedPlaintext); ``` +### CBC-CTS mode + +For CBC ciphertext stealing (CBC-CTS) ciphers, the `ctsMode` option to +[`crypto.createCipheriv()`][] or [`crypto.createDecipheriv()`][] selects the +variant: + +* `'CS1'` is the default. For block-aligned input, its output is the same as CBC + mode. +* `'CS2'` is also the same as CBC for block-aligned input. For input with a + partial final block, it swaps the final full and partial ciphertext blocks + relative to CS1. +* `'CS3'` is the Kerberos 5 variant. It uses the CS2 ordering for a partial + final block and swaps the final two ciphertext blocks even for block-aligned + input. + +Encryption and decryption must use the same variant. The option is available +only with CBC-CTS provider ciphers on OpenSSL 3.0 or later. + +Applications which use this mode must adhere to these restrictions: + +* The plaintext or ciphertext must be at least one block long. +* The ciphertext has the same length as the plaintext. +* `cipher.update()` or `decipher.update()` must be called exactly once with all + input data. Stream methods such as `write(data)`, `end(data)`, or `pipe()` may + fail because CBC-CTS does not accept multiple data updates. +* `cipher.final()` or `decipher.final()` must still be called to complete the + operation. +* `crypto.getCipherInfo()` reports the base mode as `'cbc'`. + +### XTS mode + +XTS ciphers operate on independently tweakable data units. A `Cipheriv` or +`Decipheriv` instance represents one complete data unit, and its `iv` argument +provides the 16-byte tweak. Use a new instance with the appropriate positional +tweak for each different logical data unit. + +Applications which use XTS mode must adhere to these restrictions: + +* The plaintext or ciphertext must be at least one 16-byte block. Its length + does not have to be a multiple of 16 bytes because XTS uses ciphertext + stealing for a final partial block. +* The ciphertext has the same length as the plaintext. +* `cipher.update()` or `decipher.update()` must be called exactly once with all + input data. Stream methods such as `write(data)`, `end(data)`, or `pipe()` may + fail because XTS does not accept multiple data updates. +* `cipher.final()` or `decipher.final()` must still be called to complete the + operation. + +For `sm4-xts`, the `xtsStandard` option to [`crypto.createCipheriv()`][] or +[`crypto.createDecipheriv()`][] selects either the default `'GB'` variant from +GB/T 17964-2021 or the `'IEEE'` variant from IEEE Std 1619-2007. Encryption and +decryption must use the same variant. The option is available only for +`sm4-xts`; it does not apply to AES-XTS ciphers. OpenSSL's default provider +supports `sm4-xts` in OpenSSL 3.2 or later. + +### AES key wrap modes + +AES key wrap (`AES-WRAP`) and AES key wrap with padding (`AES-WRAP-PAD`) +ciphers operate on a complete key-data value rather than on an incremental byte +stream. The inverse-transform variants have the same processing restrictions. + +Applications which use an AES key wrap cipher must adhere to these +restrictions: + +* `cipher.update()` or `decipher.update()` must be called exactly once with the + complete, non-empty input value. +* Do not use AES key wrap ciphers as generic [`stream.Transform`][] streams. + Methods such as `write(data)`, `end(data)`, and `pipe()` can split one value + across multiple updates, with each update being treated as a separate wrap or + unwrap operation. +* `cipher.final()` or `decipher.final()` must still be called to complete the + operation. + ### SIV and GCM-SIV modes `SIV`[^openssl30] and `GCM-SIV`[^openssl32] are supported [AEAD algorithms][] @@ -7157,6 +7400,8 @@ See the [list of SSL OP Flags][] for details. [^openssl35]: Requires OpenSSL >= 3.5 [AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption +[AES key wrap modes]: #aes-key-wrap-modes +[CBC-CTS mode]: #cbc-cts-mode [CCM mode]: #ccm-mode [CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532 [Caveats]: #support-for-weak-or-compromised-algorithms @@ -7186,7 +7431,9 @@ See the [list of SSL OP Flags][] for details. [RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt [RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt [RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt +[SIV and GCM-SIV modes]: #siv-and-gcm-siv-modes [Web Crypto API documentation]: webcrypto.md +[XTS mode]: #xts-mode [`--allow-openssl-store`]: cli.md#--allow-openssl-store [`--enable-fips`]: cli.md#--enable-fips [`--force-fips`]: cli.md#--force-fips @@ -7216,6 +7463,7 @@ See the [list of SSL OP Flags][] for details. [`crypto.createVerify()`]: #cryptocreateverifyalgorithm-options [`crypto.generateKey()`]: #cryptogeneratekeytype-options-callback [`crypto.generateKeyPair()`]: #cryptogeneratekeypairtype-options-callback +[`crypto.getCiphers()`]: #cryptogetciphers [`crypto.getCurves()`]: #cryptogetcurves [`crypto.getDiffieHellman()`]: #cryptogetdiffiehellmangroupname [`crypto.getFips()`]: #cryptogetfips @@ -7248,6 +7496,7 @@ See the [list of SSL OP Flags][] for details. [`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist [`sign.sign()`]: #signsignprivatekey-outputencoding [`sign.update()`]: #signupdatedata-inputencoding +[`stream.Transform`]: stream.md#class-streamtransform [`stream.Writable` options]: stream.md#new-streamwritableoptions [`stream.transform` options]: stream.md#new-streamtransformoptions [`util.promisify()`]: util.md#utilpromisifyoriginal diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index dafcc59d5066..4b4165c55084 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -33,6 +33,7 @@ const { const { validateEncoding, + validateOneOf, validateUint32, validateObject, validateString, @@ -60,6 +61,9 @@ const { normalizeEncoding, kEmptyObject } = require('internal/util'); const { StringDecoder } = require('string_decoder'); +const kCtsModes = ['CS1', 'CS2', 'CS3']; +const kXtsStandards = ['GB', 'IEEE']; + function rsaFunctionFor(method, defaultPadding, keyType) { const keyName = keyType === 'private' ? 'privateKey' : undefined; return (key, buffer) => { @@ -119,8 +123,32 @@ function getUIntOption(options, key) { } function createCipherBase(cipher, credential, options, isEncrypt, iv) { - const authTagLength = getUIntOption(options, 'authTagLength'); - this[kHandle] = new CipherBase(isEncrypt, cipher, credential, iv, authTagLength); + let authTagLength = -1; + let ctsMode; + let xtsStandard; + if (options != null) { + authTagLength = getUIntOption(options, 'authTagLength'); + ctsMode = getStringOption(options, 'ctsMode') ?? undefined; + xtsStandard = getStringOption(options, 'xtsStandard') ?? undefined; + if (ctsMode !== undefined) + validateOneOf(ctsMode, 'options.ctsMode', kCtsModes); + if (xtsStandard !== undefined) + validateOneOf(xtsStandard, 'options.xtsStandard', kXtsStandards); + } + + if (ctsMode === undefined && xtsStandard === undefined) { + this[kHandle] = new CipherBase( + isEncrypt, cipher, credential, iv, authTagLength); + } else { + this[kHandle] = new CipherBase( + isEncrypt, + cipher, + credential, + iv, + authTagLength, + ctsMode, + xtsStandard); + } this._decoder = null; FunctionPrototypeCall(LazyTransform, this, options); 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..1de25e514793 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,58 @@ 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 getCiphers = cachedArrayByFipsGeneration( + () => filterDuplicateStrings(_getCiphers())); +const getHashes = cachedArrayByFipsGeneration( + () => filterDuplicateStrings(_getHashes()), + () => { + _hashCache = undefined; + }); + const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); const emitOpenSSLEngineDeprecation = getDeprecationWarningEmitter( diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 76359f14e7df..6b18b1e29e24 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -13,15 +13,15 @@ namespace node::crypto { constexpr unsigned kNoAuthTagLength = static_cast(-1); #define VARIANTS_COMMON(V) \ - V(CTR_128, AES_CTR_Cipher, ncrypto::Cipher::AES_128_CTR) \ - V(CTR_192, AES_CTR_Cipher, ncrypto::Cipher::AES_192_CTR) \ - V(CTR_256, AES_CTR_Cipher, ncrypto::Cipher::AES_256_CTR) \ - V(CBC_128, AES_Cipher, ncrypto::Cipher::AES_128_CBC) \ - V(CBC_192, AES_Cipher, ncrypto::Cipher::AES_192_CBC) \ - V(CBC_256, AES_Cipher, ncrypto::Cipher::AES_256_CBC) \ - V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM) \ - V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM) \ - V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM) \ + V(CTR_128, AES_CTR_Cipher, ncrypto::Cipher::AES_128_CTR()) \ + V(CTR_192, AES_CTR_Cipher, ncrypto::Cipher::AES_192_CTR()) \ + V(CTR_256, AES_CTR_Cipher, ncrypto::Cipher::AES_256_CTR()) \ + V(CBC_128, AES_Cipher, ncrypto::Cipher::AES_128_CBC()) \ + V(CBC_192, AES_Cipher, ncrypto::Cipher::AES_192_CBC()) \ + V(CBC_256, AES_Cipher, ncrypto::Cipher::AES_256_CBC()) \ + V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM()) \ + V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM()) \ + V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM()) \ VARIANTS_KW(V) #ifdef OPENSSL_IS_BORINGSSL @@ -33,16 +33,16 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(KW_256, AES_KW_Cipher, static_cast(nullptr)) #else #define VARIANTS_KW(V) \ - V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW) \ - V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \ - V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) + V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW()) \ + V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW()) \ + V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW()) #endif #if OPENSSL_WITH_AES_OCB #define VARIANTS_OCB(V) \ - V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \ - V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \ - V(OCB_256, AES_Cipher, ncrypto::Cipher::AES_256_OCB) + V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB()) \ + V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB()) \ + V(OCB_256, AES_Cipher, ncrypto::Cipher::AES_256_OCB()) #else #define VARIANTS_OCB(V) #endif diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc index 1cdb933c65d4..56ba78be7ccd 100644 --- a/src/crypto/crypto_chacha20_poly1305.cc +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -105,7 +105,7 @@ Maybe ChaCha20Poly1305CipherTraits::AdditionalConfig( ChaCha20Poly1305CipherConfig* params) { Environment* env = Environment::GetCurrent(args); - params->cipher = ncrypto::Cipher::CHACHA20_POLY1305; + params->cipher = ncrypto::Cipher::CHACHA20_POLY1305(); #ifndef OPENSSL_IS_BORINGSSL // On BoringSSL, ChaCha20-Poly1305 is not exposed via the EVP_CIPHER registry diff --git a/src/crypto/crypto_cipher.cc b/src/crypto/crypto_cipher.cc index 720eb656b89d..dfd797c82659 100644 --- a/src/crypto/crypto_cipher.cc +++ b/src/crypto/crypto_cipher.cc @@ -75,10 +75,10 @@ void GetCipherInfo(const FunctionCallbackInfo& args) { const auto cipher = ([&] { if (args[0]->IsString()) { Utf8Value name(env->isolate(), args[0]); - return Cipher::FromName(*name); + return Cipher::FromName(*name, env->provider_cipher_cache.get()); } else { int nid = args[0].As()->Value(); - return Cipher::FromNid(nid); + return Cipher::FromNid(nid, env->provider_cipher_cache.get()); } })(); @@ -227,7 +227,7 @@ CipherBase::CipherBase(Environment* env, Local wrap, CipherKind kind) auth_tag_state_(kAuthTagUnknown), auth_tag_len_(kNoAuthTagLength), pending_auth_failed_(false), - has_siv_update_(false), + has_one_shot_update_(false), siv_aad_components_(0) { MakeWeak(); } @@ -311,7 +311,7 @@ void CipherBase::RegisterExternalReferences( void CipherBase::New(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - CHECK_EQ(args.Length(), 5); + CHECK(args.Length() == 5 || args.Length() == 7); CipherBase* cipher = new CipherBase(env, args.This(), args[0]->IsTrue() ? kCipher : kDecipher); @@ -343,7 +343,25 @@ void CipherBase::New(const FunctionCallbackInfo& args) { auth_tag_len = kNoAuthTagLength; } - cipher->InitIv(*cipher_type, key_buf, iv_buf, auth_tag_len); + if (args.Length() == 5) { + cipher->InitIv( + *cipher_type, key_buf, iv_buf, auth_tag_len, nullptr, nullptr); + return; + } + + CHECK(args[5]->IsString() || args[5]->IsUndefined()); + CHECK(args[6]->IsString() || args[6]->IsUndefined()); + const Utf8Value cts_mode(env->isolate(), + args[5]->IsString() ? args[5] : Local()); + const Utf8Value xts_standard(env->isolate(), + args[6]->IsString() ? args[6] : Local()); + + cipher->InitIv(*cipher_type, + key_buf, + iv_buf, + auth_tag_len, + args[5]->IsString() ? *cts_mode : nullptr, + args[6]->IsString() ? *xts_standard : nullptr); } void CipherBase::CommonInit(const char* cipher_type, @@ -352,7 +370,9 @@ void CipherBase::CommonInit(const char* cipher_type, int key_len, const unsigned char* iv, int iv_len, - unsigned int auth_tag_len) { + unsigned int auth_tag_len, + const char* cts_mode, + const char* xts_standard) { MarkPopErrorOnReturn mark_pop_error_on_return; CHECK(!ctx_); ctx_ = CipherCtxPointer::New(); @@ -372,6 +392,18 @@ void CipherBase::CommonInit(const char* cipher_type, "Failed to initialize cipher"); } + if (cts_mode != nullptr && !ctx_.setCtsMode(cts_mode)) { + ctx_.reset(); + return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION( + env(), "%s does not support the ctsMode option", cipher_type); + } + + if (xts_standard != nullptr && !ctx_.setXtsStandard(xts_standard)) { + ctx_.reset(); + return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION( + env(), "%s does not support the xtsStandard option", cipher_type); + } + if (cipher.isSupportedAuthenticatedMode()) { CHECK_GE(iv_len, 0); if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len)) { @@ -394,11 +426,14 @@ void CipherBase::CommonInit(const char* cipher_type, void CipherBase::InitIv(const char* cipher_type, const ByteSource& key_buf, const ArrayBufferOrViewContents& iv_buf, - unsigned int auth_tag_len) { + unsigned int auth_tag_len, + const char* cts_mode, + const char* xts_standard) { HandleScope scope(env()->isolate()); MarkPopErrorOnReturn mark_pop_error_on_return; - auto cipher = Cipher::FromName(cipher_type); + auto cipher = + Cipher::FromName(cipher_type, env()->provider_cipher_cache.get()); if (!cipher) return THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env()); const int expected_iv_len = cipher.getIvLength(); @@ -436,14 +471,15 @@ void CipherBase::InitIv(const char* cipher_type, return THROW_ERR_CRYPTO_INVALID_IV(env()); } - CommonInit( - cipher_type, - cipher, - key_buf.data(), - key_buf.size(), - iv_buf.data(), - iv_buf.size(), - auth_tag_len); + CommonInit(cipher_type, + cipher, + key_buf.data(), + key_buf.size(), + iv_buf.data(), + iv_buf.size(), + auth_tag_len, + cts_mode, + xts_standard); } bool CipherBase::InitAuthenticated(const char* cipher_type, @@ -563,7 +599,7 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { } if ((cipher->ctx_.isSivMode() || cipher->ctx_.isGcmSivMode()) && - cipher->has_siv_update_) { + cipher->has_one_shot_update_) { return args.GetReturnValue().Set(false); } @@ -598,7 +634,7 @@ bool CipherBase::SetAAD( if (!ctx_ || !IsAuthenticatedMode()) return false; const bool is_siv = ctx_.isSivMode(); - if ((is_siv || ctx_.isGcmSivMode()) && has_siv_update_) return false; + if ((is_siv || ctx_.isGcmSivMode()) && has_one_shot_update_) return false; if (is_siv && siv_aad_components_ >= kMaxSivAADComponents) return false; MarkPopErrorOnReturn mark_pop_error_on_return; @@ -659,12 +695,20 @@ CipherBase::UpdateResult CipherBase::Update( if (!ctx_ || len > INT_MAX) return kErrorState; MarkPopErrorOnReturn mark_pop_error_on_return; - if (ctx_.isCcmMode() && !CheckCCMMessageLength(len)) { + const bool is_ccm_mode = ctx_.isCcmMode(); + const bool is_ccm_decipher = kind_ == kDecipher && is_ccm_mode; + + if (is_ccm_mode && !CheckCCMMessageLength(len)) { return kErrorMessageSize; } const bool is_siv = ctx_.isSivMode() || ctx_.isGcmSivMode(); - if (is_siv && has_siv_update_) { + const bool is_cts = ctx_.isCtsMode(); + const bool is_xts = ctx_.isXtsMode(); + const bool is_wrap = ctx_.isWrapMode(); + const bool is_one_shot = + is_ccm_decipher || is_siv || is_cts || is_xts || is_wrap; + if (is_one_shot && has_one_shot_update_) { return kErrorState; } @@ -694,8 +738,8 @@ CipherBase::UpdateResult CipherBase::Update( bool r = ctx_.update( buffer, static_cast((*out)->Data()), &buf_len); - if (is_siv) { - has_siv_update_ = true; + if (is_ccm_decipher || is_siv || ((is_cts || is_xts || is_wrap) && r)) { + has_one_shot_update_ = true; } // When in CCM mode, EVP_CipherUpdate will fail if the authentication tag is @@ -773,7 +817,10 @@ void CipherBase::SetAutoPadding(const FunctionCallbackInfo& args) { bool CipherBase::Final(std::unique_ptr* out) { if (!ctx_) return false; - if ((ctx_.isSivMode() || ctx_.isGcmSivMode()) && !has_siv_update_) { + const bool is_one_shot = ctx_.isSivMode() || ctx_.isGcmSivMode() || + ctx_.isCtsMode() || ctx_.isXtsMode() || + ctx_.isWrapMode(); + if (is_one_shot && !has_one_shot_update_) { ctx_.reset(); return false; } @@ -796,7 +843,8 @@ bool CipherBase::Final(std::unique_ptr* out) { // EVP_CipherFinal_ex must not be called and will fail. bool ok; if (kind_ == kDecipher && ctx_.isCcmMode()) { - ok = !pending_auth_failed_; + ok = auth_tag_state_ == kAuthTagSetByUser && has_one_shot_update_ && + !pending_auth_failed_; *out = ArrayBuffer::NewBackingStore(env()->isolate(), 0); } else { int out_len = (*out)->ByteLength(); diff --git a/src/crypto/crypto_cipher.h b/src/crypto/crypto_cipher.h index b751173d5591..f6deb774ea36 100644 --- a/src/crypto/crypto_cipher.h +++ b/src/crypto/crypto_cipher.h @@ -51,11 +51,15 @@ class CipherBase : public BaseObject { int key_len, const unsigned char* iv, int iv_len, - unsigned int auth_tag_len); + unsigned int auth_tag_len, + const char* cts_mode, + const char* xts_standard); void InitIv(const char* cipher_type, const ByteSource& key_buf, const ArrayBufferOrViewContents& iv_buf, - unsigned int auth_tag_len); + unsigned int auth_tag_len, + const char* cts_mode, + const char* xts_standard); bool InitAuthenticated(const char* cipher_type, int iv_len, unsigned int auth_tag_len); @@ -87,7 +91,7 @@ class CipherBase : public BaseObject { unsigned int auth_tag_len_; char auth_tag_[ncrypto::Cipher::MAX_AUTH_TAG_LENGTH]; bool pending_auth_failed_; - bool has_siv_update_; + bool has_one_shot_update_; unsigned int siv_aad_components_; int max_message_size_; }; diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 95564f02b343..2919a2c0174b 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -2507,9 +2507,11 @@ int SecureContext::TicketKeyCallback(SSL* ssl, ArrayBufferViewContents aes_key(aes.As()); if (enc) { - EVP_EncryptInit_ex(ectx, Cipher::AES_128_CBC, nullptr, aes_key.data(), iv); + EVP_EncryptInit_ex( + ectx, Cipher::AES_128_CBC(), nullptr, aes_key.data(), iv); } else { - EVP_DecryptInit_ex(ectx, Cipher::AES_128_CBC, nullptr, aes_key.data(), iv); + EVP_DecryptInit_ex( + ectx, Cipher::AES_128_CBC(), nullptr, aes_key.data(), iv); } return r; @@ -2532,7 +2534,8 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl, memcpy(name, sc->ticket_key_name_, sizeof(sc->ticket_key_name_)); if (!ncrypto::CSPRNG(iv, 16) || EVP_EncryptInit_ex( - ectx, Cipher::AES_128_CBC, nullptr, sc->ticket_key_aes_, iv) <= 0 || + ectx, Cipher::AES_128_CBC(), nullptr, sc->ticket_key_aes_, iv) <= + 0 || !InitTicketHmac( hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_))) { return -1; @@ -2546,7 +2549,7 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl, } if (EVP_DecryptInit_ex( - ectx, Cipher::AES_128_CBC, nullptr, sc->ticket_key_aes_, iv) <= 0 || + ectx, Cipher::AES_128_CBC(), nullptr, sc->ticket_key_aes_, iv) <= 0 || !InitTicketHmac( hctx, sc->ticket_key_hmac_, sizeof(sc->ticket_key_hmac_))) { return -1; diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 2ed356120c48..1e47258857e8 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,23 @@ 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(); 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_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 +327,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 +340,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 +368,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 +389,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 +427,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 +437,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 +518,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 +599,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 +611,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 +662,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 +786,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 +799,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 +819,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 +832,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 +847,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 +865,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..ecacfda218dc 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,11 @@ 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(); + provider_cipher_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 +1124,12 @@ 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(); + provider_cipher_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..c14e51a51f4b 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,11 @@ #include #include +namespace ncrypto { +class CipherCache; +class DigestCache; +} // namespace ncrypto + namespace node { namespace shadow_realm { @@ -1091,12 +1092,9 @@ 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::unique_ptr provider_cipher_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..fc0f93ef45c8 100644 --- a/test/addons/openssl-providers/providers.cjs +++ b/test/addons/openssl-providers/providers.cjs @@ -10,7 +10,13 @@ if (!hasOpenSSL3) { common.skip('this test requires OpenSSL 3.x'); } const assert = require('node:assert'); -const { createHash, getCiphers, getHashes } = require('node:crypto'); +const { + createCipheriv, + createDecipheriv, + createHash, + getCiphers, + getHashes, +} = require('node:crypto'); const { debuglog } = require('node:util'); const { getProviders } = require(`./build/${common.buildType}/binding`); @@ -20,11 +26,45 @@ const { getProviders } = require(`./build/${common.buildType}/binding`); // supported by the provider. const providers = { 'default': { - ciphers: ['des3-wrap'], - hashes: ['sha512-256'], + ciphers: [ + { + name: 'aes-128-cbc-cts', + keyLength: 16, + ivLength: 16, + plaintextLength: 32, + unavailableCode: 'ERR_CRYPTO_UNKNOWN_CIPHER', + }, + { + name: 'des3-wrap', + keyLength: 24, + ivLength: null, + plaintextLength: 16, + unavailableCode: 'ERR_OSSL_EVP_UNSUPPORTED', + }, + ], + hashes: [ + 'sha512-256', + ...['keccak-kmac-128', 'keccak-kmac128'] + .filter((name) => getHashes().includes(name)), + ], }, 'legacy': { - ciphers: ['blowfish', 'idea'], + ciphers: [ + { + name: 'blowfish', + keyLength: 16, + ivLength: 8, + plaintextLength: 16, + unavailableCode: 'ERR_OSSL_EVP_UNSUPPORTED', + }, + { + name: 'idea', + keyLength: 16, + ivLength: 8, + plaintextLength: 16, + unavailableCode: 'ERR_OSSL_EVP_UNSUPPORTED', + }, + ], hashes: ['md4', 'whirlpool'], }, }; @@ -47,17 +87,50 @@ 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 assertCipherRoundTrip({ + name, + keyLength, + ivLength, + plaintextLength, +}) { + const key = Buffer.alloc(keyLength, 0x42); + const iv = ivLength === null ? null : Buffer.alloc(ivLength, 0x24); + const plaintext = Buffer.alloc(plaintextLength, 0x61); + + const cipher = createCipheriv(name, key, iv); + const ciphertext = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + const decipher = createDecipheriv(name, key, iv); + const received = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + assert.deepStrictEqual(received, plaintext); +} + function testProviderPresent(provider) { debug(`Checking '${provider}' is present`); assertArrayIncludes(getProviders(), provider, 'Loaded providers'); for (const cipher of providers[provider].ciphers || []) { - debug(`Checking '${cipher}' cipher is available`); - assertArrayIncludes(getCiphers(), cipher, 'Available ciphers'); + debug(`Checking '${cipher.name}' cipher is available`); + assertArrayIncludes(getCiphers(), cipher.name, 'Available ciphers'); + assertCipherRoundTrip(cipher); } for (const hash of providers[provider].hashes || []) { debug(`Checking '${hash}' hash is available`); assertArrayIncludes(getHashes(), hash, 'Available hashes'); - createHash(hash); + createSupportedHash(hash); } } @@ -65,8 +138,14 @@ function testProviderAbsent(provider) { debug(`Checking '${provider}' is absent`); assertArrayDoesNotInclude(getProviders(), provider, 'Loaded providers'); for (const cipher of providers[provider].ciphers || []) { - debug(`Checking '${cipher}' cipher is unavailable`); - assertArrayDoesNotInclude(getCiphers(), cipher, 'Available ciphers'); + const { name, keyLength, ivLength, unavailableCode } = cipher; + debug(`Checking '${name}' cipher is unavailable`); + assertArrayDoesNotInclude(getCiphers(), name, 'Available ciphers'); + const key = Buffer.alloc(keyLength, 0x42); + const iv = ivLength === null ? null : Buffer.alloc(ivLength, 0x24); + assert.throws(() => createCipheriv(name, key, iv), { + code: unavailableCode, + }); } for (const hash of providers[provider].hashes || []) { debug(`Checking '${hash}' hash is unavailable`); 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/aead-vectors.js b/test/fixtures/aead-vectors.js index e511c6babf38..4784b8165ffa 100644 --- a/test/fixtures/aead-vectors.js +++ b/test/fixtures/aead-vectors.js @@ -52,6 +52,38 @@ module.exports = [ ct: '0eaccb', tag: '93da9bb81333aee0c785b240d319719d', tampered: false }, + // RFC 8998, Appendix A.1 + { algo: 'sm4-gcm', + key: '0123456789abcdeffedcba9876543210', + iv: '00001234567800000000abcd', + plain: 'aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb' + + 'ccccccccccccccccdddddddddddddddd' + + 'eeeeeeeeeeeeeeeeffffffffffffffff' + + 'eeeeeeeeeeeeeeeeaaaaaaaaaaaaaaaa', + plainIsHex: true, + aad: 'feedfacedeadbeeffeedfacedeadbeefabaddad2', + ct: '17f399f08c67d5ee19d0dc9969c4bb7d' + + '5fd46fd3756489069157b282bb200735' + + 'd82710ca5c22f0ccfa7cbf93d496ac15' + + 'a56834cbcf98c397b4024a2691233b8d', + tag: '83de3541e4c2b58177e065a9bf7b62ec', tampered: false }, + + // RFC 8998, Appendix A.2 + { algo: 'sm4-ccm', + key: '0123456789abcdeffedcba9876543210', + iv: '00001234567800000000abcd', + plain: 'aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb' + + 'ccccccccccccccccdddddddddddddddd' + + 'eeeeeeeeeeeeeeeeffffffffffffffff' + + 'eeeeeeeeeeeeeeeeaaaaaaaaaaaaaaaa', + plainIsHex: true, + aad: 'feedfacedeadbeeffeedfacedeadbeefabaddad2', + ct: '48af93501fa62adbcd414cce6034d895' + + 'dda1bf8f132f042098661572e7483094' + + 'fd12e518ce062c98acee28d95df4416b' + + 'ed31a2f04476c18bb40c84a74b97dc5b', + tag: '16842d4fa186f56ab33256971fa110f4', tampered: false }, + { algo: 'aes-128-gcm', key: '6970787039613669314d623455536234', iv: '583673497131313748307652', plain: 'Hello World!', 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/fixtures/snapshot/crypto-provider-cipher-cache.js b/test/fixtures/snapshot/crypto-provider-cipher-cache.js new file mode 100644 index 000000000000..6d765f6b89bb --- /dev/null +++ b/test/fixtures/snapshot/crypto-provider-cipher-cache.js @@ -0,0 +1,55 @@ +'use strict'; + +const assert = require('assert'); +const { + createHash, + createCipheriv, + getCipherInfo, + getCiphers, + getHashes, + setFips, +} = require('crypto'); +const { setDeserializeMainFunction } = require('v8').startupSnapshot; + +const algorithm = 'aes-128-cbc-cts'; +const key = Buffer.alloc(16); +const iv = Buffer.alloc(16); +const legacyCipher = 'blowfish'; +const legacyHash = 'md4'; + +setFips(0); +assert(getCiphers().includes(algorithm)); +assert(getCiphers().includes(legacyCipher)); +assert(getHashes().includes(legacyHash)); +assert(getCipherInfo(algorithm)); +createCipheriv(algorithm, key, iv); +createHash(legacyHash).digest(); + +setDeserializeMainFunction(() => { + assert(getCiphers().includes(algorithm)); + assert(!getCiphers().includes(legacyCipher)); + assert(!getHashes().includes(legacyHash)); + assert(getCipherInfo(algorithm)); + createCipheriv(algorithm, key, iv); + assert.throws( + () => createCipheriv(legacyCipher, key, Buffer.alloc(8)), + { code: 'ERR_OSSL_EVP_UNSUPPORTED' }, + ); + assert.throws( + () => createHash(legacyHash), + { code: 'ERR_OSSL_EVP_UNSUPPORTED' }, + ); + + setFips(1); + assert(!getCiphers().includes(algorithm)); + assert.strictEqual(getCipherInfo(algorithm), undefined); + assert.throws(() => createCipheriv(algorithm, key, iv), { + code: 'ERR_CRYPTO_UNKNOWN_CIPHER', + }); + + setFips(0); + assert(getCiphers().includes(algorithm)); + assert(getCipherInfo(algorithm)); + createCipheriv(algorithm, key, iv); + console.log('provider crypto caches snapshot: ok'); +}); diff --git a/test/parallel/test-crypto-aes-wrap.js b/test/parallel/test-crypto-aes-wrap.js index 951e93d728e3..b6561524dc92 100644 --- a/test/parallel/test-crypto-aes-wrap.js +++ b/test/parallel/test-crypto-aes-wrap.js @@ -63,3 +63,146 @@ const key3 = Buffer.from('29c9eab5ed5ad44134a1437fe2e673b4d88a5b7c72e68454fea087 const msg = decipher.update(cipher.update(text, 'utf8'), 'buffer', 'utf8'); assert.strictEqual(msg, text, `${algorithm} test case failed`); }); + +const kwIV = Buffer.alloc(8, 0xa6); +const kwpIV = Buffer.from('a65959a6', 'hex'); + +// NIST SP 800-38F known-answer vectors. +[ + { + algorithm: 'aes-128-wrap', + key: '000102030405060708090a0b0c0d0e0f', + plaintext: '00112233445566778899aabbccddeeff', + ciphertext: '1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5', + iv: kwIV, + }, + { + algorithm: 'aes-192-wrap', + key: '000102030405060708090a0b0c0d0e0f1011121314151617', + plaintext: '00112233445566778899aabbccddeeff', + ciphertext: '96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d', + iv: kwIV, + }, + { + algorithm: 'aes-256-wrap', + key: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + plaintext: '00112233445566778899aabbccddeeff', + ciphertext: '64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7', + iv: kwIV, + }, + { + algorithm: 'aes-128-wrap-pad', + key: '6decf10a1caf8e3b80c7a4be8c9c84e8', + plaintext: '49', + ciphertext: '01a7d657fc4a5b216f261cca4d052c2b', + iv: kwpIV, + }, + { + algorithm: 'aes-192-wrap-pad', + key: '9ca11078baebc1597a68ce2fe3fc79a201626575252b8860', + plaintext: '76', + ciphertext: '866bc0ae30e290bb20a0dab31a6e7165', + iv: kwpIV, + }, + { + algorithm: 'aes-256-wrap-pad', + key: '95da2700ca6fd9a52554ee2a8df1386f5b94a1a60ed8a4aef60a8d61ab5f225a', + plaintext: 'd1', + ciphertext: '06ba7ae6f3248cfdcf267507fa001bc4', + iv: kwpIV, + }, + { + algorithm: 'aes-128-wrap-inv', + key: 'e88ba734ea243480a6129366753b58eb', + plaintext: 'd140ac16a44c1c2b3f47037ea8898a3e', + ciphertext: '600861ee14320006f0ae55c46d5e1ebf3303751df7f038df', + iv: kwIV, + }, + { + algorithm: 'aes-192-wrap-inv', + key: '370c715135b44eb3773b1aff833bcd28b59aee866d4a36b3', + plaintext: 'eae0f60f1cf33d5b75869e84c764a04e', + ciphertext: 'ea4ba4add8add19950ca491d109ffa08f90312693055677a', + iv: kwIV, + }, + { + algorithm: 'aes-256-wrap-inv', + key: 'de982f7c871f78e37462e2f48a62eecb2da81a10799c6ebf2bee8c786b624b0e', + plaintext: 'ecafc437d9f1643c7645c2416c14c003', + ciphertext: 'aec02ddb3f6de1f99103c6042dfc9001eb3cf56d9c2a11f7', + iv: kwIV, + }, + { + algorithm: 'aes-128-wrap-pad-inv', + key: '1c321a356b0ee25e30de2d618c1facbe', + plaintext: '42', + ciphertext: '3ddf22da3080a1a5252574c76f833790', + iv: kwpIV, + }, + { + algorithm: 'aes-192-wrap-pad-inv', + key: 'fe3fe235bb36dcf03f01cbf32cc98a3abf10ab3d608d3b30', + plaintext: '1d2b7fc29991bafaf7', + ciphertext: 'c11afb3c0de263dfb9b672a5f81fe0b9acfe9c407691f332', + iv: kwpIV, + }, + { + algorithm: 'aes-256-wrap-pad-inv', + key: '148a3fa618a6998c30b9f0f67922354a3747f2fa2e4d2e0b7af9582d6f548fee', + plaintext: '441125592acf9e5208dcd558a7ac0034d15530dbad7a2913963da0cbf60aa3', + ciphertext: '23f26a9476829885055694062c89b86399e8d6125509c9e88bb0a5b5113f4bfc8d34a62cba3c9eee', + iv: kwpIV, + }, +].forEach(({ algorithm, key, plaintext, ciphertext, iv }) => { + if (!crypto.getCiphers().includes(algorithm)) { + common.printSkipMessage(`Skipping unsupported ${algorithm} test case`); + return; + } + + const keyBuffer = Buffer.from(key, 'hex'); + const plaintextBuffer = Buffer.from(plaintext, 'hex'); + const expected = Buffer.from(ciphertext, 'hex'); + const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv); + const actual = Buffer.concat([ + cipher.update(plaintextBuffer), + cipher.final(), + ]); + assert.deepStrictEqual(actual, expected, `${algorithm} wrap failed`); + + const decipher = crypto.createDecipheriv(algorithm, keyBuffer, iv); + const unwrapped = Buffer.concat([ + decipher.update(actual), + decipher.final(), + ]); + assert.deepStrictEqual( + unwrapped, plaintextBuffer, `${algorithm} unwrap failed`); +}); + +{ + const algorithm = crypto.getCiphers().includes('aes-128-wrap-inv') ? + 'aes-128-wrap-inv' : 'aes128-wrap'; + if (!crypto.getCiphers().includes(algorithm)) { + common.printSkipMessage(`Skipping unsupported ${algorithm} state tests`); + } else { + const key = Buffer.from('e88ba734ea243480a6129366753b58eb', 'hex'); + const iv = Buffer.alloc(8, 0xa6); + const plaintextParts = [Buffer.alloc(16), Buffer.alloc(16, 1)]; + const wrappedParts = plaintextParts.map((plaintext) => { + const cipher = crypto.createCipheriv(algorithm, key, iv); + return Buffer.concat([cipher.update(plaintext), cipher.final()]); + }); + + for (const [create, inputParts] of [ + [crypto.createCipheriv, plaintextParts], + [crypto.createDecipheriv, wrappedParts], + ]) { + const withoutUpdate = create(algorithm, key, iv); + assert.throws(() => withoutUpdate.final(), /Unsupported state/); + + const multipleUpdates = create(algorithm, key, iv); + multipleUpdates.update(inputParts[0]); + assert.throws(() => multipleUpdates.update(inputParts[1]), + /Trying to add data in unsupported state/); + } + } +} diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index f27fcfa22dbf..0def68e93a04 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -63,7 +63,7 @@ for (const test of TEST_CASES) { continue; } - const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo); + const isCCM = /^(?:aes-(?:128|192|256)|sm4)-ccm$/.test(test.algo); const isOCB = /^aes-(128|192|256)-ocb$/.test(test.algo); const isSIV = /^aes-(128|192|256)-siv$/.test(test.algo); @@ -963,6 +963,7 @@ if (!fips3 && !process.features.openssl_is_boringssl) { if (ciphers.includes('aes-128-ccm')) { const key = crypto.randomBytes(16); const nonce = crypto.randomBytes(13); + const authError = /Unsupported state or unable to authenticate data/; const cipher = crypto.createCipheriv('aes-128-ccm', key, nonce, { authTagLength: 16, @@ -986,6 +987,47 @@ if (ciphers.includes('aes-128-ccm')) { decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); decipher.update(new DataView(new ArrayBuffer(0))); decipher.final(); + + const invalidTag = Buffer.from(tag); + invalidTag[0] ^= 0xff; + + { + const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + decipher.setAuthTag(tag); + decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); + assert.throws(() => decipher.final(), authError); + } + + { + const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); + assert.throws(() => decipher.final(), authError); + } + + { + const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + decipher.setAuthTag(invalidTag); + decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); + decipher.update(Buffer.alloc(0)); + assert.throws(() => decipher.final(), authError); + } + + { + const decipher = crypto.createDecipheriv('aes-128-ccm', key, nonce, { + authTagLength: 16, + }); + decipher.setAuthTag(tag); + decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); + decipher.update(Buffer.alloc(0)); + assert.throws(() => decipher.update(Buffer.alloc(0)), errMessages.state); + decipher.final(); + } } } else { common.printSkipMessage('Skipping unsupported aes-128-ccm test'); diff --git a/test/parallel/test-crypto-cipherbase-options-fast-path.js b/test/parallel/test-crypto-cipherbase-options-fast-path.js new file mode 100644 index 000000000000..9649b61fa7c5 --- /dev/null +++ b/test/parallel/test-crypto-cipherbase-options-fast-path.js @@ -0,0 +1,130 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createCipheriv, + createDecipheriv, +} = require('crypto'); + +const key = Buffer.alloc(16); +const gcmIv = Buffer.alloc(12); +const cbcIv = Buffer.alloc(16); + +for (const create of [createCipheriv, createDecipheriv]) { + // None of these supply either of the extended cipher options. + create('aes-128-gcm', key, gcmIv); + create('aes-128-gcm', key, gcmIv, null); + create('aes-128-gcm', key, gcmIv, {}); + create('aes-128-gcm', key, gcmIv, undefined); + create('aes-128-gcm', key, gcmIv, { authTagLength: 16 }); + + for (const options of [ + { ctsMode: null }, + { ctsMode: undefined }, + { xtsStandard: null }, + { xtsStandard: undefined }, + { ctsMode: null, xtsStandard: undefined }, + ]) { + create('aes-128-gcm', key, gcmIv, options); + } + + const accesses = []; + create('aes-128-gcm', key, gcmIv, { + get authTagLength() { + accesses.push('authTagLength'); + return 16; + }, + get ctsMode() { + accesses.push('ctsMode'); + return null; + }, + get xtsStandard() { + accesses.push('xtsStandard'); + return undefined; + }, + }); + assert.deepStrictEqual( + accesses, + ['authTagLength', 'ctsMode', 'xtsStandard']); + + const extendedAccesses = []; + assert.throws( + () => create('aes-128-cbc', key, cbcIv, { + get authTagLength() { + extendedAccesses.push('authTagLength'); + return null; + }, + get ctsMode() { + extendedAccesses.push('ctsMode'); + return 'CS1'; + }, + get xtsStandard() { + extendedAccesses.push('xtsStandard'); + return null; + }, + }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + assert.deepStrictEqual( + extendedAccesses, + ['authTagLength', 'ctsMode', 'xtsStandard']); + + const invalidAuthTagAccesses = []; + assert.throws(() => create('aes-128-cbc', key, cbcIv, { + get authTagLength() { + invalidAuthTagAccesses.push('authTagLength'); + return -2; + }, + get ctsMode() { + invalidAuthTagAccesses.push('ctsMode'); + return undefined; + }, + }), { code: 'ERR_INVALID_ARG_VALUE' }); + assert.deepStrictEqual(invalidAuthTagAccesses, ['authTagLength']); + + const invalidCtsTypeAccesses = []; + assert.throws(() => create('aes-128-cbc', key, cbcIv, { + get ctsMode() { + invalidCtsTypeAccesses.push('ctsMode'); + return 1; + }, + get xtsStandard() { + invalidCtsTypeAccesses.push('xtsStandard'); + return undefined; + }, + }), { code: 'ERR_INVALID_ARG_TYPE' }); + assert.deepStrictEqual(invalidCtsTypeAccesses, ['ctsMode']); + + const invalidCtsValueAccesses = []; + assert.throws(() => create('aes-128-cbc', key, cbcIv, { + get ctsMode() { + invalidCtsValueAccesses.push('ctsMode'); + return 'CS4'; + }, + get xtsStandard() { + invalidCtsValueAccesses.push('xtsStandard'); + return undefined; + }, + }), { code: 'ERR_INVALID_ARG_VALUE' }); + assert.deepStrictEqual( + invalidCtsValueAccesses, + ['ctsMode', 'xtsStandard']); + + assert.throws( + () => create('aes-128-cbc', key, cbcIv, { xtsStandard: 'GB' }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + + for (const ctsMode of ['', 'CS4']) { + assert.throws( + () => create('aes-128-cbc', key, cbcIv, { ctsMode }), + { code: 'ERR_INVALID_ARG_VALUE' }); + } + for (const xtsStandard of ['', 'IEEE-1619']) { + assert.throws( + () => create('aes-128-cbc', key, cbcIv, { xtsStandard }), + { code: 'ERR_INVALID_ARG_VALUE' }); + } +} diff --git a/test/parallel/test-crypto-cipheriv-cbc-cts.js b/test/parallel/test-crypto-cipheriv-cbc-cts.js new file mode 100644 index 000000000000..c3447b2908b3 --- /dev/null +++ b/test/parallel/test-crypto-cipheriv-cbc-cts.js @@ -0,0 +1,123 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createCipheriv, + createDecipheriv, + getCiphers, +} = require('crypto'); + +const algorithm = 'aes-128-cbc-cts'; +const key = Buffer.from('636869636b656e207465726979616b69', 'hex'); +const iv = Buffer.alloc(16); + +for (const create of [createCipheriv, createDecipheriv]) { + for (const ctsMode of ['cs1', 'CS4', '']) { + assert.throws( + () => create('aes-128-cbc', key, iv, { ctsMode }), + { code: 'ERR_INVALID_ARG_VALUE' }); + } + for (const ctsMode of [1, true, {}]) { + assert.throws( + () => create('aes-128-cbc', key, iv, { ctsMode }), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + assert.throws( + () => create('aes-128-cbc', key, iv, { ctsMode: 'CS1' }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); +} + +if (!getCiphers().includes(algorithm)) { + common.printSkipMessage(`unsupported ${algorithm}`); + return; +} + +// OpenSSL AES-128-CBC-CTS CS1 test vector. +const plaintext = Buffer.from('4920776f756c64206c696b652074686520', + 'hex'); +const expected = Buffer.from('97c6353568f2bf8cb4d8a580362da7ff7f', + 'hex'); + +const cipher = createCipheriv(algorithm, key, iv); +const ciphertext = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), +]); +assert.deepStrictEqual(ciphertext, expected); + +const decipher = createDecipheriv(algorithm, key, iv); +const decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), +]); +assert.deepStrictEqual(decrypted, plaintext); + +const vectors = [ + { + plaintext, + ciphertext: { + CS1: expected, + CS2: Buffer.from('c6353568f2bf8cb4d8a580362da7ff7f97', 'hex'), + CS3: Buffer.from('c6353568f2bf8cb4d8a580362da7ff7f97', 'hex'), + }, + }, + { + plaintext: Buffer.from( + '4920776f756c64206c696b6520746865' + + '2047656e6572616c2047617527732043', 'hex'), + ciphertext: { + CS1: Buffer.from( + '97687268d6ecccc0c07b25e25ecfe584' + + '39312523a78662d5be7fcbcc98ebf5a8', 'hex'), + CS2: Buffer.from( + '97687268d6ecccc0c07b25e25ecfe584' + + '39312523a78662d5be7fcbcc98ebf5a8', 'hex'), + CS3: Buffer.from( + '39312523a78662d5be7fcbcc98ebf5a8' + + '97687268d6ecccc0c07b25e25ecfe584', 'hex'), + }, + }, +]; + +for (const { plaintext, ciphertext } of vectors) { + for (const ctsMode of ['CS1', 'CS2', 'CS3']) { + const cipher = createCipheriv(algorithm, key, iv, { ctsMode }); + const encrypted = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + assert.deepStrictEqual(encrypted, ciphertext[ctsMode]); + + const decipher = createDecipheriv(algorithm, key, iv, { ctsMode }); + const decrypted = Buffer.concat([ + decipher.update(encrypted), + decipher.final(), + ]); + assert.deepStrictEqual(decrypted, plaintext); + } +} + +const tooShort = createCipheriv(algorithm, key, iv); +assert.throws(() => tooShort.update(Buffer.alloc(15)), + /Trying to add data in unsupported state/); +assert.deepStrictEqual(Buffer.concat([ + tooShort.update(plaintext), + tooShort.final(), +]), expected); + +for (const [create, input] of [ + [createCipheriv, plaintext], + [createDecipheriv, expected], +]) { + const withoutUpdate = create(algorithm, key, iv); + assert.throws(() => withoutUpdate.final(), /Unsupported state/); + + const multipleUpdates = create(algorithm, key, iv); + multipleUpdates.update(input.subarray(0, 16)); + assert.throws(() => multipleUpdates.update(input.subarray(16)), + /Trying to add data in unsupported state/); +} diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index b1965c5a80c1..d2c216924ea8 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -85,6 +85,84 @@ function testCipher3(key, iv) { `encryption/decryption with key ${key} and iv ${iv}`); } +function testSm4Xts() { + const aesKey = Buffer.alloc(16); + const aesIv = Buffer.alloc(16); + for (const create of [crypto.createCipheriv, crypto.createDecipheriv]) { + for (const xtsStandard of ['gb', 'IEEE-1619', '']) { + assert.throws( + () => create('aes-128-cbc', aesKey, aesIv, { xtsStandard }), + { code: 'ERR_INVALID_ARG_VALUE' }); + } + for (const xtsStandard of [1, true, {}]) { + assert.throws( + () => create('aes-128-cbc', aesKey, aesIv, { xtsStandard }), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + assert.throws( + () => create('aes-128-cbc', aesKey, aesIv, { xtsStandard: 'GB' }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + } + + if (!crypto.getCiphers().includes('sm4-xts')) { + common.printSkipMessage('unsupported sm4-xts test'); + return; + } + + // GB/T 17964-2021 + const key = Buffer.from( + '2B7E151628AED2A6ABF7158809CF4F3C' + + '000102030405060708090A0B0C0D0E0F', 'hex'); + const iv = Buffer.from('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF', 'hex'); + const plaintext = Buffer.from( + '6BC1BEE22E409F96E93D7E117393172A' + + 'AE2D8A571E03AC9C9EB76FAC45AF8E51' + + '30C81C46A35CE411E5FBC1191A0A52EF' + + 'F69F2445DF4F9B17', 'hex'); + const vectors = [ + { + options: undefined, + ciphertext: Buffer.from( + 'E9538251C71D7B80BBE4483FEF497BD1' + + '2C5C581BD6242FC51E08964FB4F60FDB' + + '0BA42F63499279213D318D2C11F6886E' + + '903BE7F93A1B3479', 'hex'), + }, + { + options: { xtsStandard: 'GB' }, + ciphertext: Buffer.from( + 'E9538251C71D7B80BBE4483FEF497BD1' + + '2C5C581BD6242FC51E08964FB4F60FDB' + + '0BA42F63499279213D318D2C11F6886E' + + '903BE7F93A1B3479', 'hex'), + }, + { + options: { xtsStandard: 'IEEE' }, + ciphertext: Buffer.from( + 'E9538251C71D7B80BBE4483FEF497BD1' + + 'B3DB1A3E60408C575D63FF7DB39F8326' + + '0869F9E2585FEC9F0B863BF8FD784B86' + + '27D16C0DB6D2CFC7', 'hex'), + }, + ]; + + for (const { options, ciphertext } of vectors) { + const cipher = crypto.createCipheriv('sm4-xts', key, iv, options); + const encrypted = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + assert.deepStrictEqual(encrypted, ciphertext); + + const decipher = crypto.createDecipheriv('sm4-xts', key, iv, options); + const decrypted = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + assert.deepStrictEqual(decrypted, plaintext); + } +} + { const Cipheriv = crypto.Cipheriv; const algorithm = fips3 ? 'aes-128-cbc' : 'des-ede3-cbc'; @@ -167,6 +245,7 @@ if (!isFipsEnabled) { testCipher3(Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'), Buffer.from('A6A6A6A6A6A6A6A6', 'hex')); } +testSm4Xts(); // Zero-sized IV or null should be accepted in ECB mode. crypto.createCipheriv('aes-128-ecb', Buffer.alloc(16), Buffer.alloc(0)); diff --git a/test/parallel/test-crypto-cipheriv-xts.js b/test/parallel/test-crypto-cipheriv-xts.js new file mode 100644 index 000000000000..236ff578f8f8 --- /dev/null +++ b/test/parallel/test-crypto-cipheriv-xts.js @@ -0,0 +1,71 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createCipheriv, + createDecipheriv, + getCiphers, +} = require('crypto'); + +const iv = Buffer.from('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', 'hex'); +const plaintext = Buffer.from( + '000102030405060708090a0b0c0d0e0f10', 'hex'); +const cases = [ + { + algorithm: 'aes-128-xts', + key: Buffer.from( + '000102030405060708090a0b0c0d0e0f' + + '101112131415161718191a1b1c1d1e1f', 'hex'), + }, + { + algorithm: 'sm4-xts', + key: Buffer.from( + '2b7e151628aed2a6abf7158809cf4f3c' + + '000102030405060708090a0b0c0d0e0f', 'hex'), + }, +]; + +for (const { algorithm, key } of cases) { + if (!getCiphers().includes(algorithm)) { + common.printSkipMessage(`unsupported ${algorithm} test`); + continue; + } + + const cipher = createCipheriv(algorithm, key, iv); + const ciphertext = cipher.update(plaintext); + assert.strictEqual(ciphertext.length, plaintext.length); + assert.deepStrictEqual(cipher.final(), Buffer.alloc(0)); + + const decipher = createDecipheriv(algorithm, key, iv); + assert.deepStrictEqual(decipher.update(ciphertext), plaintext); + assert.deepStrictEqual(decipher.final(), Buffer.alloc(0)); + + for (const [create, input, expected] of [ + [createCipheriv, plaintext, ciphertext], + [createDecipheriv, ciphertext, plaintext], + ]) { + const withoutUpdate = create(algorithm, key, iv); + assert.throws(() => withoutUpdate.final(), /Unsupported state/); + + const failedUpdate = create(algorithm, key, iv); + assert.throws(() => failedUpdate.update(Buffer.alloc(15)), + /Trying to add data in unsupported state/); + assert.throws(() => failedUpdate.final(), /Unsupported state/); + + const retry = create(algorithm, key, iv); + assert.throws(() => retry.update(Buffer.alloc(15)), + /Trying to add data in unsupported state/); + assert.deepStrictEqual(retry.update(input), expected); + assert.deepStrictEqual(retry.final(), Buffer.alloc(0)); + + const oneUpdate = create(algorithm, key, iv); + assert.deepStrictEqual(oneUpdate.update(input), expected); + assert.throws(() => oneUpdate.update(Buffer.alloc(16)), + /Trying to add data in unsupported state/); + assert.deepStrictEqual(oneUpdate.final(), Buffer.alloc(0)); + } +} diff --git a/test/parallel/test-crypto-getcipherinfo.js b/test/parallel/test-crypto-getcipherinfo.js index 0a984f0ce54e..5afd2e5a4208 100644 --- a/test/parallel/test-crypto-getcipherinfo.js +++ b/test/parallel/test-crypto-getcipherinfo.js @@ -5,11 +5,12 @@ if (!common.hasCrypto) common.skip('missing crypto'); const { + createCipheriv, createHash, getCiphers, getCipherInfo, } = require('crypto'); -const { hasFIPS } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL3 } = require('../common/crypto'); const assert = require('assert'); @@ -17,6 +18,42 @@ const ciphers = getCiphers(); assert.strictEqual(getCipherInfo(-1), undefined); assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined); +if (hasOpenSSL3) { + assert.deepStrictEqual( + ciphers.filter((cipher) => cipher.includes('cbc-hmac')), []); + for (const cipher of [ + 'null', + 'aes-128-cbc-hmac-sha1', + 'aes-256-cbc-hmac-sha1', + 'aes-128-cbc-hmac-sha256', + 'aes-256-cbc-hmac-sha256', + 'aes-128-cbc-hmac-sha1-etm', + 'aes-192-cbc-hmac-sha1-etm', + 'aes-256-cbc-hmac-sha1-etm', + 'aes-128-cbc-hmac-sha256-etm', + 'aes-192-cbc-hmac-sha256-etm', + 'aes-256-cbc-hmac-sha256-etm', + 'aes-128-cbc-hmac-sha512-etm', + 'aes-192-cbc-hmac-sha512-etm', + 'aes-256-cbc-hmac-sha512-etm', + ]) { + assert(!ciphers.includes(cipher)); + assert.strictEqual(getCipherInfo(cipher), undefined); + assert.throws( + () => createCipheriv(cipher, Buffer.alloc(16), Buffer.alloc(16)), { + code: 'ERR_CRYPTO_UNKNOWN_CIPHER', + }); + } +} + +if (ciphers.includes('aes-128-wrap-inv')) { + const alias = 'aes128-wrap-inv'; + assert(ciphers.includes(alias)); + assert.deepStrictEqual(getCipherInfo(alias), + getCipherInfo('aes-128-wrap-inv')); +} +assert(!ciphers.some((cipher) => /^\d+(?:\.\d+)+$/.test(cipher))); + if (!process.features.openssl_is_boringssl) { // A failed provider fetch must not contaminate the OpenSSL error queue. assert.throws(() => createHash('sha256', { outputLength: 28 }), { @@ -96,6 +133,19 @@ if (hasFIPS(3)) { common.printSkipMessage('Skipping unsupported aes-128-ocb test cases'); } +if (ciphers.includes('aes-128-cbc-cts')) { + const info = getCipherInfo('aes-128-cbc-cts'); + assert.strictEqual(info.name, 'aes-128-cbc-cts'); + assert.strictEqual(info.mode, 'cbc'); + assert.strictEqual(info.keyLength, 16); + assert.strictEqual(info.blockSize, 16); + assert.strictEqual(info.ivLength, 16); + assert(getCipherInfo('aes-128-cbc-cts', { ivLength: 16 })); + assert(!getCipherInfo('aes-128-cbc-cts', { ivLength: 15 })); +} else { + common.printSkipMessage('Skipping unsupported aes-128-cbc-cts test cases'); +} + if (ciphers.includes('aes-128-siv')) { const info = getCipherInfo('aes-128-siv'); assert.strictEqual(info.name, 'aes-128-siv'); @@ -119,3 +169,20 @@ if (ciphers.includes('aes-128-gcm-siv')) { } else { common.printSkipMessage('Skipping unsupported aes-128-gcm-siv test cases'); } + +for (const [name, mode, keyLength, ivLength] of [ + ['sm4-gcm', 'gcm', 16, 12], + ['sm4-ccm', 'ccm', 16, 12], + ['sm4-xts', 'xts', 32, 16], +]) { + if (ciphers.includes(name)) { + const info = getCipherInfo(name); + assert.strictEqual(info.name, name); + assert.strictEqual(info.mode, mode); + assert.strictEqual(info.nid, undefined); + assert.strictEqual(info.keyLength, keyLength); + assert.strictEqual(info.ivLength, ivLength); + } else { + common.printSkipMessage(`Skipping unsupported ${name} test cases`); + } +} diff --git a/test/parallel/test-crypto-provider-cipher-cache-snapshot.js b/test/parallel/test-crypto-provider-cipher-cache-snapshot.js new file mode 100644 index 000000000000..1afc5df8d94d --- /dev/null +++ b/test/parallel/test-crypto-provider-cipher-cache-snapshot.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { hasOpenSSL3 } = require('../common/crypto'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); + +if (!hasOpenSSL3) + common.skip('this test requires OpenSSL 3.x'); + +const entry = fixtures.path('snapshot', 'crypto-provider-cipher-cache.js'); +const buildEnv = { + OPENSSL_CONF: fixtures.path( + 'openssl3-conf', 'legacy_provider_enabled.cnf'), +}; +const runEnv = { + OPENSSL_CONF: fixtures.path('openssl3-conf', 'default_only.cnf'), +}; + +tmpdir.refresh(); +buildSnapshot(entry, buildEnv); +const { stdout } = runWithSnapshot(undefined, runEnv); +assert.match(stdout, /provider crypto caches snapshot: ok/); diff --git a/test/parallel/test-crypto-provider-cipher-cache.js b/test/parallel/test-crypto-provider-cipher-cache.js new file mode 100644 index 000000000000..f42564e869d2 --- /dev/null +++ b/test/parallel/test-crypto-provider-cipher-cache.js @@ -0,0 +1,180 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL3 } = require('../common/crypto'); +if (!hasOpenSSL3) + common.skip('this test requires OpenSSL 3.x'); + +const assert = require('assert'); +const { + createCipheriv, + getCipherInfo, + getCiphers, + getFips, + getHashes, + setFips, +} = require('crypto'); +const { internalBinding } = require('internal/test/binding'); +const { Worker } = require('worker_threads'); + +const algorithm = 'camellia-128-cbc-cts'; +const hashAlgorithm = 'md5'; +const originalFips = getFips(); +setFips(0); + +if (!getCiphers().includes(algorithm)) { + common.skip(`${algorithm} is not supported`); +} +assert(getHashes().includes(hashAlgorithm)); + +const binding = internalBinding('crypto'); +const generation = binding.getFipsCryptoGeneration(); +setFips(0); +assert.strictEqual(binding.getFipsCryptoGeneration(), generation); + +const ciphers = getCiphers(); +ciphers.length = 0; +assert(getCiphers().includes(algorithm)); + +const info = getCipherInfo(algorithm); +assert(info); +assert.deepStrictEqual(getCipherInfo(algorithm.toUpperCase()), info); +assert.deepStrictEqual(getCipherInfo(algorithm), info); +assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined); +assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined); + +const key = Buffer.alloc(16); +const iv = Buffer.alloc(16); +const plaintext = Buffer.alloc(32); +const liveCipher = createCipheriv(algorithm, key, iv); + +const worker = new Worker(` + 'use strict'; + const { + createHash, + createCipheriv, + getCipherInfo, + getCiphers, + getHashes, + } = require('crypto'); + const { internalBinding } = require('internal/test/binding'); + const { parentPort, workerData } = require('worker_threads'); + + const binding = internalBinding('crypto'); + const key = Buffer.from(workerData.key); + const iv = Buffer.from(workerData.iv); + const plaintext = Buffer.from(workerData.plaintext); + const liveCipher = createCipheriv(workerData.algorithm, key, iv); + + getHashes(); + getCiphers(); + getCipherInfo(workerData.algorithm); + parentPort.postMessage({ + phase: 'warm', + generation: binding.getFipsCryptoGeneration(), + }); + + parentPort.on('message', (phase) => { + if (phase === 'fips-on') { + let errorCode; + try { + createCipheriv(workerData.algorithm, key, iv); + } catch (error) { + errorCode = error.code; + } + const output = Buffer.concat([ + liveCipher.update(plaintext), + liveCipher.final(), + ]); + parentPort.postMessage({ + phase, + errorCode, + generation: binding.getFipsCryptoGeneration(), + hasCipher: getCiphers().includes(workerData.algorithm), + hasHash: getHashes().includes(workerData.hashAlgorithm), + hasInfo: getCipherInfo(workerData.algorithm) !== undefined, + outputLength: output.length, + }); + } else if (phase === 'fips-off') { + const cipher = createCipheriv(workerData.algorithm, key, iv); + const hash = createHash(workerData.hashAlgorithm).digest('hex'); + const output = Buffer.concat([ + cipher.update(plaintext), + cipher.final(), + ]); + parentPort.postMessage({ + phase, + generation: binding.getFipsCryptoGeneration(), + hasHash: getHashes().includes(workerData.hashAlgorithm), + hasCipher: getCiphers().includes(workerData.algorithm), + hasInfo: getCipherInfo(workerData.algorithm) !== undefined, + hash, + outputLength: output.length, + }); + } else { + parentPort.close(); + } + }); +`, { + eval: true, + workerData: { algorithm, hashAlgorithm, key, iv, plaintext }, +}); + +let enabledGeneration; +worker.on('message', common.mustCall((message) => { + if (message.phase === 'warm') { + assert.strictEqual(message.generation, generation); + + setFips(1); + enabledGeneration = binding.getFipsCryptoGeneration(); + assert.strictEqual(enabledGeneration, generation + 1n); + assert(!getCiphers().includes(algorithm)); + assert(!getHashes().includes(hashAlgorithm)); + assert.strictEqual(getCipherInfo(algorithm), undefined); + assert.throws(() => createCipheriv(algorithm, key, iv), { + code: 'ERR_CRYPTO_UNKNOWN_CIPHER', + }); + + const output = Buffer.concat([ + liveCipher.update(plaintext), + liveCipher.final(), + ]); + assert.strictEqual(output.length, plaintext.length); + worker.postMessage('fips-on'); + } else if (message.phase === 'fips-on') { + assert.strictEqual(message.generation, enabledGeneration); + assert.strictEqual(message.hasCipher, false); + assert.strictEqual(message.hasHash, false); + assert.strictEqual(message.hasInfo, false); + assert.strictEqual(message.errorCode, 'ERR_CRYPTO_UNKNOWN_CIPHER'); + assert.strictEqual(message.outputLength, plaintext.length); + + setFips(0); + assert.strictEqual( + binding.getFipsCryptoGeneration(), enabledGeneration + 1n); + assert(getHashes().includes(hashAlgorithm)); + assert(getCiphers().includes(algorithm)); + assert(getCipherInfo(algorithm)); + worker.postMessage('fips-off'); + } else { + assert.strictEqual(message.phase, 'fips-off'); + assert.strictEqual( + message.generation, binding.getFipsCryptoGeneration()); + assert.strictEqual(message.hasCipher, true); + assert.strictEqual(message.hasHash, true); + assert.strictEqual(message.hasInfo, true); + assert.strictEqual( + message.hash, + 'd41d8cd98f00b204e9800998ecf8427e', + ); + assert.strictEqual(message.outputLength, plaintext.length); + worker.postMessage('done'); + setFips(originalFips); + } +}, 3)); +worker.on('error', common.mustNotCall()); +worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0))); 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..f7307c2f96e4 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; } @@ -806,6 +810,8 @@ export interface CryptoBinding { credential: InternalCryptoBinding.PreparedSecretKeyData, iv: InternalCryptoBinding.ByteSource | null, authTagLength?: number, + ctsMode?: 'CS1' | 'CS2' | 'CS3', + xtsStandard?: 'GB' | 'IEEE', ) => InternalCryptoBinding.CipherBaseHandle; DiffieHellman: new ( sizeOrKey: number | InternalCryptoBinding.ByteSource, @@ -818,6 +824,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 +947,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 +962,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;