mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
tls: add allowPartialTrustChain
flag
This commit exposes the `X509_V_FLAG_PARTIAL_CHAIN` OpenSSL flag to users. This is behavior that has been requested repeatedly in the Github issues, and allows aligning behavior with other TLS libraries and commonly used applications (e.g. `curl`). As a drive-by, simplify the `SecureContext` source by deduplicating call sites at which a new custom certificate store was created for the `secureContext` in question. Fixes: https://github.com/nodejs/node/issues/36453 PR-URL: https://github.com/nodejs/node/pull/54790 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
parent
2ef4b15604
commit
c3a7b29e56
@ -1856,6 +1856,9 @@ argument.
|
||||
<!-- YAML
|
||||
added: v0.11.13
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/54790
|
||||
description: The `allowPartialTrustChain` option has been added.
|
||||
- version:
|
||||
- v22.4.0
|
||||
- v20.16.0
|
||||
@ -1912,6 +1915,8 @@ changes:
|
||||
-->
|
||||
|
||||
* `options` {Object}
|
||||
* `allowPartialTrustChain` {boolean} Treat intermediate (non-self-signed)
|
||||
certificates in the trust CA certificate list as trusted.
|
||||
* `ca` {string|string\[]|Buffer|Buffer\[]} Optionally override the trusted CA
|
||||
certificates. Default is to trust the well-known CAs curated by Mozilla.
|
||||
Mozilla's CAs are completely replaced when CAs are explicitly specified
|
||||
|
@ -130,6 +130,7 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
|
||||
validateObject(options, name);
|
||||
|
||||
const {
|
||||
allowPartialTrustChain,
|
||||
ca,
|
||||
cert,
|
||||
ciphers = getDefaultCiphers(),
|
||||
@ -182,6 +183,10 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
|
||||
context.addRootCerts();
|
||||
}
|
||||
|
||||
if (allowPartialTrustChain) {
|
||||
context.setAllowPartialTrustChain();
|
||||
}
|
||||
|
||||
if (cert) {
|
||||
setCerts(context, ArrayIsArray(cert) ? cert : [cert], `${name}.cert`);
|
||||
}
|
||||
|
@ -314,6 +314,8 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate(
|
||||
SetProtoMethod(isolate, tmpl, "setKey", SetKey);
|
||||
SetProtoMethod(isolate, tmpl, "setCert", SetCert);
|
||||
SetProtoMethod(isolate, tmpl, "addCACert", AddCACert);
|
||||
SetProtoMethod(
|
||||
isolate, tmpl, "setAllowPartialTrustChain", SetAllowPartialTrustChain);
|
||||
SetProtoMethod(isolate, tmpl, "addCRL", AddCRL);
|
||||
SetProtoMethod(isolate, tmpl, "addRootCerts", AddRootCerts);
|
||||
SetProtoMethod(isolate, tmpl, "setCipherSuites", SetCipherSuites);
|
||||
@ -390,6 +392,7 @@ void SecureContext::RegisterExternalReferences(
|
||||
registry->Register(AddCACert);
|
||||
registry->Register(AddCRL);
|
||||
registry->Register(AddRootCerts);
|
||||
registry->Register(SetAllowPartialTrustChain);
|
||||
registry->Register(SetCipherSuites);
|
||||
registry->Register(SetCiphers);
|
||||
registry->Register(SetSigalgs);
|
||||
@ -753,17 +756,39 @@ void SecureContext::SetCert(const FunctionCallbackInfo<Value>& args) {
|
||||
USE(sc->AddCert(env, std::move(bio)));
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(runtime/int)
|
||||
void SecureContext::SetX509StoreFlag(unsigned long flags) {
|
||||
X509_STORE* cert_store = GetCertStoreOwnedByThisSecureContext();
|
||||
CHECK_EQ(1, X509_STORE_set_flags(cert_store, flags));
|
||||
}
|
||||
|
||||
X509_STORE* SecureContext::GetCertStoreOwnedByThisSecureContext() {
|
||||
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()) {
|
||||
cert_store = NewRootCertStore();
|
||||
SSL_CTX_set_cert_store(ctx_.get(), cert_store);
|
||||
}
|
||||
|
||||
return own_cert_store_cache_ = cert_store;
|
||||
}
|
||||
|
||||
void SecureContext::SetAllowPartialTrustChain(
|
||||
const FunctionCallbackInfo<Value>& args) {
|
||||
SecureContext* sc;
|
||||
ASSIGN_OR_RETURN_UNWRAP(&sc, args.This());
|
||||
sc->SetX509StoreFlag(X509_V_FLAG_PARTIAL_CHAIN);
|
||||
}
|
||||
|
||||
void SecureContext::SetCACert(const BIOPointer& bio) {
|
||||
ClearErrorOnReturn clear_error_on_return;
|
||||
if (!bio) return;
|
||||
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get());
|
||||
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX(
|
||||
bio.get(), nullptr, NoPasswordCallback, nullptr))) {
|
||||
if (cert_store == GetOrCreateRootCertStore()) {
|
||||
cert_store = NewRootCertStore();
|
||||
SSL_CTX_set_cert_store(ctx_.get(), cert_store);
|
||||
}
|
||||
CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get()));
|
||||
CHECK_EQ(1,
|
||||
X509_STORE_add_cert(GetCertStoreOwnedByThisSecureContext(),
|
||||
x509.get()));
|
||||
CHECK_EQ(1, SSL_CTX_add_client_CA(ctx_.get(), x509.get()));
|
||||
}
|
||||
}
|
||||
@ -793,11 +818,7 @@ Maybe<void> SecureContext::SetCRL(Environment* env, const BIOPointer& bio) {
|
||||
return Nothing<void>();
|
||||
}
|
||||
|
||||
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get());
|
||||
if (cert_store == GetOrCreateRootCertStore()) {
|
||||
cert_store = NewRootCertStore();
|
||||
SSL_CTX_set_cert_store(ctx_.get(), cert_store);
|
||||
}
|
||||
X509_STORE* cert_store = GetCertStoreOwnedByThisSecureContext();
|
||||
|
||||
CHECK_EQ(1, X509_STORE_add_crl(cert_store, crl.get()));
|
||||
CHECK_EQ(1,
|
||||
@ -1080,8 +1101,6 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
|
||||
sc->issuer_.reset();
|
||||
sc->cert_.reset();
|
||||
|
||||
X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
|
||||
|
||||
DeleteFnPtr<PKCS12, PKCS12_free> p12;
|
||||
EVPKeyPointer pkey;
|
||||
X509Pointer cert;
|
||||
@ -1135,11 +1154,7 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
|
||||
for (int i = 0; i < sk_X509_num(extra_certs.get()); i++) {
|
||||
X509* ca = sk_X509_value(extra_certs.get(), i);
|
||||
|
||||
if (cert_store == GetOrCreateRootCertStore()) {
|
||||
cert_store = NewRootCertStore();
|
||||
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
|
||||
}
|
||||
X509_STORE_add_cert(cert_store, ca);
|
||||
X509_STORE_add_cert(sc->GetCertStoreOwnedByThisSecureContext(), ca);
|
||||
SSL_CTX_add_client_CA(sc->ctx_.get(), ca);
|
||||
}
|
||||
ret = true;
|
||||
|
@ -64,6 +64,9 @@ class SecureContext final : public BaseObject {
|
||||
void SetCACert(const BIOPointer& bio);
|
||||
void SetRootCerts();
|
||||
|
||||
void SetX509StoreFlag(unsigned long flags); // NOLINT(runtime/int)
|
||||
X509_STORE* GetCertStoreOwnedByThisSecureContext();
|
||||
|
||||
// TODO(joyeecheung): track the memory used by OpenSSL types
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(SecureContext)
|
||||
@ -90,6 +93,8 @@ class SecureContext final : public BaseObject {
|
||||
#endif // !OPENSSL_NO_ENGINE
|
||||
static void SetCert(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void AddCACert(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void SetAllowPartialTrustChain(
|
||||
const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void AddCRL(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void AddRootCerts(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
static void SetCipherSuites(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
@ -142,6 +147,8 @@ class SecureContext final : public BaseObject {
|
||||
SSLCtxPointer ctx_;
|
||||
X509Pointer cert_;
|
||||
X509Pointer issuer_;
|
||||
// Non-owning cache for SSL_CTX_get_cert_store(ctx_.get())
|
||||
X509_STORE* own_cert_store_cache_ = nullptr;
|
||||
#ifndef OPENSSL_NO_ENGINE
|
||||
bool client_cert_engine_provided_ = false;
|
||||
ncrypto::EnginePointer private_key_engine_;
|
||||
|
53
test/parallel/test-tls-client-allow-partial-trust-chain.js
Normal file
53
test/parallel/test-tls-client-allow-partial-trust-chain.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) { common.skip('missing crypto'); };
|
||||
|
||||
const assert = require('assert');
|
||||
const { once } = require('events');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
// agent6-cert.pem is signed by intermediate cert of ca3.
|
||||
// The server has a cert chain of agent6->ca3->ca1(root).
|
||||
|
||||
const { it, beforeEach, afterEach, describe } = require('node:test');
|
||||
|
||||
describe('allowPartialTrustChain', { skip: !common.hasCrypto }, function() {
|
||||
const tls = require('tls');
|
||||
let server;
|
||||
let client;
|
||||
let opts;
|
||||
|
||||
beforeEach(async function() {
|
||||
server = tls.createServer({
|
||||
ca: fixtures.readKey('ca3-cert.pem'),
|
||||
key: fixtures.readKey('agent6-key.pem'),
|
||||
cert: fixtures.readKey('agent6-cert.pem'),
|
||||
}, (socket) => socket.resume());
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
opts = {
|
||||
port: server.address().port,
|
||||
ca: fixtures.readKey('ca3-cert.pem'),
|
||||
checkServerIdentity() {}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
client?.destroy();
|
||||
server?.close();
|
||||
});
|
||||
|
||||
it('can connect successfully with allowPartialTrustChain: true', async function() {
|
||||
client = tls.connect({ ...opts, allowPartialTrustChain: true });
|
||||
await once(client, 'secureConnect'); // Should not throw
|
||||
});
|
||||
|
||||
it('fails without with allowPartialTrustChain: true for an intermediate cert in the CA', async function() {
|
||||
// Consistency check: Connecting fails without allowPartialTrustChain: true
|
||||
await assert.rejects(async () => {
|
||||
const client = tls.connect(opts);
|
||||
await once(client, 'secureConnect');
|
||||
}, { code: 'UNABLE_TO_GET_ISSUER_CERT' });
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user