Skip to content

Commit 7da3223

Browse files
addaleaxtpoisseau
authored andcommitted
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: nodejs#36453 PR-URL: nodejs#54790 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent dec344d commit 7da3223

File tree

5 files changed

+103
-18
lines changed

5 files changed

+103
-18
lines changed

doc/api/tls.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,6 +1856,9 @@ argument.
18561856
<!-- YAML
18571857
added: v0.11.13
18581858
changes:
1859+
- version: REPLACEME
1860+
pr-url: https://github.com/nodejs/node/pull/54790
1861+
description: The `allowPartialTrustChain` option has been added.
18591862
- version:
18601863
- v22.4.0
18611864
- v20.16.0
@@ -1912,6 +1915,8 @@ changes:
19121915
-->
19131916

19141917
* `options` {Object}
1918+
* `allowPartialTrustChain` {boolean} Treat intermediate (non-self-signed)
1919+
certificates in the trust CA certificate list as trusted.
19151920
* `ca` {string|string\[]|Buffer|Buffer\[]} Optionally override the trusted CA
19161921
certificates. Default is to trust the well-known CAs curated by Mozilla.
19171922
Mozilla's CAs are completely replaced when CAs are explicitly specified

lib/internal/tls/secure-context.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
130130
validateObject(options, name);
131131

132132
const {
133+
allowPartialTrustChain,
133134
ca,
134135
cert,
135136
ciphers = getDefaultCiphers(),
@@ -182,6 +183,10 @@ function configSecureContext(context, options = kEmptyObject, name = 'options')
182183
context.addRootCerts();
183184
}
184185

186+
if (allowPartialTrustChain) {
187+
context.setAllowPartialTrustChain();
188+
}
189+
185190
if (cert) {
186191
setCerts(context, ArrayIsArray(cert) ? cert : [cert], `${name}.cert`);
187192
}

src/crypto/crypto_context.cc

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ Local<FunctionTemplate> SecureContext::GetConstructorTemplate(
314314
SetProtoMethod(isolate, tmpl, "setKey", SetKey);
315315
SetProtoMethod(isolate, tmpl, "setCert", SetCert);
316316
SetProtoMethod(isolate, tmpl, "addCACert", AddCACert);
317+
SetProtoMethod(
318+
isolate, tmpl, "setAllowPartialTrustChain", SetAllowPartialTrustChain);
317319
SetProtoMethod(isolate, tmpl, "addCRL", AddCRL);
318320
SetProtoMethod(isolate, tmpl, "addRootCerts", AddRootCerts);
319321
SetProtoMethod(isolate, tmpl, "setCipherSuites", SetCipherSuites);
@@ -390,6 +392,7 @@ void SecureContext::RegisterExternalReferences(
390392
registry->Register(AddCACert);
391393
registry->Register(AddCRL);
392394
registry->Register(AddRootCerts);
395+
registry->Register(SetAllowPartialTrustChain);
393396
registry->Register(SetCipherSuites);
394397
registry->Register(SetCiphers);
395398
registry->Register(SetSigalgs);
@@ -753,17 +756,39 @@ void SecureContext::SetCert(const FunctionCallbackInfo<Value>& args) {
753756
USE(sc->AddCert(env, std::move(bio)));
754757
}
755758

759+
// NOLINTNEXTLINE(runtime/int)
760+
void SecureContext::SetX509StoreFlag(unsigned long flags) {
761+
X509_STORE* cert_store = GetCertStoreOwnedByThisSecureContext();
762+
CHECK_EQ(1, X509_STORE_set_flags(cert_store, flags));
763+
}
764+
765+
X509_STORE* SecureContext::GetCertStoreOwnedByThisSecureContext() {
766+
if (own_cert_store_cache_ != nullptr) return own_cert_store_cache_;
767+
768+
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get());
769+
if (cert_store == GetOrCreateRootCertStore()) {
770+
cert_store = NewRootCertStore();
771+
SSL_CTX_set_cert_store(ctx_.get(), cert_store);
772+
}
773+
774+
return own_cert_store_cache_ = cert_store;
775+
}
776+
777+
void SecureContext::SetAllowPartialTrustChain(
778+
const FunctionCallbackInfo<Value>& args) {
779+
SecureContext* sc;
780+
ASSIGN_OR_RETURN_UNWRAP(&sc, args.This());
781+
sc->SetX509StoreFlag(X509_V_FLAG_PARTIAL_CHAIN);
782+
}
783+
756784
void SecureContext::SetCACert(const BIOPointer& bio) {
757785
ClearErrorOnReturn clear_error_on_return;
758786
if (!bio) return;
759-
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get());
760787
while (X509Pointer x509 = X509Pointer(PEM_read_bio_X509_AUX(
761788
bio.get(), nullptr, NoPasswordCallback, nullptr))) {
762-
if (cert_store == GetOrCreateRootCertStore()) {
763-
cert_store = NewRootCertStore();
764-
SSL_CTX_set_cert_store(ctx_.get(), cert_store);
765-
}
766-
CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get()));
789+
CHECK_EQ(1,
790+
X509_STORE_add_cert(GetCertStoreOwnedByThisSecureContext(),
791+
x509.get()));
767792
CHECK_EQ(1, SSL_CTX_add_client_CA(ctx_.get(), x509.get()));
768793
}
769794
}
@@ -793,11 +818,7 @@ Maybe<void> SecureContext::SetCRL(Environment* env, const BIOPointer& bio) {
793818
return Nothing<void>();
794819
}
795820

