tls,quic: commonize TLS cert handling between tls, dtls & quic

Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64711
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Tim Perry authored and GitHub committed 2026-08-09 18:56:10 -04:00
1 parent 8c8190f453
commit e79c6b31cc
8 files changed
+242 -158

No files matched your search

+2
View File
@@ -407,6 +407,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',
@@ -442,6 +443,7 @@
'src/crypto/crypto_scrypt.h',
'src/crypto/crypto_tls.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',
+13 -49
View File
@@ -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"
@@ -1739,22 +1740,14 @@ void SecureContext::SetKey(const FunctionCallbackInfo<Value>& args) {
ByteSource passphrase;
if (args[1]->IsString())
passphrase = ByteSource::FromString(env, args[1].As<String>());
// 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<Value>& args) {
@@ -1857,16 +1850,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(
@@ -1878,13 +1862,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<Value>& args) {
@@ -1904,20 +1882,11 @@ Maybe<void> 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<X509_CRL, X509_CRL_free> 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<void>();
}
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();
}
@@ -1935,12 +1904,7 @@ void SecureContext::AddCRL(const FunctionCallbackInfo<Value>& 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<Value>& args) {
-5
View File
@@ -207,11 +207,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
+96
View File
@@ -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<X509_CRL, X509_CRL_free> 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
+60
View File
@@ -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_
+27 -59
View File
@@ -4,6 +4,8 @@
#if HAVE_OPENSSL && HAVE_DTLS
#include <base_object-inl.h>
#include <crypto/crypto_bio.h>
#include <crypto/crypto_tls_certificates.h>
#include <crypto/crypto_util.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
@@ -186,35 +188,17 @@ void DTLSContext::SetCert(const FunctionCallbackInfo<Value>& 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<Value>& args) {
@@ -228,25 +212,20 @@ void DTLSContext::SetKey(const FunctionCallbackInfo<Value>& 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.
@@ -267,24 +246,13 @@ void DTLSContext::AddCACert(const FunctionCallbackInfo<Value>& 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");
auto bio = crypto::NodeBIO::NewFixed(*ca_pem, ca_pem.length());
if (!bio) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to create BIO");
}
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++;
}
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");
}
@@ -358,7 +326,7 @@ void DTLSContext::SetVerifyMode(const FunctionCallbackInfo<Value>& args) {
void DTLSContext::LoadDefaultCAs(const FunctionCallbackInfo<Value>& 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<Value>& args) {
+6 -45
View File
@@ -3,6 +3,7 @@
#ifndef OPENSSL_NO_QUIC
#include <async_wrap-inl.h>
#include <base_object-inl.h>
#include <crypto/crypto_tls_certificates.h>
#include <crypto/crypto_util.h>
#include <debug_utils-inl.h>
#include <env-inl.h>
@@ -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<X509_CRL, X509_CRL_free> 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));
}
}
+38
View File
@@ -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();