From caedfcb513605149dac189b2a814e34393342185 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Mar 2025 16:43:43 -0800 Subject: [PATCH 01/21] src: refine ncrypto more MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An eventual goal for ncrypto is to completely abstract away details of working directly with openssl in order to make it easier to work with multiple different openssl/boringssl versions. As part of that we want to move away from direct reliance on specific openssl APIs in the runtime and instead go through the ncrypto abstractions. Not only does this help other runtimes trying to be compatible with Node.js, but it helps Node.js also by reducing the complexity of the crypto code in Node.js itself. PR-URL: https://github.com/nodejs/node/pull/57300 Reviewed-By: Yagiz Nizipli Note: Merge conflicts were resolved by Claude Code. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- include/ncrypto.h | 112 ++++++++++++++++++--- src/ncrypto.cpp | 242 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 309 insertions(+), 45 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index 4e27d0f..1e8f5bc 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -253,11 +253,45 @@ struct Buffer { size_t len = 0; }; +class Digest final { + public: + 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; + inline Digest& operator=(const EVP_MD* md) { + md_ = md; + return *this; + } + NCRYPTO_DISALLOW_MOVE(Digest) + + size_t size() const; + + inline const EVP_MD* get() const { return md_; } + inline operator const EVP_MD*() const { return md_; } + inline operator bool() const { return md_ != nullptr; } + + static const Digest MD5; + static const Digest SHA1; + static const Digest SHA256; + static const Digest SHA384; + static const Digest SHA512; + + static const Digest FromName(std::string_view name); + + private: + const EVP_MD* md_ = nullptr; +}; + DataPointer hashDigest(const Buffer& data, const EVP_MD* md); class Cipher final { public: + static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH; + static constexpr size_t MAX_IV_LENGTH = EVP_MAX_IV_LENGTH; + Cipher() = default; Cipher(const EVP_CIPHER* cipher) : cipher_(cipher) {} Cipher(const Cipher&) = default; @@ -280,15 +314,53 @@ class Cipher final { std::string_view getModeLabel() const; std::string_view getName() const; + bool isGcmMode() const; + bool isWrapMode() const; + bool isCtrMode() const; + bool isCcmMode() const; + bool isOcbMode() const; + bool isStreamMode() const; + bool isChaCha20Poly1305() const; + bool isSupportedAuthenticatedMode() const; + int bytesToKey(const Digest& digest, + const Buffer& input, + unsigned char* key, + unsigned char* iv) const; + static const Cipher FromName(std::string_view name); static const Cipher FromNid(int nid); static const Cipher FromCtx(const CipherCtxPointer& ctx); + using CipherNameCallback = std::function; + + // Iterates the known ciphers if the underlying implementation + // 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; + struct CipherParams { int padding; - const EVP_MD* digest; + Digest digest; const Buffer label; }; @@ -612,7 +684,8 @@ class CipherCtxPointer final { void reset(EVP_CIPHER_CTX* ctx = nullptr); EVP_CIPHER_CTX* release(); - void setFlags(int flags); + void setAllowWrap(); + bool setKeyLength(size_t length); bool setIvLength(size_t length); bool setAeadTag(const Buffer& tag); @@ -627,6 +700,11 @@ class CipherCtxPointer final { int getMode() const; int getNid() const; + bool isGcmMode() const; + bool isCcmMode() const; + bool isWrapMode() const; + bool isChaCha20Poly1305() const; + bool update(const Buffer& in, unsigned char* out, int* out_len, @@ -662,13 +740,13 @@ class EVPKeyCtxPointer final { bool setDsaParameters(uint32_t bits, std::optional q_bits); bool setEcParameters(int curve, int encoding); - bool setRsaOaepMd(const EVP_MD* md); - bool setRsaMgf1Md(const EVP_MD* md); + bool setRsaOaepMd(const Digest& md); + bool setRsaMgf1Md(const Digest& md); bool setRsaPadding(int padding); bool setRsaKeygenPubExp(BignumPointer&& e); bool setRsaKeygenBits(int bits); - bool setRsaPssKeygenMd(const EVP_MD* md); - bool setRsaPssKeygenMgf1Md(const EVP_MD* md); + bool setRsaPssKeygenMd(const Digest& md); + bool setRsaPssKeygenMgf1Md(const Digest& md); bool setRsaPssSaltlen(int salt_len); bool setRsaImplicitRejection(); bool setRsaOaepLabel(DataPointer&& data); @@ -945,6 +1023,8 @@ class SSLCtxPointer final { SSL_CTX_set_tlsext_status_arg(get(), nullptr); } + bool setCipherSuites(std::string_view ciphers); + static SSLCtxPointer NewServer(); static SSLCtxPointer NewClient(); static SSLCtxPointer New(const SSL_METHOD* method = TLS_method()); @@ -1073,7 +1153,7 @@ class X509View final { bool checkPrivateKey(const EVPKeyPointer& pkey) const; bool checkPublicKey(const EVPKeyPointer& pkey) const; - std::optional getFingerprint(const EVP_MD* method) const; + std::optional getFingerprint(const Digest& method) const; X509Pointer clone() const; @@ -1269,16 +1349,16 @@ class EVPMDCtxPointer final { void reset(EVP_MD_CTX* ctx = nullptr); EVP_MD_CTX* release(); - bool digestInit(const EVP_MD* digest); + bool digestInit(const Digest& digest); bool digestUpdate(const Buffer& in); DataPointer digestFinal(size_t length); bool digestFinalInto(Buffer* buf); size_t getExpectedSize(); std::optional signInit(const EVPKeyPointer& key, - const EVP_MD* digest); + const Digest& digest); std::optional verifyInit(const EVPKeyPointer& key, - const EVP_MD* digest); + const Digest& digest); DataPointer signOneShot(const Buffer& buf) const; DataPointer sign(const Buffer& buf) const; @@ -1313,7 +1393,7 @@ class HMACCtxPointer final { void reset(HMAC_CTX* ctx = nullptr); HMAC_CTX* release(); - bool init(const Buffer& buf, const EVP_MD* md); + bool init(const Buffer& buf, const Digest& md); bool update(const Buffer& buf); DataPointer digest(); bool digestInto(Buffer* buf); @@ -1414,20 +1494,20 @@ const EVP_CIPHER* getCipherByName(const std::string_view name); // Verify that the specified HKDF output length is valid for the given digest. // The maximum length for HKDF output for a given digest is 255 times the // hash size for the given digest algorithm. -bool checkHkdfLength(const EVP_MD* md, size_t length); +bool checkHkdfLength(const Digest& digest, size_t length); bool extractP1363(const Buffer& buf, unsigned char* dest, size_t n); -bool hkdfInfo(const EVP_MD* md, +bool hkdfInfo(const Digest& md, const Buffer& key, const Buffer& info, const Buffer& salt, size_t length, Buffer* out); -DataPointer hkdf(const EVP_MD* md, +DataPointer hkdf(const Digest& md, const Buffer& key, const Buffer& info, const Buffer& salt, @@ -1452,14 +1532,14 @@ DataPointer scrypt(const Buffer& pass, uint64_t maxmem, size_t length); -bool pbkdf2Into(const EVP_MD* md, +bool pbkdf2Into(const Digest& md, const Buffer& pass, const Buffer& salt, uint32_t iterations, size_t length, Buffer* out); -DataPointer pbkdf2(const EVP_MD* md, +DataPointer pbkdf2(const Digest& md, const Buffer& pass, const Buffer& salt, uint32_t iterations, diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 4b28ca8..9e01b15 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -1234,7 +1234,7 @@ X509View X509View::From(const SSLCtxPointer& ctx) { } std::optional X509View::getFingerprint( - const EVP_MD* method) const { + const Digest& method) const { unsigned int md_size; unsigned char md[EVP_MAX_MD_SIZE]; static constexpr char hex[] = "0123456789ABCDEF"; @@ -1751,17 +1751,17 @@ const EVP_CIPHER* getCipherByName(const std::string_view name) { return EVP_get_cipherbyname(name.data()); } -bool checkHkdfLength(const EVP_MD* md, size_t length) { +bool checkHkdfLength(const Digest& md, size_t length) { // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as // the output of the hash function. 255 is a hard limit because HKDF appends // an 8-bit counter to each HMAC'd message, starting at 1. static constexpr size_t kMaxDigestMultiplier = 255; - size_t max_length = EVP_MD_size(md) * kMaxDigestMultiplier; + size_t max_length = md.size() * kMaxDigestMultiplier; if (length > max_length) return false; return true; } -bool hkdfInfo(const EVP_MD* md, +bool hkdfInfo(const Digest& md, const Buffer& key, const Buffer& info, const Buffer& salt, @@ -1779,13 +1779,16 @@ bool hkdfInfo(const EVP_MD* md, if (salt.len > 0) { actual_salt = {reinterpret_cast(salt.data), salt.len}; } else { - actual_salt = {default_salt, static_cast(EVP_MD_size(md))}; + actual_salt = {default_salt, static_cast(md.size())}; } #ifndef NCRYPTO_NO_KDF_H auto ctx = EVPKeyCtxPointer::NewFromID(EVP_PKEY_HKDF); + // OpenSSL < 3.0.0 accepted only a void* as the argument of + // EVP_PKEY_CTX_set_hkdf_md. + const EVP_MD* md_ptr = md; if (!ctx || !EVP_PKEY_derive_init(ctx.get()) || - !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) || + !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md_ptr) || !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) { return false; } @@ -1830,7 +1833,7 @@ bool hkdfInfo(const EVP_MD* md, #endif } -DataPointer hkdf(const EVP_MD* md, +DataPointer hkdf(const Digest& md, const Buffer& key, const Buffer& info, const Buffer& salt, @@ -1895,7 +1898,7 @@ DataPointer scrypt(const Buffer& pass, return {}; } -bool pbkdf2Into(const EVP_MD* md, +bool pbkdf2Into(const Digest& md, const Buffer& pass, const Buffer& salt, uint32_t iterations, @@ -1908,12 +1911,13 @@ bool pbkdf2Into(const EVP_MD* md, return false; } + const EVP_MD* md_ptr = md; if (PKCS5_PBKDF2_HMAC(pass.data, pass.len, salt.data, salt.len, iterations, - md, + md_ptr, length, out->data)) { return true; @@ -1922,7 +1926,7 @@ bool pbkdf2Into(const EVP_MD* md, return false; } -DataPointer pbkdf2(const EVP_MD* md, +DataPointer pbkdf2(const Digest& md, const Buffer& pass, const Buffer& salt, uint32_t iterations, @@ -2885,6 +2889,17 @@ bool SSLCtxPointer::setGroups(const char* groups) { #endif } +bool SSLCtxPointer::setCipherSuites(std::string_view ciphers) { +#ifndef OPENSSL_IS_BORINGSSL + if (!ctx_) return false; + return SSL_CTX_set_ciphersuites(ctx_.get(), ciphers.data()); +#else + // BoringSSL does not allow API config of TLS 1.3 cipher suites. + // We treat this as a non-op. + return true; +#endif +} + // ============================================================================ const Cipher Cipher::FromName(std::string_view name) { @@ -2899,6 +2914,55 @@ const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) { return Cipher(EVP_CIPHER_CTX_cipher(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); + +bool Cipher::isGcmMode() const { + if (!cipher_) return false; + return getMode() == EVP_CIPH_GCM_MODE; +} + +bool Cipher::isWrapMode() const { + if (!cipher_) return false; + return getMode() == EVP_CIPH_WRAP_MODE; +} + +bool Cipher::isCtrMode() const { + if (!cipher_) return false; + return getMode() == EVP_CIPH_CTR_MODE; +} + +bool Cipher::isCcmMode() const { + if (!cipher_) return false; + return getMode() == EVP_CIPH_CCM_MODE; +} + +bool Cipher::isOcbMode() const { + if (!cipher_) return false; + return getMode() == EVP_CIPH_OCB_MODE; +} + +bool Cipher::isStreamMode() const { + if (!cipher_) return false; + return getMode() == EVP_CIPH_STREAM_CIPHER; +} + +bool Cipher::isChaCha20Poly1305() const { + if (!cipher_) return false; + return getNid() == NID_chacha20_poly1305; +} + int Cipher::getMode() const { if (!cipher_) return 0; return EVP_CIPHER_mode(cipher_); @@ -2975,6 +3039,82 @@ bool Cipher::isSupportedAuthenticatedMode() const { } } +int Cipher::bytesToKey(const Digest& digest, + const Buffer& input, + unsigned char* key, + unsigned char* iv) const { + return EVP_BytesToKey( + *this, Digest::MD5, nullptr, input.data, input.len, 1, key, iv); +} + +namespace { +struct CipherCallbackContext { + Cipher::CipherNameCallback cb; + void operator()(std::string_view name) { cb(name); } +}; + +#if OPENSSL_VERSION_MAJOR >= 3 +template +void array_push_back(const TypeName* evp_ref, + const char* from, + const char* to, + void* arg) { + if (from == nullptr) return; + + const TypeName* real_instance = getbyname(from); + if (!real_instance) return; + + const char* real_name = getname(real_instance); + if (!real_name) return; + + // 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). + TypeName* fetched = fetch_type(nullptr, real_name, nullptr); + if (fetched == nullptr) return; + + free_type(fetched); + auto& cb = *(static_cast(arg)); + cb(from); +} +#else +template +void array_push_back(const TypeName* evp_ref, + const char* from, + const char* to, + void* arg) { + if (!from) return; + auto& cb = *(static_cast(arg)); + cb(from); +} +#endif +} // namespace + +void Cipher::ForEach(Cipher::CipherNameCallback callback) { + ClearErrorOnReturn clearErrorOnReturn; + CipherCallbackContext context; + context.cb = std::move(callback); + + EVP_CIPHER_do_all_sorted( +#if OPENSSL_VERSION_MAJOR >= 3 + array_push_back, +#else + array_push_back, +#endif + &context); +} + // ============================================================================ CipherCtxPointer CipherCtxPointer::New() { @@ -3008,9 +3148,9 @@ EVP_CIPHER_CTX* CipherCtxPointer::release() { return ctx_.release(); } -void CipherCtxPointer::setFlags(int flags) { +void CipherCtxPointer::setAllowWrap() { if (!ctx_) return; - EVP_CIPHER_CTX_set_flags(ctx_.get(), flags); + EVP_CIPHER_CTX_set_flags(ctx_.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); } bool CipherCtxPointer::setKeyLength(size_t length) { @@ -3056,6 +3196,26 @@ int CipherCtxPointer::getNid() const { return EVP_CIPHER_CTX_nid(ctx_.get()); } +bool CipherCtxPointer::isGcmMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_GCM_MODE; +} + +bool CipherCtxPointer::isCcmMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_CCM_MODE; +} + +bool CipherCtxPointer::isWrapMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_WRAP_MODE; +} + +bool CipherCtxPointer::isChaCha20Poly1305() const { + if (!ctx_) return false; + return getNid() == NID_chacha20_poly1305; +} + bool CipherCtxPointer::init(const Cipher& cipher, bool encrypt, const unsigned char* key, @@ -3420,14 +3580,16 @@ bool EVPKeyCtxPointer::setEcParameters(int curve, int encoding) { EVP_PKEY_CTX_set_ec_param_enc(ctx_.get(), encoding) == 1; } -bool EVPKeyCtxPointer::setRsaOaepMd(const EVP_MD* md) { - if (md == nullptr || !ctx_) return false; - return EVP_PKEY_CTX_set_rsa_oaep_md(ctx_.get(), md) > 0; +bool EVPKeyCtxPointer::setRsaOaepMd(const Digest& md) { + if (!md || !ctx_) return false; + const EVP_MD* md_ptr = md; + return EVP_PKEY_CTX_set_rsa_oaep_md(ctx_.get(), md_ptr) > 0; } -bool EVPKeyCtxPointer::setRsaMgf1Md(const EVP_MD* md) { - if (md == nullptr || !ctx_) return false; - return EVP_PKEY_CTX_set_rsa_mgf1_md(ctx_.get(), md) > 0; +bool EVPKeyCtxPointer::setRsaMgf1Md(const Digest& md) { + if (!md || !ctx_) return false; + const EVP_MD* md_ptr = md; + return EVP_PKEY_CTX_set_rsa_mgf1_md(ctx_.get(), md_ptr) > 0; } bool EVPKeyCtxPointer::setRsaPadding(int padding) { @@ -3462,14 +3624,17 @@ bool EVPKeyCtxPointer::setRsaKeygenPubExp(BignumPointer&& e) { return false; } -bool EVPKeyCtxPointer::setRsaPssKeygenMd(const EVP_MD* md) { - if (md == nullptr || !ctx_) return false; - return EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx_.get(), md) > 0; +bool EVPKeyCtxPointer::setRsaPssKeygenMd(const Digest& md) { + if (!md || !ctx_) return false; + // OpenSSL < 3 accepts a void* for the md parameter. + const EVP_MD* md_ptr = md; + return EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx_.get(), md_ptr) > 0; } -bool EVPKeyCtxPointer::setRsaPssKeygenMgf1Md(const EVP_MD* md) { - if (md == nullptr || !ctx_) return false; - return EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx_.get(), md) > 0; +bool EVPKeyCtxPointer::setRsaPssKeygenMgf1Md(const Digest& md) { + if (!md || !ctx_) return false; + const EVP_MD* md_ptr = md; + return EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx_.get(), md_ptr) > 0; } bool EVPKeyCtxPointer::setRsaPssSaltlen(int salt_len) { @@ -3903,7 +4068,7 @@ EVP_MD_CTX* EVPMDCtxPointer::release() { return ctx_.release(); } -bool EVPMDCtxPointer::digestInit(const EVP_MD* digest) { +bool EVPMDCtxPointer::digestInit(const Digest& digest) { if (!ctx_) return false; return EVP_DigestInit_ex(ctx_.get(), digest, nullptr) > 0; } @@ -3969,7 +4134,7 @@ bool EVPMDCtxPointer::copyTo(const EVPMDCtxPointer& other) const { } std::optional EVPMDCtxPointer::signInit(const EVPKeyPointer& key, - const EVP_MD* digest) { + const Digest& digest) { EVP_PKEY_CTX* ctx = nullptr; if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { return std::nullopt; @@ -3978,7 +4143,7 @@ std::optional EVPMDCtxPointer::signInit(const EVPKeyPointer& key, } std::optional EVPMDCtxPointer::verifyInit( - const EVPKeyPointer& key, const EVP_MD* digest) { + const EVPKeyPointer& key, const Digest& digest) { EVP_PKEY_CTX* ctx = nullptr; if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { return std::nullopt; @@ -4075,9 +4240,10 @@ HMAC_CTX* HMACCtxPointer::release() { return ctx_.release(); } -bool HMACCtxPointer::init(const Buffer& buf, const EVP_MD* md) { +bool HMACCtxPointer::init(const Buffer& buf, const Digest& md) { if (!ctx_) return false; - return HMAC_Init_ex(ctx_.get(), buf.data, buf.len, md, nullptr) == 1; + const EVP_MD* md_ptr = md; + return HMAC_Init_ex(ctx_.get(), buf.data, buf.len, md_ptr, nullptr) == 1; } bool HMACCtxPointer::update(const Buffer& buf) { @@ -4218,6 +4384,24 @@ size_t Dsa::getDivisorLength() const { if (dsa_ == nullptr) return 0; return BignumPointer::GetBitCount(getQ()); } + +// ============================================================================ + +size_t Digest::size() const { + if (md_ == nullptr) return 0; + return EVP_MD_size(md_); +} + +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()); + +const Digest Digest::FromName(std::string_view name) { + return ncrypto::getDigestByName(name); +} + } // namespace ncrypto // =========================================================================== From cf77a601c5a1e0f15f12a6041c5c3e1600f5cbe1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 4 Mar 2025 14:17:59 -0800 Subject: [PATCH 02/21] src: cleanup crypto more MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use ncrypto APIs where appropriate * Remove obsolete no-longer used functions * Improve error handling a bit * move secure heap handling to ncrypto To simplify handling of boringssl/openssl, move secure heap impl to ncrypto. Overall the reduces the complexity of the code in crypto_util by eliminating additional ifdef branches. * simplify CryptoErrorStore::ToException a bit * simplify error handling in crypto_common * move curve utility methods to ncrypto * verify that released DataPointers aren't on secure heap The ByteSource does not currently know how to free a DataPointer allocated on the secure heap, so just verify. DataPointers on the secure heap are not something that users can allocate on their own. Their use is rare. Eventually ByteSource is going to be refactored around ncrypto APIs so these additional checks should be temporary. * simplify some ifdefs that are covered by ncrypto * cleanup some obsolete includes in crypto_util Merge conflicts were resolved by Claude Code. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- include/ncrypto.h | 38 ++++++++++++++- src/ncrypto.cpp | 117 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 139 insertions(+), 16 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index 1e8f5bc..e2e8965 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -52,6 +52,8 @@ using OPENSSL_SIZE_T = int; #ifdef NCRYPTO_BSSL_NEEDS_DH_PRIMES #include "dh-primes.h" #endif // NCRYPTO_BSSL_NEEDS_DH_PRIMES +// FIXME: This should be removed when patch 0037 is applied +#define OPENSSL_TLS_SECURITY_LEVEL 1 #endif // OPENSSL_IS_BORINGSSL namespace ncrypto { @@ -549,6 +551,11 @@ class Ec final { inline operator bool() const { return ec_ != nullptr; } inline operator OSSL3_CONST EC_KEY*() const { return ec_; } + static int GetCurveIdFromName(std::string_view name); + + using GetCurveCallback = std::function; + static bool GetCurves(GetCurveCallback callback); + inline const BignumPointer& getX() const { return x_; } inline const BignumPointer& getY() const { return y_; } inline const BignumPointer& getD() const { return d_; } @@ -568,9 +575,31 @@ class DataPointer final { static DataPointer Alloc(size_t len); static DataPointer Copy(const Buffer& buffer); + // Attempts to allocate the buffer space using the secure heap, if + // supported/enabled. If the secure heap is disabled, then this + // ends up being equivalent to Alloc(len). Note that allocation + // will fail if there is not enough free space remaining in the + // secure heap space. + static DataPointer SecureAlloc(size_t len); + + // If the secure heap is enabled, returns the amount of data that + // has been allocated from the heap. + static size_t GetSecureHeapUsed(); + + enum class InitSecureHeapResult { + FAILED, + UNABLE_TO_MEMORY_MAP, + OK, + }; + + // Attempt to initialize the secure heap. The secure heap is not + // supported on all operating systems and whenever boringssl is + // used. + static InitSecureHeapResult TryInitSecureHeap(size_t amount, size_t min); + DataPointer() = default; - explicit DataPointer(void* data, size_t len); - explicit DataPointer(const Buffer& buffer); + explicit DataPointer(void* data, size_t len, bool secure = false); + explicit DataPointer(const Buffer& buffer, bool secure = false); DataPointer(DataPointer&& other) noexcept; DataPointer& operator=(DataPointer&& other) noexcept; NCRYPTO_DISALLOW_COPY(DataPointer) @@ -606,9 +635,12 @@ class DataPointer final { }; } + bool isSecure() const { return secure_; } + private: void* data_ = nullptr; size_t len_ = 0; + bool secure_ = false; }; class BIOPointer final { @@ -1067,6 +1099,8 @@ class SSLPointer final { std::optional verifyPeerCertificate() const; + static std::optional getSecurityLevel(); + void getCiphers(std::function cb) const; static SSLPointer New(const SSLCtxPointer& ctx); diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 9e01b15..df23c26 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -118,20 +118,64 @@ DataPointer DataPointer::Alloc(size_t len) { #endif } +DataPointer DataPointer::SecureAlloc(size_t len) { +#ifndef OPENSSL_IS_BORINGSSL + auto ptr = OPENSSL_secure_zalloc(len); + if (ptr == nullptr) return {}; + return DataPointer(ptr, len, true); +#else + // BoringSSL does not implement the OPENSSL_secure_zalloc API. + auto ptr = OPENSSL_malloc(len); + if (ptr == nullptr) return {}; + memset(ptr, 0, len); + return DataPointer(ptr, len); +#endif +} + +size_t DataPointer::GetSecureHeapUsed() { +#ifndef OPENSSL_IS_BORINGSSL + return CRYPTO_secure_malloc_initialized() ? CRYPTO_secure_used() : 0; +#else + // BoringSSL does not have the secure heap and therefore + // will always return 0. + return 0; +#endif +} + +DataPointer::InitSecureHeapResult DataPointer::TryInitSecureHeap(size_t amount, + size_t min) { +#ifndef OPENSSL_IS_BORINGSSL + switch (CRYPTO_secure_malloc_init(amount, min)) { + case 0: + return InitSecureHeapResult::FAILED; + case 2: + return InitSecureHeapResult::UNABLE_TO_MEMORY_MAP; + case 1: + return InitSecureHeapResult::OK; + default: + return InitSecureHeapResult::FAILED; + } +#else + // BoringSSL does not actually support the secure heap + return InitSecureHeapResult::FAILED; +#endif +} + DataPointer DataPointer::Copy(const Buffer& buffer) { return DataPointer(OPENSSL_memdup(buffer.data, buffer.len), buffer.len); } -DataPointer::DataPointer(void* data, size_t length) - : data_(data), len_(length) {} +DataPointer::DataPointer(void* data, size_t length, bool secure) + : data_(data), len_(length), secure_(secure) {} -DataPointer::DataPointer(const Buffer& buffer) - : data_(buffer.data), len_(buffer.len) {} +DataPointer::DataPointer(const Buffer& buffer, bool secure) + : data_(buffer.data), len_(buffer.len), secure_(secure) {} DataPointer::DataPointer(DataPointer&& other) noexcept - : data_(other.data_), len_(other.len_) { + : data_(other.data_), len_(other.len_), secure_(other.secure_) { other.data_ = nullptr; other.len_ = 0; + other.secure_ = false; } DataPointer& DataPointer::operator=(DataPointer&& other) noexcept { @@ -151,7 +195,11 @@ void DataPointer::zero() { void DataPointer::reset(void* data, size_t length) { if (data_ != nullptr) { - OPENSSL_clear_free(data_, len_); + if (secure_) { + OPENSSL_secure_clear_free(data_, len_); + } else { + OPENSSL_clear_free(data_, len_); + } } data_ = data; len_ = length; @@ -182,6 +230,7 @@ DataPointer DataPointer::resize(size_t len) { // ============================================================================ bool isFipsEnabled() { + ClearErrorOnReturn clear_error_on_return; #if OPENSSL_VERSION_MAJOR >= 3 return EVP_default_properties_is_fips_enabled(nullptr) == 1; #else @@ -193,30 +242,31 @@ bool setFipsEnabled(bool enable, CryptoErrorList* errors) { if (isFipsEnabled() == enable) return true; ClearErrorOnReturn clearErrorOnReturn(errors); #if OPENSSL_VERSION_MAJOR >= 3 - return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; + return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1 && + EVP_default_properties_is_fips_enabled(nullptr); #else return FIPS_mode_set(enable ? 1 : 0) == 1; #endif } bool testFipsEnabled() { + ClearErrorOnReturn clear_error_on_return; #if OPENSSL_VERSION_MAJOR >= 3 OSSL_PROVIDER* fips_provider = nullptr; if (OSSL_PROVIDER_available(nullptr, "fips")) { fips_provider = OSSL_PROVIDER_load(nullptr, "fips"); } - const auto enabled = fips_provider == nullptr ? 0 - : OSSL_PROVIDER_self_test(fips_provider) ? 1 - : 0; + if (fips_provider == nullptr) return false; + int result = OSSL_PROVIDER_self_test(fips_provider); + OSSL_PROVIDER_unload(fips_provider); + return result; #else #ifdef OPENSSL_FIPS - const auto enabled = FIPS_selftest() ? 1 : 0; + return FIPS_selftest(); #else // OPENSSL_FIPS - const auto enabled = 0; + return false; #endif // OPENSSL_FIPS #endif - - return enabled; } // ============================================================================ @@ -2840,6 +2890,21 @@ std::optional SSLPointer::getCipherVersion() const { return SSL_CIPHER_get_version(cipher); } +std::optional SSLPointer::getSecurityLevel() { +#ifndef OPENSSL_IS_BORINGSSL + auto ctx = SSLCtxPointer::New(); + if (!ctx) return std::nullopt; + + auto ssl = SSLPointer::New(ctx); + if (!ssl) return std::nullopt; + + return SSL_get_security_level(ssl); +#else + // for BoringSSL assume the same as the default + return OPENSSL_TLS_SECURITY_LEVEL; +#endif // OPENSSL_IS_BORINGSSL +} + SSLCtxPointer::SSLCtxPointer(SSL_CTX* ctx) : ctx_(ctx) {} SSLCtxPointer::SSLCtxPointer(SSLCtxPointer&& other) noexcept @@ -3515,6 +3580,10 @@ EVPKeyCtxPointer EVPKeyCtxPointer::New(const EVPKeyPointer& key) { } EVPKeyCtxPointer EVPKeyCtxPointer::NewFromID(int id) { +#ifdef OPENSSL_IS_BORINGSSL + // DSA keys are not supported with BoringSSL + if (id == EVP_PKEY_DSA) return {}; +#endif return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(id, nullptr)); } @@ -4026,6 +4095,26 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } +int Ec::GetCurveIdFromName(std::string_view name) { + int nid = EC_curve_nist2nid(name.data()); + if (nid == NID_undef) { + nid = OBJ_sn2nid(name.data()); + } + return nid; +} + +bool Ec::GetCurves(Ec::GetCurveCallback callback) { + const size_t count = EC_get_builtin_curves(nullptr, 0); + std::vector curves(count); + if (EC_get_builtin_curves(curves.data(), count) != count) { + return false; + } + for (auto curve : curves) { + if (!callback(OBJ_nid2sn(curve.nid))) return false; + } + return true; +} + uint32_t Ec::getDegree() const { return EC_GROUP_get_degree(getGroup()); } From 849ada975064fbab606afe35a1f61bfc57a5f185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 9 Apr 2025 09:36:49 +0200 Subject: [PATCH 03/21] crypto: make auth tag size assumption explicit The `CipherBase` class assumes that any authentication tag will fit into `EVP_GCM_TLS_TAG_LEN` bytes, which is true because Node.js only supports GCM with AES as the blocker cipher, and the block size of AES happens to be 16 bytes, which coincidentally is also the output size of the Poly1305 construction used by ChaCha20-Poly1305 as well as the maximum size of authentication tags produced by AES in CCM or OCB mode. This commit adds a new constant `ncrypto::Cipher::MAX_AUTH_TAG_LENGTH` which is the maximum length of authentication tags produced by algorithms that Node.js supports and replaces some constants in `CipherBase` with semantically more meaningful named constants. The OpenSSL team is debating whether a constant like `MAX_AUTH_TAG_LENGTH` (`EVP_MAX_AEAD_TAG_LENGTH`) should exist at all since its value necessarily depends on the set of AEAD algorithms supported, but I do believe that, for Node.js, this is a step in the right direction. It certainly makes more sense than to use the AES-GCM tag size as defined by TLS. PR-URL: https://github.com/nodejs/node/pull/57803 Reviewed-By: Anna Henningsen Reviewed-By: James M Snell --- include/ncrypto.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index e2e8965..71803e9 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -293,6 +293,14 @@ class Cipher final { public: static constexpr size_t MAX_KEY_LENGTH = EVP_MAX_KEY_LENGTH; static constexpr size_t MAX_IV_LENGTH = EVP_MAX_IV_LENGTH; +#ifdef EVP_MAX_AEAD_TAG_LENGTH + static constexpr size_t MAX_AUTH_TAG_LENGTH = EVP_MAX_AEAD_TAG_LENGTH; +#else + static constexpr size_t MAX_AUTH_TAG_LENGTH = 16; +#endif + static_assert(EVP_GCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && + EVP_CCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && + EVP_CHACHAPOLY_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH); Cipher() = default; Cipher(const EVP_CIPHER* cipher) : cipher_(cipher) {} From 937fef68af1188aa875740f7c8be3bab88a18393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Tue, 15 Apr 2025 14:09:22 +0200 Subject: [PATCH 04/21] crypto: revert dangerous uses of std::string_view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An `std::string_view v` is a `const char* v.data()` along with an `std::size_t v.size()` that guarantees that `v.size()` contiguous elements of type `char` can be accessed relative to the pointer `v.data()`. One of the main reasons behind the existence of `std::string_view` is the ability to operate on `char` sequences without requiring null termination, which otherwise often requires expensive copies of strings to be made. As a consequence, it is generally incorrect to assume that `v.data()` points to a null-terminated sequence of `char`, and the only way to obtain a null-terminated string from an `std::string_view` is to make a copy. It is not even possible to check if the sequence pointed to by `v.data()` is null-terminated because the null character would be at position `v.data() + v.size()`, which is outside of the range that `v` guarantees safe access to. (A default-constructed `std::string_view` even sets its own data pointer to a `nullptr`, which is fine because it only needs to guarantee safe access to zero elements, i.e., to no elements). In `deps/ncrypto` and `src/crypto`, there are various APIs that consume `std::string_view v` arguments but then ignore `v.size()` and treat `v.data()` as a C-style string of type `const char*`. However, that is not what call sites would expect from functions that explicitly ask for `std::string_view` arguments, since it makes assumptions beyond the guarantees provided by `std::string_view` and leads to undefined behavior unless the given view either contains an embedded null character or the `char` at address `v.data() + v.size()` is a null character. This is not a reasonable assumption for `std::string_view` in general, and it also defeats the purpose of `std::string_view` for the most part since, when `v.size()` is being ignored, it is essentially just a `const char*`. Constructing an `std::string_view` from a `const char*` is also not "free" but requires computing the length of the C-style string (unless the length can be computed at compile time, e.g., because the value is just a string literal). Repeated conversion between `const char*` as used by OpenSSL and `std::string_view` as used by ncrypto thus incurs the additional overhead of computing the length of the string whenever an `std::string_view` is constructed from a `const char*`. (This seems negligible compared to the safety argument though.) Similarly, returning a `const char*` pointer to a C-style string as an `std::string_view` has two downsides: the function must compute the length of the string in order to construct the view, and the caller can no longer assume that the return value is null-terminated and thus cannot pass the returned view to functions that require their arguments to be null terminated. (And, for the reasons explained above, the caller also cannot check if the value is null-terminated without potentially invoking undefined behavior.) C++20 unfortunately does not have a type similar to Rust's `CStr` or GSL `czstring`. Therefore, this commit changes many occurrences of `std::string_view` back to `const char*`, which is conventional for null-terminated C-style strings and does not require computing the length of strings. There are _a lot_ of occurrences of `std::string_view` in ncrypto and for each one, we need to evaluate if it is safe and a good abstraction. I tried to do so, but I might have changed too few or too many, so please feel free to give feedback on individual occurrences. Merge conflicts were resolved by Claude Code. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- include/ncrypto.h | 54 +++++++++++++++++++++++------------------------ src/engine.cpp | 10 ++++----- src/ncrypto.cpp | 46 +++++++++++++++++++--------------------- 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index 71803e9..90e336d 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -280,7 +280,7 @@ class Digest final { static const Digest SHA384; static const Digest SHA512; - static const Digest FromName(std::string_view name); + static const Digest FromName(const char* name); private: const EVP_MD* md_ = nullptr; @@ -298,9 +298,10 @@ class Cipher final { #else static constexpr size_t MAX_AUTH_TAG_LENGTH = 16; #endif - static_assert(EVP_GCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && - EVP_CCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && - EVP_CHACHAPOLY_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH); + // FIXME: These constants are not available in all OpenSSL/BoringSSL versions + // static_assert(EVP_GCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && + // EVP_CCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && + // EVP_CHACHAPOLY_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH); Cipher() = default; Cipher(const EVP_CIPHER* cipher) : cipher_(cipher) {} @@ -322,7 +323,7 @@ class Cipher final { int getKeyLength() const; int getBlockSize() const; std::string_view getModeLabel() const; - std::string_view getName() const; + const char* getName() const; bool isGcmMode() const; bool isWrapMode() const; @@ -339,11 +340,11 @@ class Cipher final { unsigned char* key, unsigned char* iv) const; - static const Cipher FromName(std::string_view name); + static const Cipher FromName(const char* name); static const Cipher FromNid(int nid); static const Cipher FromCtx(const CipherCtxPointer& ctx); - using CipherNameCallback = std::function; + using CipherNameCallback = std::function; // Iterates the known ciphers if the underlying implementation // is able to do so. @@ -559,9 +560,9 @@ class Ec final { inline operator bool() const { return ec_ != nullptr; } inline operator OSSL3_CONST EC_KEY*() const { return ec_; } - static int GetCurveIdFromName(std::string_view name); + static int GetCurveIdFromName(const char* name); - using GetCurveCallback = std::function; + using GetCurveCallback = std::function; static bool GetCurves(GetCurveCallback callback); inline const BignumPointer& getX() const { return x_; } @@ -658,7 +659,7 @@ class BIOPointer final { static BIOPointer New(const BIO_METHOD* method); static BIOPointer New(const void* data, size_t len); static BIOPointer New(const BIGNUM* bn); - static BIOPointer NewFile(std::string_view filename, std::string_view mode); + static BIOPointer NewFile(const char* filename, const char* mode); static BIOPointer NewFp(FILE* fd, int flags); template @@ -962,9 +963,8 @@ class DHPointer final { static BignumPointer GetStandardGenerator(); static BignumPointer FindGroup( - const std::string_view name, - FindGroupOption option = FindGroupOption::NONE); - static DHPointer FromGroup(const std::string_view name, + std::string_view name, FindGroupOption option = FindGroupOption::NONE); + static DHPointer FromGroup(std::string_view name, FindGroupOption option = FindGroupOption::NONE); static DHPointer New(BignumPointer&& p, BignumPointer&& g); @@ -1063,7 +1063,7 @@ class SSLCtxPointer final { SSL_CTX_set_tlsext_status_arg(get(), nullptr); } - bool setCipherSuites(std::string_view ciphers); + bool setCipherSuites(const char* ciphers); static SSLCtxPointer NewServer(); static SSLCtxPointer NewClient(); @@ -1092,8 +1092,8 @@ class SSLPointer final { bool setSession(const SSLSessionPointer& session); bool setSniContext(const SSLCtxPointer& ctx) const; - const std::string_view getClientHelloAlpn() const; - const std::string_view getClientHelloServerName() const; + const char* getClientHelloAlpn() const; + const char* getClientHelloServerName() const; std::optional getServerName() const; X509View getCertificate() const; @@ -1109,7 +1109,7 @@ class SSLPointer final { static std::optional getSecurityLevel(); - void getCiphers(std::function cb) const; + void getCiphers(std::function cb) const; static SSLPointer New(const SSLCtxPointer& ctx); static std::optional GetServerName(const SSL* ssl); @@ -1205,13 +1205,13 @@ class X509View final { INVALID_NAME, OPERATION_FAILED, }; - CheckMatch checkHost(const std::string_view host, + CheckMatch checkHost(std::string_view host, int flags, DataPointer* peerName = nullptr) const; - CheckMatch checkEmail(const std::string_view email, int flags) const; - CheckMatch checkIp(const std::string_view ip, int flags) const; + CheckMatch checkEmail(std::string_view email, int flags) const; + CheckMatch checkIp(std::string_view ip, int flags) const; - using UsageCallback = std::function; + using UsageCallback = std::function; bool enumUsages(UsageCallback callback) const; template @@ -1248,8 +1248,8 @@ class X509Pointer final { X509View view() const; operator X509View() const { return view(); } - static std::string_view ErrorCode(int32_t err); - static std::optional ErrorReason(int32_t err); + static const char* ErrorCode(int32_t err); + static std::optional ErrorReason(int32_t err); private: DeleteFnPtr cert_; @@ -1465,7 +1465,7 @@ class EnginePointer final { bool setAsDefault(uint32_t flags, CryptoErrorList* errors = nullptr); bool init(bool finish_on_exit = false); - EVPKeyPointer loadPrivateKey(const std::string_view key_name); + EVPKeyPointer loadPrivateKey(const char* key_name); // Release ownership of the ENGINE* pointer. ENGINE* release(); @@ -1473,7 +1473,7 @@ class EnginePointer final { // Retrieve an OpenSSL Engine instance by name. If the name does not // identify a valid named engine, the returned EnginePointer will be // empty. - static EnginePointer getEngineByName(const std::string_view name, + static EnginePointer getEngineByName(const char* name, CryptoErrorList* errors = nullptr); // Call once when initializing OpenSSL at startup for the process. @@ -1530,8 +1530,8 @@ DataPointer ExportChallenge(const Buffer& buf); // ============================================================================ // KDF -const EVP_MD* getDigestByName(const std::string_view name); -const EVP_CIPHER* getCipherByName(const std::string_view name); +const EVP_MD* getDigestByName(const char* name); +const EVP_CIPHER* getCipherByName(const char* name); // Verify that the specified HKDF output length is valid for the given digest. // The maximum length for HKDF output for a given digest is 255 times the diff --git a/src/engine.cpp b/src/engine.cpp index 1eae89d..1845cfc 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -44,15 +44,15 @@ ENGINE* EnginePointer::release() { return ret; } -EnginePointer EnginePointer::getEngineByName(const std::string_view name, +EnginePointer EnginePointer::getEngineByName(const char* name, CryptoErrorList* errors) { MarkPopErrorOnReturn mark_pop_error_on_return(errors); - EnginePointer engine(ENGINE_by_id(name.data())); + EnginePointer engine(ENGINE_by_id(name)); if (!engine) { // Engine not found, try loading dynamically. engine = EnginePointer(ENGINE_by_id("dynamic")); if (engine) { - if (!ENGINE_ctrl_cmd_string(engine.get(), "SO_PATH", name.data(), 0) || + if (!ENGINE_ctrl_cmd_string(engine.get(), "SO_PATH", name, 0) || !ENGINE_ctrl_cmd_string(engine.get(), "LOAD", nullptr, 0)) { engine.reset(); } @@ -73,10 +73,10 @@ bool EnginePointer::init(bool finish_on_exit) { return ENGINE_init(engine) == 1; } -EVPKeyPointer EnginePointer::loadPrivateKey(const std::string_view key_name) { +EVPKeyPointer EnginePointer::loadPrivateKey(const char* key_name) { if (engine == nullptr) return EVPKeyPointer(); return EVPKeyPointer( - ENGINE_load_private_key(engine, key_name.data(), nullptr, nullptr)); + ENGINE_load_private_key(engine, key_name, nullptr, nullptr)); } void EnginePointer::initEnginesOnce() { diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index df23c26..6cb99a0 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -1410,7 +1410,7 @@ X509Pointer X509Pointer::PeerFrom(const SSLPointer& ssl) { // When adding or removing errors below, please also update the list in the API // documentation. See the "OpenSSL Error Codes" section of doc/api/errors.md // Also *please* update the respective section in doc/api/tls.md as well -std::string_view X509Pointer::ErrorCode(int32_t err) { // NOLINT(runtime/int) +const char* X509Pointer::ErrorCode(int32_t err) { // NOLINT(runtime/int) #define CASE(CODE) \ case X509_V_ERR_##CODE: \ return #CODE; @@ -1448,7 +1448,7 @@ std::string_view X509Pointer::ErrorCode(int32_t err) { // NOLINT(runtime/int) return "UNSPECIFIED"; } -std::optional X509Pointer::ErrorReason(int32_t err) { +std::optional X509Pointer::ErrorReason(int32_t err) { if (err == X509_V_OK) return std::nullopt; return X509_verify_cert_error_string(err); } @@ -1504,9 +1504,8 @@ BIOPointer BIOPointer::New(const void* data, size_t len) { return BIOPointer(BIO_new_mem_buf(data, len)); } -BIOPointer BIOPointer::NewFile(std::string_view filename, - std::string_view mode) { - return BIOPointer(BIO_new_file(filename.data(), mode.data())); +BIOPointer BIOPointer::NewFile(const char* filename, const char* mode) { + return BIOPointer(BIO_new_file(filename, mode)); } BIOPointer BIOPointer::NewFp(FILE* fd, int close_flag) { @@ -1788,17 +1787,17 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey, // ============================================================================ // KDF -const EVP_MD* getDigestByName(const std::string_view name) { +const EVP_MD* getDigestByName(const char* name) { // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 // exposed through the public API. - if (name == "dss1" || name == "DSS1") [[unlikely]] { + if (strcmp(name, "dss1") == 0 || strcmp(name, "DSS1") == 0) [[unlikely]] { return EVP_sha1(); } - return EVP_get_digestbyname(name.data()); + return EVP_get_digestbyname(name); } -const EVP_CIPHER* getCipherByName(const std::string_view name) { - return EVP_get_cipherbyname(name.data()); +const EVP_CIPHER* getCipherByName(const char* name) { + return EVP_get_cipherbyname(name); } bool checkHkdfLength(const Digest& md, size_t length) { @@ -2714,8 +2713,7 @@ SSLPointer SSLPointer::New(const SSLCtxPointer& ctx) { return SSLPointer(SSL_new(ctx.get())); } -void SSLPointer::getCiphers( - std::function cb) const { +void SSLPointer::getCiphers(std::function cb) const { if (!ssl_) return; STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(get()); @@ -2780,7 +2778,7 @@ std::optional SSLPointer::verifyPeerCertificate() const { return std::nullopt; } -const std::string_view SSLPointer::getClientHelloAlpn() const { +const char* SSLPointer::getClientHelloAlpn() const { if (ssl_ == nullptr) return {}; #ifndef OPENSSL_IS_BORINGSSL const unsigned char* buf; @@ -2805,7 +2803,7 @@ const std::string_view SSLPointer::getClientHelloAlpn() const { #endif } -const std::string_view SSLPointer::getClientHelloServerName() const { +const char* SSLPointer::getClientHelloServerName() const { if (ssl_ == nullptr) return {}; #ifndef OPENSSL_IS_BORINGSSL const unsigned char* buf; @@ -2954,10 +2952,10 @@ bool SSLCtxPointer::setGroups(const char* groups) { #endif } -bool SSLCtxPointer::setCipherSuites(std::string_view ciphers) { +bool SSLCtxPointer::setCipherSuites(const char* ciphers) { #ifndef OPENSSL_IS_BORINGSSL if (!ctx_) return false; - return SSL_CTX_set_ciphersuites(ctx_.get(), ciphers.data()); + return SSL_CTX_set_ciphersuites(ctx_.get(), ciphers); #else // BoringSSL does not allow API config of TLS 1.3 cipher suites. // We treat this as a non-op. @@ -2967,8 +2965,8 @@ bool SSLCtxPointer::setCipherSuites(std::string_view ciphers) { // ============================================================================ -const Cipher Cipher::FromName(std::string_view name) { - return Cipher(EVP_get_cipherbyname(name.data())); +const Cipher Cipher::FromName(const char* name) { + return Cipher(EVP_get_cipherbyname(name)); } const Cipher Cipher::FromNid(int nid) { @@ -3082,7 +3080,7 @@ std::string_view Cipher::getModeLabel() const { return "{unknown}"; } -std::string_view Cipher::getName() const { +const char* Cipher::getName() const { if (!cipher_) return {}; // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of // EVP_CIPHER_name(cipher) for compatibility with BoringSSL. @@ -3115,7 +3113,7 @@ int Cipher::bytesToKey(const Digest& digest, namespace { struct CipherCallbackContext { Cipher::CipherNameCallback cb; - void operator()(std::string_view name) { cb(name); } + void operator()(const char* name) { cb(name); } }; #if OPENSSL_VERSION_MAJOR >= 3 @@ -4095,10 +4093,10 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } -int Ec::GetCurveIdFromName(std::string_view name) { - int nid = EC_curve_nist2nid(name.data()); +int Ec::GetCurveIdFromName(const char* name) { + int nid = EC_curve_nist2nid(name); if (nid == NID_undef) { - nid = OBJ_sn2nid(name.data()); + nid = OBJ_sn2nid(name); } return nid; } @@ -4487,7 +4485,7 @@ const Digest Digest::SHA256 = Digest(EVP_sha256()); const Digest Digest::SHA384 = Digest(EVP_sha384()); const Digest Digest::SHA512 = Digest(EVP_sha512()); -const Digest Digest::FromName(std::string_view name) { +const Digest Digest::FromName(const char* name) { return ncrypto::getDigestByName(name); } From 8c83332b6c78bd1163d56e8e7a615af17282129b Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 12 May 2025 14:11:50 +0200 Subject: [PATCH 05/21] crypto: handle missing OPENSSL_TLS_SECURITY_LEVEL PR-URL: https://github.com/nodejs/node/pull/58103 Reviewed-By: Michael Dawson Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Rafael Gonzaga --- include/ncrypto.h | 2 -- src/ncrypto.cpp | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index 90e336d..5d0e7ef 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -52,8 +52,6 @@ using OPENSSL_SIZE_T = int; #ifdef NCRYPTO_BSSL_NEEDS_DH_PRIMES #include "dh-primes.h" #endif // NCRYPTO_BSSL_NEEDS_DH_PRIMES -// FIXME: This should be removed when patch 0037 is applied -#define OPENSSL_TLS_SECURITY_LEVEL 1 #endif // OPENSSL_IS_BORINGSSL namespace ncrypto { diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 6cb99a0..011fa94 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -2898,8 +2898,9 @@ std::optional SSLPointer::getSecurityLevel() { return SSL_get_security_level(ssl); #else - // for BoringSSL assume the same as the default - return OPENSSL_TLS_SECURITY_LEVEL; + // OPENSSL_TLS_SECURITY_LEVEL is not defined in BoringSSL + // so assume it is the default OPENSSL_TLS_SECURITY_LEVEL value. + return 1; #endif // OPENSSL_IS_BORINGSSL } From aa476b18986fd4e3240bde27afd79bc8e12c2807 Mon Sep 17 00:00:00 2001 From: Aditi <62544124+Aditi-1400@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:09:00 +0530 Subject: [PATCH 06/21] crypto: support outputLength option in crypto.hash for XOF functions Support `outputLength` option in crypto.hash() for XOF hash functions to align with the behaviour of crypto.createHash() API closes: https://github.com/nodejs/node/issues/57312 Co-authored-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/58121 Fixes: https://github.com/nodejs/node/issues/57312 Reviewed-By: Joyee Cheung Reviewed-By: Filip Skokan Reviewed-By: Rafael Gonzaga --- include/ncrypto.h | 5 +++++ src/ncrypto.cpp | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index 5d0e7ef..26a8c4c 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -284,8 +284,13 @@ class Digest final { const EVP_MD* md_ = nullptr; }; +// Computes a fixed-length digest. DataPointer hashDigest(const Buffer& data, const EVP_MD* md); +// Computes a variable-length digest for XOF algorithms (e.g. SHAKE128). +DataPointer xofHashDigest(const Buffer& data, + const EVP_MD* md, + size_t length); class Cipher final { public: diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 011fa94..baf60b1 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -4384,6 +4384,22 @@ DataPointer hashDigest(const Buffer& buf, return data.resize(result_size); } +DataPointer xofHashDigest(const Buffer& buf, + const EVP_MD* md, + size_t output_length) { + if (md == nullptr) return {}; + + EVPMDCtxPointer ctx = EVPMDCtxPointer::New(); + if (!ctx) return {}; + if (ctx.digestInit(md) != 1) { + return {}; + } + if (ctx.digestUpdate(reinterpret_cast&>(buf)) != 1) { + return {}; + } + return ctx.digestFinal(output_length); +} + // ============================================================================ X509Name::X509Name() : name_(nullptr), total_(0) {} From 748a1ba1845f8d994e58f0ad4a4d71f504ecdcf5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 6 Aug 2025 22:49:30 +0100 Subject: [PATCH 07/21] crypto: support ML-DSA KeyObject, sign, and verify PR-URL: https://github.com/nodejs/node/pull/59259 Reviewed-By: Yagiz Nizipli Reviewed-By: Ethan Arrowood Reviewed-By: James M Snell --- include/ncrypto.h | 11 +++++++ src/ncrypto.cpp | 74 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index 26a8c4c..01fbd6b 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -30,6 +30,9 @@ #if OPENSSL_VERSION_MAJOR >= 3 #define OSSL3_CONST const +#if OPENSSL_VERSION_MINOR >= 5 +#include +#endif #else #define OSSL3_CONST #endif @@ -834,6 +837,10 @@ class EVPKeyPointer final { const Buffer& data); static EVPKeyPointer NewRawPrivate(int id, const Buffer& data); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + static EVPKeyPointer NewRawSeed(int id, + const Buffer& data); +#endif static EVPKeyPointer NewDH(DHPointer&& dh); static EVPKeyPointer NewRSA(RSAPointer&& rsa); @@ -927,6 +934,10 @@ class EVPKeyPointer final { DataPointer rawPrivateKey() const; BIOPointer derPublicKey() const; +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + DataPointer rawSeed() const; +#endif + Result writePrivateKey( const PrivateKeyEncodingConfig& config) const; Result writePublicKey( diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index baf60b1..e6b92e6 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -2035,6 +2035,31 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); } +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +EVPKeyPointer EVPKeyPointer::NewRawSeed( + int id, const Buffer& data) { + if (id == 0) return {}; + + OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED, + const_cast(data.data), + data.len), + OSSL_PARAM_END}; + + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); + if (ctx == nullptr) return {}; + + EVP_PKEY* pkey = nullptr; + if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 || + EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { + EVP_PKEY_CTX_free(ctx); + return {}; + } + + return EVPKeyPointer(pkey); +} +#endif + EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) { #ifndef NCRYPTO_NO_EVP_DH if (!dh) return {}; @@ -2092,7 +2117,16 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; - return EVP_PKEY_id(key); + int type = EVP_PKEY_id(key); +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + // https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870 + if (type == -1) { + if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44; + if (EVP_PKEY_is_a(key, "ML-DSA-65")) return EVP_PKEY_ML_DSA_65; + if (EVP_PKEY_is_a(key, "ML-DSA-87")) return EVP_PKEY_ML_DSA_87; + } +#endif + return type; } int EVPKeyPointer::base_id(const EVP_PKEY* key) { @@ -2148,6 +2182,31 @@ DataPointer EVPKeyPointer::rawPublicKey() const { return {}; } +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +DataPointer EVPKeyPointer::rawSeed() const { + if (!pkey_) return {}; + switch (id()) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + break; + default: + unreachable(); + } + + size_t seed_len = 32; + if (auto data = DataPointer::Alloc(seed_len)) { + const Buffer buf = data; + size_t len = data.size(); + if (EVP_PKEY_get_octet_string_param( + get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1) + return {}; + return data; + } + return {}; +} +#endif + DataPointer EVPKeyPointer::rawPrivateKey() const { if (!pkey_) return {}; if (auto data = DataPointer::Alloc(rawPrivateKeySize())) { @@ -2598,7 +2657,18 @@ bool EVPKeyPointer::isRsaVariant() const { bool EVPKeyPointer::isOneShotVariant() const { if (!pkey_) return false; int type = id(); - return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448; + switch (type) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: +#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: +#endif + return true; + default: + return false; + } } bool EVPKeyPointer::isSigVariant() const { From f16afd2fd619d757a2dd5580774902a63e7ec42d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 12 Aug 2025 23:15:25 +0200 Subject: [PATCH 08/21] src: update OpenSSL pqc checks PR-URL: https://github.com/nodejs/node/pull/59436 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli Reviewed-By: Luigi Pinca Reviewed-By: Richard Lau --- include/ncrypto.h | 13 ++++++++----- src/ncrypto.cpp | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index 01fbd6b..cf99de8 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -28,11 +28,14 @@ #include #endif // OPENSSL_FIPS -#if OPENSSL_VERSION_MAJOR >= 3 -#define OSSL3_CONST const -#if OPENSSL_VERSION_MINOR >= 5 +// Define OPENSSL_WITH_PQC for post-quantum cryptography support +#if OPENSSL_VERSION_NUMBER >= 0x30500000L +#define OPENSSL_WITH_PQC 1 #include #endif + +#if OPENSSL_VERSION_MAJOR >= 3 +#define OSSL3_CONST const #else #define OSSL3_CONST #endif @@ -837,7 +840,7 @@ class EVPKeyPointer final { const Buffer& data); static EVPKeyPointer NewRawPrivate(int id, const Buffer& data); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC static EVPKeyPointer NewRawSeed(int id, const Buffer& data); #endif @@ -934,7 +937,7 @@ class EVPKeyPointer final { DataPointer rawPrivateKey() const; BIOPointer derPublicKey() const; -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC DataPointer rawSeed() const; #endif diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index e6b92e6..64e93e6 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -2035,7 +2035,7 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( EVP_PKEY_new_raw_private_key(id, nullptr, data.data, data.len)); } -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC EVPKeyPointer EVPKeyPointer::NewRawSeed( int id, const Buffer& data) { if (id == 0) return {}; @@ -2118,7 +2118,7 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC // https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870 if (type == -1) { if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44; @@ -2182,7 +2182,7 @@ DataPointer EVPKeyPointer::rawPublicKey() const { return {}; } -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; switch (id()) { @@ -2660,7 +2660,7 @@ bool EVPKeyPointer::isOneShotVariant() const { switch (type) { case EVP_PKEY_ED25519: case EVP_PKEY_ED448: -#if OPENSSL_VERSION_MAJOR >= 3 && OPENSSL_VERSION_MINOR >= 5 +#if OPENSSL_WITH_PQC case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: From 619faad9ecf5319aa8790e37cc01e9ff9fd7007a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 16 Aug 2025 16:39:14 +0200 Subject: [PATCH 09/21] crypto: support ML-KEM KeyObject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59461 Reviewed-By: Tobias Nießen Reviewed-By: Ethan Arrowood --- include/ncrypto.h | 3 +++ src/ncrypto.cpp | 32 +++++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/include/ncrypto.h b/include/ncrypto.h index cf99de8..83ffa26 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -31,6 +31,9 @@ // Define OPENSSL_WITH_PQC for post-quantum cryptography support #if OPENSSL_VERSION_NUMBER >= 0x30500000L #define OPENSSL_WITH_PQC 1 +#define EVP_PKEY_ML_KEM_512 NID_ML_KEM_512 +#define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 +#define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #include #endif diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 64e93e6..b8fd8fa 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -19,6 +19,22 @@ #include #if OPENSSL_VERSION_MAJOR >= 3 #include +#endif +#if OPENSSL_WITH_PQC +struct PQCMapping { + const char* name; + int nid; +}; + +constexpr static PQCMapping pqc_mappings[] = { + {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, + {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, + {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, + {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, + {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, + {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, +}; + #endif // EVP_PKEY_CTX_set_dsa_paramgen_q_bits was added in OpenSSL 1.1.1e. @@ -2119,11 +2135,21 @@ int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); #if OPENSSL_WITH_PQC + // EVP_PKEY_id returns -1 when EVP_PKEY_* is only implemented in a provider + // which is the case for all post-quantum NIST algorithms + // one suggested way would be to use a chain of `EVP_PKEY_is_a` // https://github.com/openssl/openssl/issues/27738#issuecomment-3013215870 + // or, this way there are less calls to the OpenSSL provider, just + // getting the name once if (type == -1) { - if (EVP_PKEY_is_a(key, "ML-DSA-44")) return EVP_PKEY_ML_DSA_44; - if (EVP_PKEY_is_a(key, "ML-DSA-65")) return EVP_PKEY_ML_DSA_65; - if (EVP_PKEY_is_a(key, "ML-DSA-87")) return EVP_PKEY_ML_DSA_87; + const char* type_name = EVP_PKEY_get0_type_name(key); + if (type_name == nullptr) return -1; + + for (const auto& mapping : pqc_mappings) { + if (strcmp(type_name, mapping.name) == 0) { + return mapping.nid; + } + } } #endif return type; From 1fbf14c5305fb4140fb54fd560e66908ee7cbf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Sat, 16 Aug 2025 16:47:58 +0200 Subject: [PATCH 10/21] crypto: fix EVPKeyCtxPointer::publicCheck() Commit 206ebeb44764d58c6a505657edab3a7a78a0b977 added an additional call to EVP_PKEY_public_check and an unconditional return from publicCheck(). This prevents the control flow from reaching the original call to either EVP_PKEY_public_check or EVP_PKEY_public_check_quick. This change restores the previous behavior, which calls EVP_PKEY_public_check_quick instead, if possible. Refs: https://github.com/nodejs/node/pull/56812 PR-URL: https://github.com/nodejs/node/pull/59471 Reviewed-By: Anna Henningsen Reviewed-By: Filip Skokan Reviewed-By: Luigi Pinca --- src/ncrypto.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index b8fd8fa..45e2a89 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -3876,7 +3876,6 @@ EVPKeyPointer EVPKeyCtxPointer::paramgen() const { bool EVPKeyCtxPointer::publicCheck() const { if (!ctx_) return false; #ifndef OPENSSL_IS_BORINGSSL - return EVP_PKEY_public_check(ctx_.get()) == 1; #if OPENSSL_VERSION_MAJOR >= 3 return EVP_PKEY_public_check_quick(ctx_.get()) == 1; #else From 2d6f90d2e62c53fd7fcbd4d6de5ee89f599b13a4 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 8 Aug 2025 13:02:42 +0200 Subject: [PATCH 11/21] crypto: add ChaCha20-Poly1305 Web Cryptography algorithm PR-URL: https://github.com/nodejs/node/pull/59365 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Yagiz Nizipli Reviewed-By: Joyee Cheung --- include/ncrypto.h | 1 + src/ncrypto.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index 83ffa26..a212fa7 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -380,6 +380,7 @@ class Cipher final { static const Cipher AES_128_KW; static const Cipher AES_192_KW; static const Cipher AES_256_KW; + static const Cipher CHACHA20_POLY1305; struct CipherParams { int padding; diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 45e2a89..99b417a 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -3087,6 +3087,7 @@ 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); +const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); bool Cipher::isGcmMode() const { if (!cipher_) return false; From ad907a8e6644243fab8cc35dd8b1879a243e6c75 Mon Sep 17 00:00:00 2001 From: Ranieri Althoff <1993083+ranisalt@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:30:38 +0200 Subject: [PATCH 12/21] crypto: add argon2() and argon2Sync() methods Co-authored-by: Filip Skokan Co-authored-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/50353 Reviewed-By: Ethan Arrowood Reviewed-By: Filip Skokan Reviewed-By: Yagiz Nizipli --- include/ncrypto.h | 17 ++++++++ src/ncrypto.cpp | 101 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index a212fa7..6f4ba29 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -1608,6 +1608,23 @@ DataPointer pbkdf2(const Digest& md, uint32_t iterations, size_t length); +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#ifndef OPENSSL_NO_ARGON2 +enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID }; + +DataPointer argon2(const Buffer& pass, + const Buffer& salt, + uint32_t lanes, + size_t length, + uint32_t memcost, + uint32_t iter, + uint32_t version, + const Buffer& secret, + const Buffer& ad, + Argon2Type type); +#endif +#endif + // ============================================================================ // Version metadata #define NCRYPTO_VERSION "0.0.1" diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 99b417a..586e256 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -18,7 +18,12 @@ #include #include #if OPENSSL_VERSION_MAJOR >= 3 +#include +#include #include +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#include +#endif #endif #if OPENSSL_WITH_PQC struct PQCMapping { @@ -2006,6 +2011,102 @@ DataPointer pbkdf2(const Digest& md, return {}; } +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#ifndef OPENSSL_NO_ARGON2 +DataPointer argon2(const Buffer& pass, + const Buffer& salt, + uint32_t lanes, + size_t length, + uint32_t memcost, + uint32_t iter, + uint32_t version, + const Buffer& secret, + const Buffer& ad, + Argon2Type type) { + ClearErrorOnReturn clearErrorOnReturn; + + std::string_view algorithm; + switch (type) { + case Argon2Type::ARGON2I: + algorithm = "ARGON2I"; + break; + case Argon2Type::ARGON2D: + algorithm = "ARGON2D"; + break; + case Argon2Type::ARGON2ID: + algorithm = "ARGON2ID"; + break; + default: + // Invalid Argon2 type + return {}; + } + + // creates a new library context to avoid locking when running concurrently + auto ctx = DeleteFnPtr{OSSL_LIB_CTX_new()}; + if (!ctx) { + return {}; + } + + // required if threads > 1 + if (lanes > 1 && OSSL_set_max_threads(ctx.get(), lanes) != 1) { + return {}; + } + + auto kdf = DeleteFnPtr{ + EVP_KDF_fetch(ctx.get(), algorithm.data(), nullptr)}; + if (!kdf) { + return {}; + } + + auto kctx = + DeleteFnPtr{EVP_KDF_CTX_new(kdf.get())}; + if (!kctx) { + return {}; + } + + std::vector params; + params.reserve(9); + + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_PASSWORD, + const_cast(pass.len > 0 ? pass.data : ""), + pass.len)); + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, const_cast(salt.data), salt.len)); + params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &lanes)); + params.push_back( + OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes)); + params.push_back( + OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memcost)); + params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iter)); + + if (ad.len != 0) { + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_ARGON2_AD, const_cast(ad.data), ad.len)); + } + + if (secret.len != 0) { + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SECRET, + const_cast(secret.data), + secret.len)); + } + + params.push_back(OSSL_PARAM_construct_end()); + + auto dp = DataPointer::Alloc(length); + if (dp && EVP_KDF_derive(kctx.get(), + reinterpret_cast(dp.get()), + length, + params.data()) == 1) { + return dp; + } + + return {}; +} +#endif +#endif + // ============================================================================ EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig( From d1f48ca763a0abde44c4a3faeaa746c78b3f7f62 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 20 Aug 2025 16:30:58 +0200 Subject: [PATCH 13/21] crypto: support ML-KEM, DHKEM, and RSASVE key encapsulation mechanisms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59491 Reviewed-By: Yagiz Nizipli Reviewed-By: Tobias Nießen Reviewed-By: Rafael Gonzaga --- include/ncrypto.h | 34 +++++++++++++ src/ncrypto.cpp | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index 6f4ba29..796167c 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -1625,6 +1625,40 @@ DataPointer argon2(const Buffer& pass, #endif #endif +// ============================================================================ +// KEM (Key Encapsulation Mechanism) +#if OPENSSL_VERSION_MAJOR >= 3 + +class KEM final { + public: + struct EncapsulateResult { + DataPointer ciphertext; + DataPointer shared_key; + + EncapsulateResult() = default; + EncapsulateResult(DataPointer ct, DataPointer sk) + : ciphertext(std::move(ct)), shared_key(std::move(sk)) {} + }; + + // Encapsulate a shared secret using KEM with a public key. + // Returns both the ciphertext and shared secret. + static std::optional Encapsulate( + const EVPKeyPointer& public_key); + + // Decapsulate a shared secret using KEM with a private key and ciphertext. + // Returns the shared secret. + static DataPointer Decapsulate(const EVPKeyPointer& private_key, + const Buffer& ciphertext); + + private: +#if !OPENSSL_VERSION_PREREQ(3, 5) + static bool SetOperationParameter(EVP_PKEY_CTX* ctx, + const EVPKeyPointer& key); +#endif +}; + +#endif // OPENSSL_VERSION_MAJOR >= 3 + // ============================================================================ // Version metadata #define NCRYPTO_VERSION "0.0.1" diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 586e256..e1f6937 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -4703,6 +4703,127 @@ const Digest Digest::FromName(const char* name) { return ncrypto::getDigestByName(name); } +// ============================================================================ +// KEM Implementation +#if OPENSSL_VERSION_MAJOR >= 3 +#if !OPENSSL_VERSION_PREREQ(3, 5) +bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { + const char* operation = nullptr; + + switch (EVP_PKEY_id(key.get())) { + case EVP_PKEY_RSA: + operation = OSSL_KEM_PARAM_OPERATION_RSASVE; + break; +#if OPENSSL_VERSION_PREREQ(3, 2) + case EVP_PKEY_EC: + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + operation = OSSL_KEM_PARAM_OPERATION_DHKEM; + break; +#endif + default: + unreachable(); + } + + if (operation != nullptr) { + OSSL_PARAM params[] = { + OSSL_PARAM_utf8_string( + OSSL_KEM_PARAM_OPERATION, const_cast(operation), 0), + OSSL_PARAM_END}; + + if (EVP_PKEY_CTX_set_params(ctx, params) <= 0) { + return false; + } + } + + return true; +} +#endif + +std::optional KEM::Encapsulate( + const EVPKeyPointer& public_key) { + ClearErrorOnReturn clear_error_on_return; + + auto ctx = public_key.newCtx(); + if (!ctx) return std::nullopt; + + if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) { + return std::nullopt; + } + +#if !OPENSSL_VERSION_PREREQ(3, 5) + if (!SetOperationParameter(ctx.get(), public_key)) { + return std::nullopt; + } +#endif + + // Determine output buffer sizes + size_t ciphertext_len = 0; + size_t shared_key_len = 0; + + if (EVP_PKEY_encapsulate( + ctx.get(), nullptr, &ciphertext_len, nullptr, &shared_key_len) <= 0) { + return std::nullopt; + } + + auto ciphertext = DataPointer::Alloc(ciphertext_len); + auto shared_key = DataPointer::Alloc(shared_key_len); + if (!ciphertext || !shared_key) return std::nullopt; + + if (EVP_PKEY_encapsulate(ctx.get(), + static_cast(ciphertext.get()), + &ciphertext_len, + static_cast(shared_key.get()), + &shared_key_len) <= 0) { + return std::nullopt; + } + + return EncapsulateResult(std::move(ciphertext), std::move(shared_key)); +} + +DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, + const Buffer& ciphertext) { + ClearErrorOnReturn clear_error_on_return; + + auto ctx = private_key.newCtx(); + if (!ctx) return {}; + + if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) { + return {}; + } + +#if !OPENSSL_VERSION_PREREQ(3, 5) + if (!SetOperationParameter(ctx.get(), private_key)) { + return {}; + } +#endif + + // First pass: determine shared secret size + size_t shared_key_len = 0; + if (EVP_PKEY_decapsulate(ctx.get(), + nullptr, + &shared_key_len, + static_cast(ciphertext.data), + ciphertext.len) <= 0) { + return {}; + } + + auto shared_key = DataPointer::Alloc(shared_key_len); + if (!shared_key) return {}; + + if (EVP_PKEY_decapsulate(ctx.get(), + static_cast(shared_key.get()), + &shared_key_len, + static_cast(ciphertext.data), + ciphertext.len) <= 0) { + return {}; + } + + return shared_key; +} + +#endif // OPENSSL_VERSION_MAJOR >= 3 + } // namespace ncrypto // =========================================================================== From d473356774b25521c85306613071e7ff9955024d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 21 Aug 2025 11:09:06 +0200 Subject: [PATCH 14/21] crypto: support ML-KEM in Web Cryptography MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59569 Reviewed-By: Tobias Nießen Reviewed-By: James M Snell --- src/ncrypto.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index e1f6937..3141f06 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -2312,21 +2312,34 @@ DataPointer EVPKeyPointer::rawPublicKey() const { #if OPENSSL_WITH_PQC DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; + + // Determine seed length and parameter name based on key type + size_t seed_len; + const char* param_name; + switch (id()) { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: + seed_len = 32; // ML-DSA uses 32-byte seeds + param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; + break; + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + seed_len = 64; // ML-KEM uses 64-byte seeds + param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; break; default: unreachable(); } - size_t seed_len = 32; if (auto data = DataPointer::Alloc(seed_len)) { const Buffer buf = data; size_t len = data.size(); + if (EVP_PKEY_get_octet_string_param( - get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1) + get(), param_name, buf.data, len, &seed_len) != 1) return {}; return data; } From bc327d3a0914a953b9d9dc727084dcbaec83ff0d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 24 Aug 2025 11:47:20 +0200 Subject: [PATCH 15/21] crypto: add AES-OCB Web Cryptography algorithm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59539 Reviewed-By: Tobias Nießen Reviewed-By: James M Snell --- include/ncrypto.h | 4 ++++ src/ncrypto.cpp | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index 796167c..6a61d12 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -380,6 +380,9 @@ class Cipher final { 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 { @@ -755,6 +758,7 @@ class CipherCtxPointer final { int getNid() const; bool isGcmMode() const; + bool isOcbMode() const; bool isCcmMode() const; bool isWrapMode() const; bool isChaCha20Poly1305() const; diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 3141f06..77ef31a 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -3201,6 +3201,9 @@ 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); +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::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); bool Cipher::isGcmMode() const { @@ -3476,6 +3479,11 @@ bool CipherCtxPointer::isGcmMode() const { return getMode() == EVP_CIPH_GCM_MODE; } +bool CipherCtxPointer::isOcbMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_OCB_MODE; +} + bool CipherCtxPointer::isCcmMode() const { if (!ctx_) return false; return getMode() == EVP_CIPH_CCM_MODE; From 8e5060044cd88c712f1d64dea5496b309a4e90da Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 26 Aug 2025 17:41:25 +0200 Subject: [PATCH 16/21] crypto: support SLH-DSA KeyObject, sign, and verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/59537 Reviewed-By: Tobias Nießen --- src/ncrypto.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 77ef31a..5f4e900 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -38,6 +38,18 @@ constexpr static PQCMapping pqc_mappings[] = { {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, + {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, + {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, + {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, + {"SLH-DSA-SHA2-192s", EVP_PKEY_SLH_DSA_SHA2_192S}, + {"SLH-DSA-SHA2-256f", EVP_PKEY_SLH_DSA_SHA2_256F}, + {"SLH-DSA-SHA2-256s", EVP_PKEY_SLH_DSA_SHA2_256S}, + {"SLH-DSA-SHAKE-128f", EVP_PKEY_SLH_DSA_SHAKE_128F}, + {"SLH-DSA-SHAKE-128s", EVP_PKEY_SLH_DSA_SHAKE_128S}, + {"SLH-DSA-SHAKE-192f", EVP_PKEY_SLH_DSA_SHAKE_192F}, + {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, + {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, + {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, }; #endif @@ -2804,6 +2816,18 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: + case EVP_PKEY_SLH_DSA_SHA2_128F: + case EVP_PKEY_SLH_DSA_SHA2_128S: + case EVP_PKEY_SLH_DSA_SHA2_192F: + case EVP_PKEY_SLH_DSA_SHA2_192S: + case EVP_PKEY_SLH_DSA_SHA2_256F: + case EVP_PKEY_SLH_DSA_SHA2_256S: + case EVP_PKEY_SLH_DSA_SHAKE_128F: + case EVP_PKEY_SLH_DSA_SHAKE_128S: + case EVP_PKEY_SLH_DSA_SHAKE_192F: + case EVP_PKEY_SLH_DSA_SHAKE_192S: + case EVP_PKEY_SLH_DSA_SHAKE_256F: + case EVP_PKEY_SLH_DSA_SHAKE_256S: #endif return true; default: From e2b9db756f1f70277540e1fb1b4bd489f994af92 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 7 Sep 2025 00:43:15 +0200 Subject: [PATCH 17/21] crypto: add KMAC Web Cryptography algorithms PR-URL: https://github.com/nodejs/node/pull/59647 Reviewed-By: Anna Henningsen Reviewed-By: James M Snell --- include/ncrypto.h | 52 +++++++++++++++++++++++++++ src/ncrypto.cpp | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index 6a61d12..bf85eef 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -235,6 +235,8 @@ class DataPointer; class DHPointer; class ECKeyPointer; class EVPKeyPointer; +class EVPMacCtxPointer; +class EVPMacPointer; class EVPMDCtxPointer; class SSLCtxPointer; class SSLPointer; @@ -1471,6 +1473,56 @@ class HMACCtxPointer final { DeleteFnPtr ctx_; }; +#if OPENSSL_VERSION_MAJOR >= 3 +class EVPMacPointer final { + public: + EVPMacPointer() = default; + explicit EVPMacPointer(EVP_MAC* mac); + EVPMacPointer(EVPMacPointer&& other) noexcept; + EVPMacPointer& operator=(EVPMacPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(EVPMacPointer) + ~EVPMacPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return mac_ == nullptr; } + inline operator bool() const { return mac_ != nullptr; } + inline EVP_MAC* get() const { return mac_.get(); } + inline operator EVP_MAC*() const { return mac_.get(); } + void reset(EVP_MAC* mac = nullptr); + EVP_MAC* release(); + + static EVPMacPointer Fetch(const char* algorithm); + + private: + DeleteFnPtr mac_; +}; + +class EVPMacCtxPointer final { + public: + EVPMacCtxPointer() = default; + explicit EVPMacCtxPointer(EVP_MAC_CTX* ctx); + EVPMacCtxPointer(EVPMacCtxPointer&& other) noexcept; + EVPMacCtxPointer& operator=(EVPMacCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(EVPMacCtxPointer) + ~EVPMacCtxPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return ctx_ == nullptr; } + inline operator bool() const { return ctx_ != nullptr; } + inline EVP_MAC_CTX* get() const { return ctx_.get(); } + inline operator EVP_MAC_CTX*() const { return ctx_.get(); } + void reset(EVP_MAC_CTX* ctx = nullptr); + EVP_MAC_CTX* release(); + + bool init(const Buffer& key, const OSSL_PARAM* params = nullptr); + bool update(const Buffer& data); + DataPointer final(size_t length); + + static EVPMacCtxPointer New(EVP_MAC* mac); + + private: + DeleteFnPtr ctx_; +}; +#endif // OPENSSL_VERSION_MAJOR >= 3 + #ifndef OPENSSL_NO_ENGINE class EnginePointer final { public: diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 5f4e900..c0017f2 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -4606,6 +4606,96 @@ HMACCtxPointer HMACCtxPointer::New() { return HMACCtxPointer(HMAC_CTX_new()); } +#if OPENSSL_VERSION_MAJOR >= 3 +EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {} + +EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept + : mac_(std::move(other.mac_)) {} + +EVPMacPointer& EVPMacPointer::operator=(EVPMacPointer&& other) noexcept { + if (this == &other) return *this; + mac_ = std::move(other.mac_); + return *this; +} + +EVPMacPointer::~EVPMacPointer() { + mac_.reset(); +} + +void EVPMacPointer::reset(EVP_MAC* mac) { + mac_.reset(mac); +} + +EVP_MAC* EVPMacPointer::release() { + return mac_.release(); +} + +EVPMacPointer EVPMacPointer::Fetch(const char* algorithm) { + return EVPMacPointer(EVP_MAC_fetch(nullptr, algorithm, nullptr)); +} + +EVPMacCtxPointer::EVPMacCtxPointer(EVP_MAC_CTX* ctx) : ctx_(ctx) {} + +EVPMacCtxPointer::EVPMacCtxPointer(EVPMacCtxPointer&& other) noexcept + : ctx_(std::move(other.ctx_)) {} + +EVPMacCtxPointer& EVPMacCtxPointer::operator=( + EVPMacCtxPointer&& other) noexcept { + if (this == &other) return *this; + ctx_ = std::move(other.ctx_); + return *this; +} + +EVPMacCtxPointer::~EVPMacCtxPointer() { + ctx_.reset(); +} + +void EVPMacCtxPointer::reset(EVP_MAC_CTX* ctx) { + ctx_.reset(ctx); +} + +EVP_MAC_CTX* EVPMacCtxPointer::release() { + return ctx_.release(); +} + +bool EVPMacCtxPointer::init(const Buffer& key, + const OSSL_PARAM* params) { + if (!ctx_) return false; + return EVP_MAC_init(ctx_.get(), + static_cast(key.data), + key.len, + params) == 1; +} + +bool EVPMacCtxPointer::update(const Buffer& data) { + if (!ctx_) return false; + return EVP_MAC_update(ctx_.get(), + static_cast(data.data), + data.len) == 1; +} + +DataPointer EVPMacCtxPointer::final(size_t length) { + if (!ctx_) return {}; + auto buf = DataPointer::Alloc(length); + if (!buf) return {}; + + size_t result_len = length; + if (EVP_MAC_final(ctx_.get(), + static_cast(buf.get()), + &result_len, + length) != 1) { + return {}; + } + + return buf; +} + +EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) { + if (!mac) return EVPMacCtxPointer(); + return EVPMacCtxPointer(EVP_MAC_CTX_new(mac)); +} +#endif // OPENSSL_VERSION_MAJOR >= 3 + DataPointer hashDigest(const Buffer& buf, const EVP_MD* md) { if (md == nullptr) return {}; From 2580833674f15d619e0940815c7738923f8ad40a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 11 Aug 2025 10:12:44 +0200 Subject: [PATCH 18/21] crypto: support Ed448 and ML-DSA context parameter in Web Cryptography PR-URL: https://github.com/nodejs/node/pull/59570 Reviewed-By: James M Snell --- include/ncrypto.h | 9 +++++++++ src/ncrypto.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index bf85eef..5c2b8ee 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -1429,6 +1429,15 @@ class EVPMDCtxPointer final { std::optional verifyInit(const EVPKeyPointer& key, const Digest& digest); + std::optional signInitWithContext( + const EVPKeyPointer& key, + const Digest& digest, + const Buffer& context_string); + std::optional verifyInitWithContext( + const EVPKeyPointer& key, + const Digest& digest, + const Buffer& context_string); + DataPointer signOneShot(const Buffer& buf) const; DataPointer sign(const Buffer& buf) const; bool verify(const Buffer& buf, diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index c0017f2..96ec3d4 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -4481,6 +4481,54 @@ std::optional EVPMDCtxPointer::verifyInit( return ctx; } +std::optional EVPMDCtxPointer::signInitWithContext( + const EVPKeyPointer& key, + const Digest& digest, + const Buffer& context_string) { +#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING + EVP_PKEY_CTX* ctx = nullptr; + + const OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_CONTEXT_STRING, + const_cast(context_string.data), + context_string.len), + OSSL_PARAM_END}; + + if (!EVP_DigestSignInit_ex( + ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) { + return std::nullopt; + } + return ctx; +#else + return std::nullopt; +#endif +} + +std::optional EVPMDCtxPointer::verifyInitWithContext( + const EVPKeyPointer& key, + const Digest& digest, + const Buffer& context_string) { +#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING + EVP_PKEY_CTX* ctx = nullptr; + + const OSSL_PARAM params[] = { + OSSL_PARAM_construct_octet_string( + OSSL_SIGNATURE_PARAM_CONTEXT_STRING, + const_cast(context_string.data), + context_string.len), + OSSL_PARAM_END}; + + if (!EVP_DigestVerifyInit_ex( + ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) { + return std::nullopt; + } + return ctx; +#else + return std::nullopt; +#endif +} + DataPointer EVPMDCtxPointer::signOneShot( const Buffer& buf) const { if (!ctx_) return {}; From 39e350757997c90ddaf681e24912f0f3b8d161f5 Mon Sep 17 00:00:00 2001 From: Patrick Costa Date: Tue, 16 Sep 2025 16:27:04 -0300 Subject: [PATCH 19/21] crypto: expose signatureAlgorithm on X509Certificate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the `signatureAlgorithm` property to a X509Certificate allowing users to retrieve a string representing the algorithm used to sign the certificate. This string is defined by the OpenSSL library. Fixes: https://github.com/nodejs/node/issues/59103 PR-URL: https://github.com/nodejs/node/pull/59235 Reviewed-By: James M Snell Reviewed-By: Filip Skokan Reviewed-By: Joyee Cheung Reviewed-By: Tobias Nießen --- include/ncrypto.h | 2 ++ src/ncrypto.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/include/ncrypto.h b/include/ncrypto.h index 5c2b8ee..49dbed3 100644 --- a/include/ncrypto.h +++ b/include/ncrypto.h @@ -1211,6 +1211,8 @@ class X509View final { BIOPointer getInfoAccess() const; BIOPointer getValidFrom() const; BIOPointer getValidTo() const; + std::optional getSignatureAlgorithm() const; + std::optional getSignatureAlgorithmOID() const; int64_t getValidFromTime() const; int64_t getValidToTime() const; DataPointer getSerialNumber() const; diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 96ec3d4..463b2f0 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -16,7 +16,9 @@ #endif #include +#include #include +#include #if OPENSSL_VERSION_MAJOR >= 3 #include #include @@ -1155,6 +1157,29 @@ BIOPointer X509View::getValidTo() const { return bio; } +std::optional X509View::getSignatureAlgorithm() const { + if (cert_ == nullptr) return std::nullopt; + int nid = X509_get_signature_nid(cert_); + if (nid == NID_undef) return std::nullopt; + const char* ln = OBJ_nid2ln(nid); + if (ln == nullptr) return std::nullopt; + return std::string_view(ln); +} + +std::optional X509View::getSignatureAlgorithmOID() const { + if (cert_ == nullptr) return std::nullopt; + const X509_ALGOR* alg = nullptr; + X509_get0_signature(nullptr, &alg, cert_); + if (alg == nullptr) return std::nullopt; + const ASN1_OBJECT* obj = nullptr; + X509_ALGOR_get0(&obj, nullptr, nullptr, alg); + if (obj == nullptr) return std::nullopt; + std::array buf{}; + int len = OBJ_obj2txt(buf.data(), buf.size(), obj, 1); + if (len < 0 || static_cast(len) >= buf.size()) return std::nullopt; + return std::string(buf.data(), static_cast(len)); +} + int64_t X509View::getValidToTime() const { #ifdef OPENSSL_IS_BORINGSSL #ifndef NCRYPTO_NO_ASN1_TIME From 8492c0410038319d860e2ef1cb688ecb39974ca6 Mon Sep 17 00:00:00 2001 From: Nicholas Paun Date: Thu, 25 Sep 2025 14:28:40 -0700 Subject: [PATCH 20/21] Temporarily guard OCB constants not available in boring --- src/ncrypto.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ncrypto.cpp b/src/ncrypto.cpp index 463b2f0..75a28bb 100644 --- a/src/ncrypto.cpp +++ b/src/ncrypto.cpp @@ -3250,9 +3250,13 @@ 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); + +#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); +#endif + const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); bool Cipher::isGcmMode() const { From ed50d63a1c95ac382d6dca2c3743cef94ac6ac9b Mon Sep 17 00:00:00 2001 From: Nicholas Paun Date: Fri, 26 Sep 2025 15:19:17 -0700 Subject: [PATCH 21/21] link against libdecrepit for EVP_CIPHER_do_all_sorted --- BUILD.bazel | 4 ++- WORKSPACE | 4 +++ ...pit-so-NodeJS-can-use-it-for-ncrypto.patch | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch diff --git a/BUILD.bazel b/BUILD.bazel index dfd6c1a..c6ea9e2 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -5,6 +5,8 @@ cc_library( includes = ["include"], visibility = ["//visibility:public"], deps = [ - "@ssl" + "@ssl//:ssl", + "@ssl//:crypto", + "@ssl//:decrepit" ] ) diff --git a/WORKSPACE b/WORKSPACE index d4bfe6d..036ab5b 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -8,4 +8,8 @@ http_archive( strip_prefix = "boringssl-0.20250818.0", type = "tgz", urls = ["https://github.com/google/boringssl/archive/refs/tags/0.20250818.0.tar.gz"], + patches = [ + "@ncrypto//:patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch" + ], + patch_strip = 1 ) diff --git a/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch b/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch new file mode 100644 index 0000000..e058724 --- /dev/null +++ b/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch @@ -0,0 +1,28 @@ +From feda75fa93bb54bc697f1f771a0aec573ab87ab7 Mon Sep 17 00:00:00 2001 +From: Nicholas Paun +Date: Fri, 26 Sep 2025 14:57:57 -0700 +Subject: [PATCH] Expose libdecrepit so NodeJS can use it for ncrypto + +--- + BUILD.bazel | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/BUILD.bazel b/BUILD.bazel +index b7dc35932..7d214716c 100644 +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -155,10 +155,10 @@ bssl_cc_binary( + ], + ) + +-# Build, but do not export libdecrepit. + bssl_cc_library( + name = "decrepit", + srcs = decrepit_sources, ++ visibility = ["//visibility:public"], + copts = ["-DBORINGSSL_IMPLEMENTATION"], + internal_hdrs = decrepit_internal_headers, + deps = [ +-- +2.50.1 (Apple Git-155) +