Skip to content

Commit 684cae7

Browse files
panvaaduh95
authored andcommitted
tls: preserve CN name constraints with OpenSSL 4.1
OpenSSL no longer applies DNS name constraints to the subject CN by default, but tls.checkServerIdentity() still uses it without a DNS SAN. Enable subject checking during chain verification only when no DNS SAN exists, preserving the existing hostname verification policy. Assisted-by: Codex Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65957 Refs: https://openssl-library.org/post/2026-09-09-openssl-4.1-alpha/ Reviewed-By: Tim Perry <pimterry@gmail.com>
1 parent 2777803 commit 684cae7

9 files changed

Lines changed: 248 additions & 0 deletions

‎src/crypto/crypto_tls.cc‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,36 @@ namespace crypto {
7373

7474
namespace {
7575

76+
#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1)
77+
int VerifyCertChain(X509_STORE_CTX* ctx, void*) {
78+
X509_VERIFY_PARAM* param = X509_STORE_CTX_get0_param(ctx);
79+
const unsigned int flags = X509_VERIFY_PARAM_get_hostflags(param);
80+
if (!(flags & (X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT |
81+
X509_CHECK_FLAG_NEVER_CHECK_SUBJECT))) {
82+
// tls.checkServerIdentity() falls back to the CN when no DNS SAN exists.
83+
// OpenSSL 4.1 requires this flag to apply name constraints to that CN.
84+
ncrypto::DeleteFnPtr<GENERAL_NAMES, GENERAL_NAMES_free> names(
85+
static_cast<GENERAL_NAMES*>(
86+
X509_get_ext_d2i(X509_STORE_CTX_get0_cert(ctx),
87+
NID_subject_alt_name,
88+
nullptr,
89+
nullptr)));
90+
bool has_dns_san = false;
91+
for (int i = 0; names && i < sk_GENERAL_NAME_num(names.get()); i++) {
92+
if (sk_GENERAL_NAME_value(names.get(), i)->type == GEN_DNS) {
93+
has_dns_san = true;
94+
break;
95+
}
96+
}
97+
if (!has_dns_san) {
98+
X509_VERIFY_PARAM_set_hostflags(
99+
param, flags | X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
100+
}
101+
}
102+
return X509_verify_cert(ctx) > 0 ? 1 : 0;
103+
}
104+
#endif
105+
76106
// Our custom implementation of the certificate verify callback
77107
// used when establishing a TLS handshake. Because we cannot perform
78108
// I/O quickly enough with X509_STORE_CTX_ APIs in this callback,
@@ -330,6 +360,9 @@ int TLSExtStatusCallback(SSL* s, void* arg) {
330360
void ConfigureSecureContext(SecureContext* sc) {
331361
// OCSP stapling
332362
sc->ctx().setStatusCallback(TLSExtStatusCallback);
363+
#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1)
364+
SSL_CTX_set_cert_verify_callback(sc->ctx().get(), VerifyCertChain, nullptr);
365+
#endif
333366
}
334367

335368
inline bool Set(

‎test/fixtures/keys/Makefile‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
all: \
2+
name-constraints-ca-cert.pem \
3+
name-constraints-permitted-cert.pem \
4+
name-constraints-excluded-cert.pem \
5+
name-constraints-dns-san-cert.pem \
6+
name-constraints-email-san-cert.pem \
27
ca1-cert.pem \
38
ca2-cert.pem \
49
ca2-crl.pem \
@@ -1258,6 +1263,30 @@ irrelevant_san_correct_subject-cert.pem: irrelevant_san_correct_subject-key.pem
12581263
irrelevant_san_correct_subject-key.pem:
12591264
openssl ecparam -name prime256v1 -genkey -noout -out irrelevant_san_correct_subject-key.pem
12601265

1266+
# Certificates for DNS name constraints with CN fallback.
1267+
# Reuse the existing ca1 and agent1 private keys.
1268+
name-constraints-ca-cert.pem: name-constraints.cnf ca1-key.pem
1269+
openssl req -new -x509 -sha256 -days 99999 -set_serial 1 \
1270+
-config name-constraints.cnf -key ca1-key.pem \
1271+
-passin "pass:password" -out $@
1272+
1273+
name-constraints-permitted-cert.pem: CN = www.example.com
1274+
name-constraints-permitted-cert.pem: SERIAL = 2
1275+
name-constraints-excluded-cert.pem: SERIAL = 3
1276+
name-constraints-dns-san-cert.pem: SERIAL = 4
1277+
name-constraints-email-san-cert.pem: SERIAL = 5
1278+
name-constraints-%-cert.pem: CN = outside.invalid
1279+
name-constraints-%-cert.pem: name-constraints.cnf name-constraints-ca-cert.pem ca1-key.pem agent1-key.pem
1280+
openssl req -new -config name-constraints.cnf -key agent1-key.pem \
1281+
-subj "/CN=$(CN)/emailAddress=subject@example.com" \
1282+
-out name-constraints-$*-csr.pem
1283+
openssl x509 -req -sha256 -days 99999 -set_serial $(SERIAL) \
1284+
-in name-constraints-$*-csr.pem \
1285+
-CA name-constraints-ca-cert.pem -CAkey ca1-key.pem \
1286+
-passin "pass:password" -extfile name-constraints.cnf \
1287+
-extensions $* -out $@
1288+
rm name-constraints-$*-csr.pem
1289+
12611290
clean:
12621291
rm -f *.pfx *.pem *.srl ca2-database.txt ca2-crl-agent3-database.txt* ca2-crl-agent3.cnf ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem
12631292
@> fake-startcom-root-database.txt
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDFjCCAf6gAwIBAgIBATANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl
3+
LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y
4+
MzAwMDYyNTEwMTkyNlowKzEpMCcGA1UEAwwgTm9kZS5qcyBuYW1lIGNvbnN0cmFp
5+
bnRzIHRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNvf4O
6+
GGep+ak+4DNjbuNgy0S/AZPxahEFp4gpbcvsi9YLOPZ31qpilQeQf7d27scIZ02Q
7+
x1YBAzljxELB8H/ZxuYScQK0s+DNP22xhmgwMWznO7TezkHP5ujN2UkbfbUpfUxG
8+
FgncXeZf9wR7yFWppeHiRWNBOgsvY7sTrS12kXjWGjqntF7xcEDHc7h+KyF6ZjVJ
9+
ZJCnP6pJEQ+rUjd51eCZXt4WjowLnQiCS1VKzXiP83a++Ma1BKKkUitTR112/Uwd
10+
5eGoiByhmLzb/BhxnHJN07GXjhlMItZRm/jfbZsx1mwnNOO3tx4r08l+DaqkinIa
11+
dvazs+1ugCaKQn8xAgMBAAGjQzBBMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
12+
BAQDAgEGMB4GA1UdHgEB/wQUMBKgEDAOggwuZXhhbXBsZS5jb20wDQYJKoZIhvcN
13+
AQELBQADggEBAAJqIezf3QGn3IAiOgF8Psr5BlzRrz/9pQ7cGhm9T34+S+s9oaux
14+
ooPWXF94Mp7zcv93kohhfP5KPRT5PDvMVkv420sQg2ATdkDV62pQwidsTSb2Uj0/
15+
gQZwPDq5UAQE+uzQccP4fnEG7EEp6BdBm4SNU/6jnMdjFdOXX8l/DqR5nSC0ljiu
16+
dFHyq0yL0hI51NLIfqVqfr5A7DAIbJhBu/TdAnuQaax0L8UVD0wXujxflwnXTxgh
17+
SCoU+KnfbbWwJYrCE8DpmK9FKZYcVCb6xMQnEhC2P4tojqLYi31BBlTxSGcD4BQn
18+
CEsF+KZ22uz8szGnCTFKmlGP4OEQlgsKqfk=
19+
-----END CERTIFICATE-----
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDNzCCAh+gAwIBAgIBBDANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl
3+
LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y
4+
MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPb3V0c2lkZS5pbnZhbGlkMSIwIAYJ
5+
KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF
6+
AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn
7+
M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP
8+
hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL
9+
pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6
10+
eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra
11+
dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABo1EwTzAMBgNVHRMB
12+
Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNV
13+
HREEEzARgg93d3cuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAKo9BvhK
14+
WAx2dVStsX9V7tOEZu3vM3hNC8SvfolX0Bl+9F+hycElC3OQmM8Fgt1MegmRMT0R
15+
dqoknu2MaiT4D+qX2i1mP2BhM9Zd0avA2OGhodMykNkMzqyDHyZWtCNmsvVB/708
16+
PWGFoTMgKLfdYf6Kjifsyxt1hu1N3iI2XZOxR2aLJ/1xMFMOrsb/FWFpsumD+5SN
17+
H2Fa5Ps/h3swXz+A/vzahnAHvbcARLjsbAYlJ+stUjgK6Mj21mOIkIiko2HHipz+
18+
JjpI+dPARD/cvq4FyTWsmmFLe7IZUNPu8GTQaC2dNYUkiQWTUNZTZySwnYcF9DsL
19+
3GNOJY+wfEMMA1U=
20+
-----END CERTIFICATE-----
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDNzCCAh+gAwIBAgIBBTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl
3+
LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y
4+
MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPb3V0c2lkZS5pbnZhbGlkMSIwIAYJ
5+
KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF
6+
AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn
7+
M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP
8+
hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL
9+
pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6
10+
eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra
11+
dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABo1EwTzAMBgNVHRMB
12+
Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNV
13+
HREEEzARgQ9zYW5AZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBABE6i1MH
14+
8iQadmytlAExVgMkX85fDrS5YAQPjp37VfMkE0zlrsJ4hnNIz4EAIEgGBWUydPfO
15+
xj//IBo0uHVzBT/tZKoL0/VrKrslj6tzGzDmaVjsjfReyEN+J1D0UAifBlJVY1sR
16+
7C6zOtXjwX1UseScOqxtBtSJJMAMWmMMtKNbZ2mOzlmWrsqVSTFLI/aB/pdKsDol
17+
sKOh9aKVLTYHQewZ3Na2bNIZgX0JiFRDoymz5ngJr1Kb6LuYj/h44svFMoNBRhpC
18+
K9kVqdIAs5DPlANViVahHRNOp2oz0yWfatwau8gF6e0rdF3Zl1mjO2RbizfDlS4s
19+
ceupch60bWYcsXA=
20+
-----END CERTIFICATE-----
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDGzCCAgOgAwIBAgIBAzANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl
3+
LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y
4+
MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPb3V0c2lkZS5pbnZhbGlkMSIwIAYJ
5+
KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF
6+
AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn
7+
M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP
8+
hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL
9+
pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6
10+
eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra
11+
dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABozUwMzAMBgNVHRMB
12+
Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkq
13+
hkiG9w0BAQsFAAOCAQEAkkWCYQN4DrdqF1V3nkbpjgSu0rUlokj8pHlzOfKQpQAk
14+
2bvGSIpnXlyK0+uYdgJDIH7feVhunVOWo2ySqxaQnjH05SFOlfSEyVqZBrpbDMiy
15+
F5ulKs8fY2jS9o7nCiqHyog+oZWhQupTTTe120tmu8xesNK4T/bm3RiNEiPMVXYL
16+
KS38VOZZE4A+REG9Pyd5zjJUOsOqngf8mEvKEmSsMh+6jEoGwJQGv4RYtCY3ZYA2
17+
waIIw/0pw9pfncRTdcDgJBAe5jeDTjDZ7APNJSqA2kmMUNv8qcLqFbAWzTuK/dvW
18+
7tE3Pa17FiTmcrVxZSc9StruPWgTZ7w8Fd1g6+v+Xg==
19+
-----END CERTIFICATE-----
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDGzCCAgOgAwIBAgIBAjANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl
3+
LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y
4+
MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMSIwIAYJ
5+
KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF
6+
AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn
7+
M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP
8+
hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL
9+
pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6
10+
eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra
11+
dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABozUwMzAMBgNVHRMB
12+
Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkq
13+
hkiG9w0BAQsFAAOCAQEAU9FR2kM66P1+k/RwQdzgeawPvxREJ36Q+JBdY0FQ50uW
14+
ToFyUmCpm0Ts4eM5v5J2kfWVRgPlvj/oq1SVJ105d/CyJP3XDt/nd6aKgLUdVDCy
15+
/AfbniwOqpmslij8MdKhwA2aFo828N/ecVKTBdX/mNZoA9/dhdu6ty1Q7DZzUv3e
16+
hkF2XcWzTIEUUjxVPXUT6H1WjVgT/ViOjbqln30GmKSxgEG6qXbGLAwIQVX03UvI
17+
Ta5uc4Jl9LYETu0iz9CVxhkzvS9ISXApBJD31sDuWBXZjmDQ1/Ky0Gd0WGogzYpz
18+
LnEqspax4sylH8b+/nlYLbdiBgRNxkc5PzaZTYD/AA==
19+
-----END CERTIFICATE-----
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[ req ]
2+
distinguished_name = req_distinguished_name
3+
prompt = no
4+
x509_extensions = ca
5+
6+
[ req_distinguished_name ]
7+
CN = Node.js name constraints test CA
8+
9+
[ ca ]
10+
basicConstraints = critical,CA:TRUE
11+
keyUsage = critical,keyCertSign,cRLSign
12+
nameConstraints = critical,permitted;DNS:.example.com
13+
14+
[ permitted ]
15+
basicConstraints = critical,CA:FALSE
16+
keyUsage = critical,digitalSignature,keyEncipherment
17+
extendedKeyUsage = serverAuth
18+
19+
[ excluded ]
20+
basicConstraints = critical,CA:FALSE
21+
keyUsage = critical,digitalSignature,keyEncipherment
22+
extendedKeyUsage = serverAuth
23+
24+
[ dns-san ]
25+
basicConstraints = critical,CA:FALSE
26+
keyUsage = critical,digitalSignature,keyEncipherment
27+
extendedKeyUsage = serverAuth
28+
subjectAltName = DNS:www.example.com
29+
30+
[ email-san ]
31+
basicConstraints = critical,CA:FALSE
32+
keyUsage = critical,digitalSignature,keyEncipherment
33+
extendedKeyUsage = serverAuth
34+
subjectAltName = email:san@example.com
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto) common.skip('missing crypto');
4+
5+
const { isBoringSSL } = require('../common/crypto');
6+
// BoringSSL rejects DNS-like CNs without SANs under name constraints,
7+
// even when the CN is permitted by those constraints.
8+
if (isBoringSSL) common.skip('requires OpenSSL CN name constraints');
9+
10+
const assert = require('assert');
11+
const tls = require('tls');
12+
const fixtures = require('../common/fixtures');
13+
14+
const ca = fixtures.readKey('name-constraints-ca-cert.pem');
15+
const key = fixtures.readKey('agent1-key.pem');
16+
17+
// A DNS SAN prevents CN fallback; an email SAN does not. Name constraints
18+
// must cover the CN whenever hostname verification can use it.
19+
for (const version of ['TLSv1.2', 'TLSv1.3']) {
20+
for (const [name, servername, valid] of [
21+
['permitted', 'www.example.com', true],
22+
['excluded', 'outside.invalid', false],
23+
['dns-san', 'www.example.com', true],
24+
['email-san', 'outside.invalid', false],
25+
]) {
26+
for (const rejectUnauthorized of [true, false]) {
27+
const rejected = !valid && rejectUnauthorized;
28+
const server = tls.createServer({
29+
key,
30+
cert: fixtures.readKey(`name-constraints-${name}-cert.pem`),
31+
minVersion: version,
32+
maxVersion: version,
33+
}, (socket) => socket.end());
34+
server.on('tlsClientError', () => {});
35+
server.listen(0, common.mustCall(() => {
36+
const client = tls.connect({
37+
port: server.address().port,
38+
ca,
39+
servername,
40+
rejectUnauthorized,
41+
});
42+
client.on('secureConnect', rejected ? common.mustNotCall() : common.mustCall(() => {
43+
assert.strictEqual(client.authorized, valid);
44+
if (!valid) assert.strictEqual(client.authorizationError, 'UNSPECIFIED');
45+
client.end();
46+
}));
47+
client.on('error', rejected ? common.mustCall((err) => {
48+
assert.strictEqual(err.code, 'UNSPECIFIED');
49+
assert.match(err.message, /permitted subtree violation/);
50+
}) : common.mustNotCall());
51+
client.on('close', common.mustCall(() => server.close()));
52+
}));
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)