diff --git a/node.gyp b/node.gyp index 368b621b1239b0..55c214e36caf39 100644 --- a/node.gyp +++ b/node.gyp @@ -411,6 +411,7 @@ 'src/crypto/crypto_timing.cc', 'src/crypto/crypto_cipher.cc', 'src/crypto/crypto_context.cc', + 'src/crypto/crypto_tls_certificates.cc', 'src/crypto/crypto_ec.cc', 'src/crypto/crypto_pqc.cc', 'src/crypto/crypto_kem.cc', @@ -449,6 +450,7 @@ 'src/crypto/crypto_tls.h', 'src/crypto/crypto_clienthello.h', 'src/crypto/crypto_context.h', + 'src/crypto/crypto_tls_certificates.h', 'src/crypto/crypto_ec.h', 'src/crypto/crypto_pqc.h', 'src/crypto/crypto_hkdf.h', diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 5447de596f98ec..a35446e09a9cb0 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -2,6 +2,7 @@ #include "base_object-inl.h" #include "crypto/crypto_bio.h" #include "crypto/crypto_common.h" +#include "crypto/crypto_tls_certificates.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -1731,22 +1732,14 @@ void SecureContext::SetKey(const FunctionCallbackInfo& args) { ByteSource passphrase; if (args[1]->IsString()) passphrase = ByteSource::FromString(env, args[1].As()); - // This redirection is necessary because the PasswordCallback expects a - // pointer to a pointer to the passphrase ByteSource to allow passing in - // const ByteSources. - const ByteSource* pass_ptr = &passphrase; - - EVPKeyPointer key( - PEM_read_bio_PrivateKey(bio.get(), - nullptr, - PasswordCallback, - &pass_ptr)); - - if (!key) - return ThrowCryptoError(env, ERR_get_error(), "PEM_read_bio_PrivateKey"); - - if (!SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get())) - return ThrowCryptoError(env, ERR_get_error(), "SSL_CTX_use_PrivateKey"); + switch (UsePrivateKey(sc->ctx_.get(), bio, &passphrase)) { + case PrivateKeyResult::kSuccess: + break; + case PrivateKeyResult::kParseError: + return ThrowCryptoError(env, ERR_get_error(), "PEM_read_bio_PrivateKey"); + case PrivateKeyResult::kApplyError: + return ThrowCryptoError(env, ERR_get_error(), "SSL_CTX_use_PrivateKey"); + } } void SecureContext::SetSigalgs(const FunctionCallbackInfo& args) { @@ -1849,16 +1842,7 @@ void SecureContext::SetX509StoreFlag(unsigned long flags) { } X509_STORE* SecureContext::GetCertStoreOwnedByThisSecureContext() { - Environment* env = this->env(); - if (own_cert_store_cache_ != nullptr) return own_cert_store_cache_; - - X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get()); - if (cert_store == GetOrCreateRootCertStore(env)) { - cert_store = NewRootCertStore(env); - SSL_CTX_set_cert_store(ctx_.get(), cert_store); - } - - return own_cert_store_cache_ = cert_store; + return GetOrCreateOwnedCertStore(env(), ctx_.get(), &own_cert_store_cache_); } void SecureContext::SetAllowPartialTrustChain( @@ -1870,13 +1854,7 @@ void SecureContext::SetAllowPartialTrustChain( void SecureContext::SetCACert(const BIOPointer& bio) { ClearErrorOnReturn clear_error_on_return; - if (!bio) return; - while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX( - bio.get(), nullptr, NoPasswordCallback, nullptr))) { - CHECK_EQ(1, - X509_STORE_add_cert(GetCertStoreOwnedByThisSecureContext(), x509)); - CHECK_EQ(1, SSL_CTX_add_client_CA(ctx_.get(), x509)); - } + AddCACertificates(env(), ctx_.get(), bio, &own_cert_store_cache_); } void SecureContext::AddCACert(const FunctionCallbackInfo& args) { @@ -1896,20 +1874,11 @@ Maybe SecureContext::SetCRL(Environment* env, const BIOPointer& bio) { // TODO(tniessen): this should be checked by the caller and not treated as ok if (!bio) return JustVoid(); - DeleteFnPtr crl( - PEM_read_bio_X509_CRL(bio.get(), nullptr, NoPasswordCallback, nullptr)); - - if (!crl) { + if (!crypto::AddCRL(env, ctx_.get(), bio, &own_cert_store_cache_)) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to parse CRL"); return Nothing(); } - X509_STORE* cert_store = GetCertStoreOwnedByThisSecureContext(); - - CHECK_EQ(1, X509_STORE_add_crl(cert_store, crl.get())); - CHECK_EQ(1, - X509_STORE_set_flags( - cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)); return JustVoid(); } @@ -1927,12 +1896,7 @@ void SecureContext::AddCRL(const FunctionCallbackInfo& args) { void SecureContext::SetRootCerts() { ClearErrorOnReturn clear_error_on_return; - Environment* env = this->env(); - auto store = GetOrCreateRootCertStore(env); - - // Increment reference count so global store is not deleted along with CTX. - X509_STORE_up_ref(store); - SSL_CTX_set_cert_store(ctx_.get(), store); + UseDefaultRootCertStore(env(), ctx_.get()); } void SecureContext::AddRootCerts(const FunctionCallbackInfo& args) { diff --git a/src/crypto/crypto_context.h b/src/crypto/crypto_context.h index 95ddea4c262dd5..73aff5b628a165 100644 --- a/src/crypto/crypto_context.h +++ b/src/crypto/crypto_context.h @@ -201,11 +201,6 @@ class SecureContext final : public BaseObject { #endif }; -int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, - ncrypto::BIOPointer&& in, - ncrypto::X509Pointer* cert, - ncrypto::X509Pointer* issuer); - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_tls_certificates.cc b/src/crypto/crypto_tls_certificates.cc new file mode 100644 index 00000000000000..ace1d41aefecc5 --- /dev/null +++ b/src/crypto/crypto_tls_certificates.cc @@ -0,0 +1,96 @@ +#include "crypto/crypto_tls_certificates.h" + +#include "crypto/crypto_context.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" + +namespace node::crypto { + +using ncrypto::DeleteFnPtr; +using ncrypto::EVPKeyPointer; +using ncrypto::X509Pointer; + +X509_STORE* GetOrCreateOwnedCertStore(Environment* env, + SSL_CTX* ctx, + X509_STORE** cache) { + if (cache != nullptr && *cache != nullptr) return *cache; + + X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx); + if (cert_store == GetOrCreateRootCertStore(env)) { + cert_store = NewRootCertStore(env); + SSL_CTX_set_cert_store(ctx, cert_store); + } + + if (cache != nullptr) *cache = cert_store; + return cert_store; +} + +void UseDefaultRootCertStore(Environment* env, SSL_CTX* ctx) { + X509_STORE* store = GetOrCreateRootCertStore(env); + + // Increment reference count so global store is not deleted along with CTX. + X509_STORE_up_ref(store); + SSL_CTX_set_cert_store(ctx, store); +} + +size_t AddCACertificates(Environment* env, + SSL_CTX* ctx, + const ncrypto::BIOPointer& bio, + X509_STORE** cache) { + if (!bio) return 0; + + size_t count = 0; + while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX( + bio.get(), nullptr, NoPasswordCallback, nullptr))) { + CHECK_EQ(1, + X509_STORE_add_cert(GetOrCreateOwnedCertStore(env, ctx, cache), + x509.get())); + CHECK_EQ(1, SSL_CTX_add_client_CA(ctx, x509.get())); + count++; + } + return count; +} + +bool AddCRL(Environment* env, + SSL_CTX* ctx, + const ncrypto::BIOPointer& bio, + X509_STORE** cache) { + if (!bio) return false; + + DeleteFnPtr crl( + PEM_read_bio_X509_CRL(bio.get(), nullptr, NoPasswordCallback, nullptr)); + if (!crl) return false; + + X509_STORE* cert_store = GetOrCreateOwnedCertStore(env, ctx, cache); + CHECK_EQ(1, X509_STORE_add_crl(cert_store, crl.get())); + CHECK_EQ(1, + X509_STORE_set_flags( + cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)); + return true; +} + +PrivateKeyResult UsePrivateKey(SSL_CTX* ctx, + const ncrypto::BIOPointer& bio, + const ByteSource* passphrase) { + if (!bio) return PrivateKeyResult::kParseError; + + ByteSource empty_passphrase; + if (passphrase == nullptr) passphrase = &empty_passphrase; + // This redirection is necessary because the PasswordCallback expects a + // pointer to a pointer to the passphrase ByteSource to allow passing in + // const ByteSources. + const ByteSource* pass_ptr = passphrase; + EVPKeyPointer key( + PEM_read_bio_PrivateKey(bio.get(), nullptr, PasswordCallback, &pass_ptr)); + if (!key) return PrivateKeyResult::kParseError; + if (!SSL_CTX_use_PrivateKey(ctx, key.get())) + return PrivateKeyResult::kApplyError; + return PrivateKeyResult::kSuccess; +} + +bool UsePrivateKey(SSL_CTX* ctx, const KeyObjectData& key) { + if (key.GetKeyType() != KeyType::kKeyTypePrivate) return false; + return SSL_CTX_use_PrivateKey(ctx, key.GetAsymmetricKey().get()); +} + +} // namespace node::crypto diff --git a/src/crypto/crypto_tls_certificates.h b/src/crypto/crypto_tls_certificates.h new file mode 100644 index 00000000000000..e36a583e6d4be6 --- /dev/null +++ b/src/crypto/crypto_tls_certificates.h @@ -0,0 +1,60 @@ +#ifndef SRC_CRYPTO_CRYPTO_TLS_CERTIFICATES_H_ +#define SRC_CRYPTO_CRYPTO_TLS_CERTIFICATES_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "ncrypto.h" + +namespace node { + +class Environment; + +namespace crypto { + +class ByteSource; +class KeyObjectData; + +// Return a store that may be modified by this SSL_CTX. If the context uses +// Node's shared root store, replace it with a private copy first. +X509_STORE* GetOrCreateOwnedCertStore(Environment* env, + SSL_CTX* ctx, + X509_STORE** cache = nullptr); + +// Attach Node's process-wide default root store to the SSL_CTX. +void UseDefaultRootCertStore(Environment* env, SSL_CTX* ctx); + +// Add every PEM certificate in |bio| to the context's certificate store and +// acceptable-client-CA list. Returns the number of certificates read. +size_t AddCACertificates(Environment* env, + SSL_CTX* ctx, + const ncrypto::BIOPointer& bio, + X509_STORE** cache = nullptr); + +// Add one PEM CRL and enable CRL checking. +bool AddCRL(Environment* env, + SSL_CTX* ctx, + const ncrypto::BIOPointer& bio, + X509_STORE** cache = nullptr); + +enum class PrivateKeyResult { + kSuccess, + kParseError, + kApplyError, +}; + +PrivateKeyResult UsePrivateKey(SSL_CTX* ctx, + const ncrypto::BIOPointer& bio, + const ByteSource* passphrase = nullptr); + +bool UsePrivateKey(SSL_CTX* ctx, const KeyObjectData& key); + +int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, + ncrypto::BIOPointer&& in, + ncrypto::X509Pointer* cert, + ncrypto::X509Pointer* issuer); + +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_TLS_CERTIFICATES_H_ diff --git a/src/dtls/dtls_context.cc b/src/dtls/dtls_context.cc index ca5003df46c295..c9be55a5d3f32c 100644 --- a/src/dtls/dtls_context.cc +++ b/src/dtls/dtls_context.cc @@ -4,6 +4,8 @@ #if HAVE_OPENSSL && HAVE_DTLS #include +#include +#include #include #include #include @@ -180,35 +182,17 @@ void DTLSContext::SetCert(const FunctionCallbackInfo& args) { Utf8Value cert_pem(env->isolate(), args[0]); - BIO* bio = BIO_new_mem_buf(*cert_pem, cert_pem.length()); - if (bio == nullptr) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "BIO_new_mem_buf failed"); + auto bio = crypto::NodeBIO::NewFixed(*cert_pem, cert_pem.length()); + if (!bio) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to create BIO"); } - X509* x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); - if (x509 == nullptr) { - BIO_free(bio); + ncrypto::X509Pointer cert; + ncrypto::X509Pointer issuer; + if (crypto::SSL_CTX_use_certificate_chain( + ctx->ctx_.get(), std::move(bio), &cert, &issuer) != 1) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "PEM_read_bio_X509 failed"); } - - int ret = SSL_CTX_use_certificate(ctx->ctx_.get(), x509); - X509_free(x509); - - // Read any additional chain certificates. - while ((x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != - nullptr) { - SSL_CTX_add_extra_chain_cert(ctx->ctx_.get(), x509); - // Note: SSL_CTX_add_extra_chain_cert takes ownership, don't free x509. - } - - // Clear any error from the chain reading loop (expected EOF). - ERR_clear_error(); - BIO_free(bio); - - if (ret != 1) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "SSL_CTX_use_certificate failed"); - } } void DTLSContext::SetKey(const FunctionCallbackInfo& args) { @@ -222,25 +206,20 @@ void DTLSContext::SetKey(const FunctionCallbackInfo& args) { Utf8Value key_pem(env->isolate(), args[0]); - BIO* bio = BIO_new_mem_buf(*key_pem, key_pem.length()); - if (bio == nullptr) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "BIO_new_mem_buf failed"); + auto bio = crypto::NodeBIO::NewFixed(*key_pem, key_pem.length()); + if (!bio) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to create BIO"); } - EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); - BIO_free(bio); - - if (pkey == nullptr) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "PEM_read_bio_PrivateKey failed"); - } - - int ret = SSL_CTX_use_PrivateKey(ctx->ctx_.get(), pkey); - EVP_PKEY_free(pkey); - - if (ret != 1) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "SSL_CTX_use_PrivateKey failed"); + switch (crypto::UsePrivateKey(ctx->ctx_.get(), bio)) { + case crypto::PrivateKeyResult::kSuccess: + break; + case crypto::PrivateKeyResult::kParseError: + return THROW_ERR_CRYPTO_OPERATION_FAILED( + env, "PEM_read_bio_PrivateKey failed"); + case crypto::PrivateKeyResult::kApplyError: + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "SSL_CTX_use_PrivateKey failed"); } // Verify that the private key matches the certificate. @@ -261,24 +240,13 @@ void DTLSContext::AddCACert(const FunctionCallbackInfo& args) { Utf8Value ca_pem(env->isolate(), args[0]); - BIO* bio = BIO_new_mem_buf(*ca_pem, ca_pem.length()); - if (bio == nullptr) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "BIO_new_mem_buf failed"); - } - - X509_STORE* store = SSL_CTX_get_cert_store(ctx->ctx_.get()); - X509* x509; - int count = 0; - while ((x509 = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != - nullptr) { - X509_STORE_add_cert(store, x509); - X509_free(x509); - count++; + auto bio = crypto::NodeBIO::NewFixed(*ca_pem, ca_pem.length()); + if (!bio) { + return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to create BIO"); } - ERR_clear_error(); - BIO_free(bio); - if (count == 0) { + ncrypto::ClearErrorOnReturn clear_error_on_return; + if (crypto::AddCACertificates(env, ctx->ctx_.get(), bio) == 0) { return THROW_ERR_CRYPTO_OPERATION_FAILED( env, "No CA certificates found in PEM data"); } @@ -349,7 +317,7 @@ void DTLSContext::SetVerifyMode(const FunctionCallbackInfo& args) { void DTLSContext::LoadDefaultCAs(const FunctionCallbackInfo& args) { DTLSContext* ctx; ASSIGN_OR_RETURN_UNWRAP(&ctx, args.This()); - SSL_CTX_set_default_verify_paths(ctx->ctx_.get()); + crypto::UseDefaultRootCertStore(ctx->env(), ctx->ctx_.get()); } void DTLSContext::SetECDHCurve(const FunctionCallbackInfo& args) { diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index cbbee77c85c109..e816bad3dbf690 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -3,6 +3,7 @@ #ifndef OPENSSL_NO_QUIC #include #include +#include #include #include #include @@ -32,7 +33,6 @@ using ncrypto::MarkPopErrorOnReturn; using ncrypto::SSLCtxPointer; using ncrypto::SSLPointer; using ncrypto::SSLSessionPointer; -using ncrypto::X509Pointer; using v8::Array; using v8::ArrayBuffer; using v8::ArrayBufferView; @@ -528,12 +528,6 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { } } - // Only load system CA certificates if no custom CAs are provided. - // SSL_CTX_set_default_verify_paths involves filesystem I/O to read - // the system CA bundle. - if (options_.ca.empty()) { - SSL_CTX_set_default_verify_paths(ctx.get()); - } if (options_.keylog) { SSL_CTX_set_keylog_callback(ctx.get(), OnKeylog); } @@ -551,30 +545,16 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { { ClearErrorOnReturn clear_error_on_return; if (options_.ca.empty()) { - auto store = crypto::GetOrCreateRootCertStore(env); - X509_STORE_up_ref(store); - SSL_CTX_set_cert_store(ctx.get(), store); + crypto::UseDefaultRootCertStore(env, ctx.get()); } else { for (const auto& ca : options_.ca) { uv_buf_t buf = ca; if (buf.len == 0) { - auto store = crypto::GetOrCreateRootCertStore(env); - X509_STORE_up_ref(store); - SSL_CTX_set_cert_store(ctx.get(), store); + crypto::UseDefaultRootCertStore(env, ctx.get()); } else { BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len); CHECK(bio); - X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get()); - while ( - auto x509 = X509Pointer(PEM_read_bio_X509_AUX( - bio.get(), nullptr, crypto::NoPasswordCallback, nullptr))) { - if (cert_store == crypto::GetOrCreateRootCertStore(env)) { - cert_store = crypto::NewRootCertStore(env); - SSL_CTX_set_cert_store(ctx.get(), cert_store); - } - CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get())); - CHECK_EQ(1, SSL_CTX_add_client_CA(ctx.get(), x509.get())); - } + crypto::AddCACertificates(env, ctx.get(), bio); } } } @@ -643,11 +623,7 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { { ClearErrorOnReturn clear_error_on_return; for (const auto& key : options_.keys) { - if (key.GetKeyType() != crypto::KeyType::kKeyTypePrivate) { - validation_error_ = "Invalid key"; - return SSLCtxPointer(); - } - if (!SSL_CTX_use_PrivateKey(ctx.get(), key.GetAsymmetricKey().get())) { + if (!crypto::UsePrivateKey(ctx.get(), key)) { validation_error_ = "Invalid key"; return SSLCtxPointer(); } @@ -659,25 +635,10 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { for (const auto& crl : options_.crl) { uv_buf_t buf = crl; BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len); - DeleteFnPtr crlptr(PEM_read_bio_X509_CRL( - bio.get(), nullptr, crypto::NoPasswordCallback, nullptr)); - - if (!crlptr) { + if (!crypto::AddCRL(env, ctx.get(), bio)) { validation_error_ = "Invalid CRL"; return SSLCtxPointer(); } - - X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get()); - if (cert_store == crypto::GetOrCreateRootCertStore(env)) { - cert_store = crypto::NewRootCertStore(env); - SSL_CTX_set_cert_store(ctx.get(), cert_store); - } - - CHECK_EQ(1, X509_STORE_add_crl(cert_store, crlptr.get())); - CHECK_EQ( - 1, - X509_STORE_set_flags( - cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)); } } diff --git a/test/parallel/test-dtls-default-ca.mjs b/test/parallel/test-dtls-default-ca.mjs new file mode 100644 index 00000000000000..7c3475ddbbe9e9 --- /dev/null +++ b/test/parallel/test-dtls-default-ca.mjs @@ -0,0 +1,38 @@ +// Flags: --experimental-dtls --no-warnings + +// Test that DTLS uses Node's configurable default CA set when no explicit +// `ca` option is provided. + +import { hasCrypto, mustCall, skip } from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; + +if (!hasCrypto) { + skip('missing crypto'); +} + +if (!process.features.dtls) { + skip('DTLS is not enabled'); +} + +const { connect, listen } = await import('node:dtls'); +const { setDefaultCACertificates } = await import('node:tls'); + +const serverCert = fixtures.readKey('agent1-cert.pem'); +const serverKey = fixtures.readKey('agent1-key.pem'); +const ca = fixtures.readKey('ca1-cert.pem'); + +setDefaultCACertificates([ca]); + +const endpoint = listen(mustCall(), { + // Also cover the shared certificate-chain loader adopted by DTLS. + cert: Buffer.concat([serverCert, ca]), + key: serverKey, + host: '127.0.0.1', + port: 0, +}); + +const client = connect('127.0.0.1', endpoint.address.port); +await client.opened; + +await client.close(); +await endpoint.close();