796-
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx_.get());
797-
if (cert_store == GetOrCreateRootCertStore()) {
798-
cert_store = NewRootCertStore();
799-
SSL_CTX_set_cert_store(ctx_.get(), cert_store);
800-
}
821+
X509_STORE* cert_store = GetCertStoreOwnedByThisSecureContext();
801822

802823
CHECK_EQ(1, X509_STORE_add_crl(cert_store, crl.get()));
803824
CHECK_EQ(1,
@@ -1080,8 +1101,6 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
10801101
sc->issuer_.reset();
10811102
sc->cert_.reset();
10821103

1083-
X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
1084-
10851104
DeleteFnPtr<PKCS12, PKCS12_free> p12;
10861105
EVPKeyPointer pkey;
10871106
X509Pointer cert;
@@ -1135,11 +1154,7 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
11351154
for (int i = 0; i < sk_X509_num(extra_certs.get()); i++) {
11361155
X509* ca = sk_X509_value(extra_certs.get(), i);
11371156

1138-
if (cert_store == GetOrCreateRootCertStore()) {
1139-
cert_store = NewRootCertStore();
1140-
SSL_CTX_set_cert_store(sc->ctx_.get(), cert_store);
1141-
}
1142-
X509_STORE_add_cert(cert_store, ca);
1157+
X509_STORE_add_cert(sc->GetCertStoreOwnedByThisSecureContext(), ca);
11431158
SSL_CTX_add_client_CA(sc->ctx_.get(), ca);
11441159
}
11451160
ret = true;

src/crypto/crypto_context.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class SecureContext final : public BaseObject {
6464
void SetCACert(const BIOPointer& bio);
6565
void SetRootCerts();
6666

67+
void SetX509StoreFlag(unsigned long flags); // NOLINT(runtime/int)
68+
X509_STORE* GetCertStoreOwnedByThisSecureContext();
69+
6770
// TODO(joyeecheung): track the memory used by OpenSSL types
6871
SET_NO_MEMORY_INFO()
6972
SET_MEMORY_INFO_NAME(SecureContext)
@@ -90,6 +93,8 @@ class SecureContext final : public BaseObject {
9093
#endif // !OPENSSL_NO_ENGINE
9194
static void SetCert(const v8::FunctionCallbackInfo<v8::Value>& args);
9295
static void AddCACert(const v8::FunctionCallbackInfo<v8::Value>& args);
96+
static void SetAllowPartialTrustChain(
97+
const v8::FunctionCallbackInfo<v8::Value>& args);
9398
static void AddCRL(const v8::FunctionCallbackInfo<v8::Value>& args);
9499
static void AddRootCerts(const v8::FunctionCallbackInfo<v8::Value>& args);
95100
static void SetCipherSuites(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -142,6 +147,8 @@ class SecureContext final : public BaseObject {
142147
SSLCtxPointer ctx_;
143148
X509Pointer cert_;
144149
X509Pointer issuer_;
150+
// Non-owning cache for SSL_CTX_get_cert_store(ctx_.get())
151+
X509_STORE* own_cert_store_cache_ = nullptr;
145152
#ifndef OPENSSL_NO_ENGINE
146153
bool client_cert_engine_provided_ = false;
147154
ncrypto::EnginePointer private_key_engine_;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto) { common.skip('missing crypto'); };
4+
5+
const assert = require('assert');
6+
const { once } = require('events');
7+
const fixtures = require('../common/fixtures');
8+
9+
// agent6-cert.pem is signed by intermediate cert of ca3.
10+
// The server has a cert chain of agent6->ca3->ca1(root).
11+
12+
const { it, beforeEach, afterEach, describe } = require('node:test');
13+
14+
describe('allowPartialTrustChain', { skip: !common.hasCrypto }, function() {
15+
const tls = require('tls');
16+
let server;
17+
let client;
18+
let opts;
19+
20+
beforeEach(async function() {
21+
server = tls.createServer({
22+
ca: fixtures.readKey('ca3-cert.pem'),
23+
key: fixtures.readKey('agent6-key.pem'),
24+
cert: fixtures.readKey('agent6-cert.pem'),
25+
}, (socket) => socket.resume());
26+
server.listen(0);
27+
await once(server, 'listening');
28+
29+
opts = {
30+
port: server.address().port,
31+
ca: fixtures.readKey('ca3-cert.pem'),
32+
checkServerIdentity() {}
33+
};
34+
});
35+
36+
afterEach(async function() {
37+
client?.destroy();
38+
server?.close();
39+
});
40+
41+
it('can connect successfully with allowPartialTrustChain: true', async function() {
42+
client = tls.connect({ ...opts, allowPartialTrustChain: true });
43+
await once(client, 'secureConnect'); // Should not throw
44+
});
45+
46+
it('fails without with allowPartialTrustChain: true for an intermediate cert in the CA', async function() {
47+
// Consistency check: Connecting fails without allowPartialTrustChain: true
48+
await assert.rejects(async () => {
49+
const client = tls.connect(opts);
50+
await once(client, 'secureConnect');
51+
}, { code: 'UNABLE_TO_GET_ISSUER_CERT' });
52+
});
53+
});

0 commit comments

Comments
 (0)