diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc index 14cf5cc8c85d..fa95ad889c64 100644 --- a/src/crypto/crypto_tls.cc +++ b/src/crypto/crypto_tls.cc @@ -73,6 +73,36 @@ namespace crypto { namespace { +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1) +int VerifyCertChain(X509_STORE_CTX* ctx, void*) { + X509_VERIFY_PARAM* param = X509_STORE_CTX_get0_param(ctx); + const unsigned int flags = X509_VERIFY_PARAM_get_hostflags(param); + if (!(flags & (X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT | + X509_CHECK_FLAG_NEVER_CHECK_SUBJECT))) { + // tls.checkServerIdentity() falls back to the CN when no DNS SAN exists. + // OpenSSL 4.1 requires this flag to apply name constraints to that CN. + ncrypto::DeleteFnPtr names( + static_cast( + X509_get_ext_d2i(X509_STORE_CTX_get0_cert(ctx), + NID_subject_alt_name, + nullptr, + nullptr))); + bool has_dns_san = false; + for (int i = 0; names && i < sk_GENERAL_NAME_num(names.get()); i++) { + if (sk_GENERAL_NAME_value(names.get(), i)->type == GEN_DNS) { + has_dns_san = true; + break; + } + } + if (!has_dns_san) { + X509_VERIFY_PARAM_set_hostflags( + param, flags | X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT); + } + } + return X509_verify_cert(ctx) > 0 ? 1 : 0; +} +#endif + // Our custom implementation of the certificate verify callback // used when establishing a TLS handshake. Because we cannot perform // I/O quickly enough with X509_STORE_CTX_ APIs in this callback, @@ -330,6 +360,9 @@ int TLSExtStatusCallback(SSL* s, void* arg) { void ConfigureSecureContext(SecureContext* sc) { // OCSP stapling sc->ctx().setStatusCallback(TLSExtStatusCallback); +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 1) + SSL_CTX_set_cert_verify_callback(sc->ctx().get(), VerifyCertChain, nullptr); +#endif } inline bool Set( diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 0960e69f36fd..0aa11a80c5f8 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -1,4 +1,9 @@ all: \ + name-constraints-ca-cert.pem \ + name-constraints-permitted-cert.pem \ + name-constraints-excluded-cert.pem \ + name-constraints-dns-san-cert.pem \ + name-constraints-email-san-cert.pem \ ca1-cert.pem \ ca2-cert.pem \ ca2-crl.pem \ @@ -1243,6 +1248,30 @@ irrelevant_san_correct_subject-cert.pem: irrelevant_san_correct_subject-key.pem irrelevant_san_correct_subject-key.pem: openssl ecparam -name prime256v1 -genkey -noout -out irrelevant_san_correct_subject-key.pem +# Certificates for DNS name constraints with CN fallback. +# Reuse the existing ca1 and agent1 private keys. +name-constraints-ca-cert.pem: name-constraints.cnf ca1-key.pem + openssl req -new -x509 -sha256 -days 99999 -set_serial 1 \ + -config name-constraints.cnf -key ca1-key.pem \ + -passin "pass:password" -out $@ + +name-constraints-permitted-cert.pem: CN = www.example.com +name-constraints-permitted-cert.pem: SERIAL = 2 +name-constraints-excluded-cert.pem: SERIAL = 3 +name-constraints-dns-san-cert.pem: SERIAL = 4 +name-constraints-email-san-cert.pem: SERIAL = 5 +name-constraints-%-cert.pem: CN = outside.invalid +name-constraints-%-cert.pem: name-constraints.cnf name-constraints-ca-cert.pem ca1-key.pem agent1-key.pem + openssl req -new -config name-constraints.cnf -key agent1-key.pem \ + -subj "/CN=$(CN)/emailAddress=subject@example.com" \ + -out name-constraints-$*-csr.pem + openssl x509 -req -sha256 -days 99999 -set_serial $(SERIAL) \ + -in name-constraints-$*-csr.pem \ + -CA name-constraints-ca-cert.pem -CAkey ca1-key.pem \ + -passin "pass:password" -extfile name-constraints.cnf \ + -extensions $* -out $@ + rm name-constraints-$*-csr.pem + clean: 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 @> fake-startcom-root-database.txt diff --git a/test/fixtures/keys/name-constraints-ca-cert.pem b/test/fixtures/keys/name-constraints-ca-cert.pem new file mode 100644 index 000000000000..baae473c3676 --- /dev/null +++ b/test/fixtures/keys/name-constraints-ca-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCAf6gAwIBAgIBATANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl +LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y +MzAwMDYyNTEwMTkyNlowKzEpMCcGA1UEAwwgTm9kZS5qcyBuYW1lIGNvbnN0cmFp +bnRzIHRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNvf4O +GGep+ak+4DNjbuNgy0S/AZPxahEFp4gpbcvsi9YLOPZ31qpilQeQf7d27scIZ02Q +x1YBAzljxELB8H/ZxuYScQK0s+DNP22xhmgwMWznO7TezkHP5ujN2UkbfbUpfUxG +FgncXeZf9wR7yFWppeHiRWNBOgsvY7sTrS12kXjWGjqntF7xcEDHc7h+KyF6ZjVJ +ZJCnP6pJEQ+rUjd51eCZXt4WjowLnQiCS1VKzXiP83a++Ma1BKKkUitTR112/Uwd +5eGoiByhmLzb/BhxnHJN07GXjhlMItZRm/jfbZsx1mwnNOO3tx4r08l+DaqkinIa +dvazs+1ugCaKQn8xAgMBAAGjQzBBMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMB4GA1UdHgEB/wQUMBKgEDAOggwuZXhhbXBsZS5jb20wDQYJKoZIhvcN +AQELBQADggEBAAJqIezf3QGn3IAiOgF8Psr5BlzRrz/9pQ7cGhm9T34+S+s9oaux +ooPWXF94Mp7zcv93kohhfP5KPRT5PDvMVkv420sQg2ATdkDV62pQwidsTSb2Uj0/ +gQZwPDq5UAQE+uzQccP4fnEG7EEp6BdBm4SNU/6jnMdjFdOXX8l/DqR5nSC0ljiu +dFHyq0yL0hI51NLIfqVqfr5A7DAIbJhBu/TdAnuQaax0L8UVD0wXujxflwnXTxgh +SCoU+KnfbbWwJYrCE8DpmK9FKZYcVCb6xMQnEhC2P4tojqLYi31BBlTxSGcD4BQn +CEsF+KZ22uz8szGnCTFKmlGP4OEQlgsKqfk= +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/name-constraints-dns-san-cert.pem b/test/fixtures/keys/name-constraints-dns-san-cert.pem new file mode 100644 index 000000000000..fe1bea091ef0 --- /dev/null +++ b/test/fixtures/keys/name-constraints-dns-san-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNzCCAh+gAwIBAgIBBDANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl +LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y +MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPb3V0c2lkZS5pbnZhbGlkMSIwIAYJ +KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn +M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP +hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL +pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6 +eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra +dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABo1EwTzAMBgNVHRMB +Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNV +HREEEzARgg93d3cuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAKo9BvhK +WAx2dVStsX9V7tOEZu3vM3hNC8SvfolX0Bl+9F+hycElC3OQmM8Fgt1MegmRMT0R +dqoknu2MaiT4D+qX2i1mP2BhM9Zd0avA2OGhodMykNkMzqyDHyZWtCNmsvVB/708 +PWGFoTMgKLfdYf6Kjifsyxt1hu1N3iI2XZOxR2aLJ/1xMFMOrsb/FWFpsumD+5SN +H2Fa5Ps/h3swXz+A/vzahnAHvbcARLjsbAYlJ+stUjgK6Mj21mOIkIiko2HHipz+ +JjpI+dPARD/cvq4FyTWsmmFLe7IZUNPu8GTQaC2dNYUkiQWTUNZTZySwnYcF9DsL +3GNOJY+wfEMMA1U= +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/name-constraints-email-san-cert.pem b/test/fixtures/keys/name-constraints-email-san-cert.pem new file mode 100644 index 000000000000..3812f271fb55 --- /dev/null +++ b/test/fixtures/keys/name-constraints-email-san-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNzCCAh+gAwIBAgIBBTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl +LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y +MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPb3V0c2lkZS5pbnZhbGlkMSIwIAYJ +KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn +M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP +hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL +pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6 +eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra +dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABo1EwTzAMBgNVHRMB +Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNV +HREEEzARgQ9zYW5AZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBABE6i1MH +8iQadmytlAExVgMkX85fDrS5YAQPjp37VfMkE0zlrsJ4hnNIz4EAIEgGBWUydPfO +xj//IBo0uHVzBT/tZKoL0/VrKrslj6tzGzDmaVjsjfReyEN+J1D0UAifBlJVY1sR +7C6zOtXjwX1UseScOqxtBtSJJMAMWmMMtKNbZ2mOzlmWrsqVSTFLI/aB/pdKsDol +sKOh9aKVLTYHQewZ3Na2bNIZgX0JiFRDoymz5ngJr1Kb6LuYj/h44svFMoNBRhpC +K9kVqdIAs5DPlANViVahHRNOp2oz0yWfatwau8gF6e0rdF3Zl1mjO2RbizfDlS4s +ceupch60bWYcsXA= +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/name-constraints-excluded-cert.pem b/test/fixtures/keys/name-constraints-excluded-cert.pem new file mode 100644 index 000000000000..491bb520bb5d --- /dev/null +++ b/test/fixtures/keys/name-constraints-excluded-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIBAzANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl +LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y +MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPb3V0c2lkZS5pbnZhbGlkMSIwIAYJ +KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn +M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP +hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL +pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6 +eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra +dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABozUwMzAMBgNVHRMB +Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkq +hkiG9w0BAQsFAAOCAQEAkkWCYQN4DrdqF1V3nkbpjgSu0rUlokj8pHlzOfKQpQAk +2bvGSIpnXlyK0+uYdgJDIH7feVhunVOWo2ySqxaQnjH05SFOlfSEyVqZBrpbDMiy +F5ulKs8fY2jS9o7nCiqHyog+oZWhQupTTTe120tmu8xesNK4T/bm3RiNEiPMVXYL +KS38VOZZE4A+REG9Pyd5zjJUOsOqngf8mEvKEmSsMh+6jEoGwJQGv4RYtCY3ZYA2 +waIIw/0pw9pfncRTdcDgJBAe5jeDTjDZ7APNJSqA2kmMUNv8qcLqFbAWzTuK/dvW +7tE3Pa17FiTmcrVxZSc9StruPWgTZ7w8Fd1g6+v+Xg== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/name-constraints-permitted-cert.pem b/test/fixtures/keys/name-constraints-permitted-cert.pem new file mode 100644 index 000000000000..5efa832cdf2e --- /dev/null +++ b/test/fixtures/keys/name-constraints-permitted-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIBAjANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb2Rl +LmpzIG5hbWUgY29uc3RyYWludHMgdGVzdCBDQTAgFw0yNjA5MTAxMDE5MjZaGA8y +MzAwMDYyNTEwMTkyNlowPjEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMSIwIAYJ +KoZIhvcNAQkBFhNzdWJqZWN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA1FYyCvsg04Jwk9wsQoTtBN+6vVbh3a5Snii3kM1CVtsn +M0nzc1/9M3x6Y2Psylont/c9xwialsbYhtsMYjiPHN1qljr81ZnVgA5YehH5CJYP +hO1QuiWigwPs2m5oT757rtyc6IATJ7FpevJQl87j8XXkAJhMDbao64e+A7TPlHdL +pW//yMY8aNat62CrvmmnsUq2prnnuqibWtq46weJfAf21Po9Zg3/V0EH0o6PY0Z6 +eIYkxXQZdpPpWc6hNi/64buhDIwNiIQKv+8QNjGy6PXDm1VIp+pX6KOfiSkYE/Ra +dsRIAzorfthAP0uqFHzzXi0lVKplzklpV5cJW/TcawIDAQABozUwMzAMBgNVHRMB +Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkq +hkiG9w0BAQsFAAOCAQEAU9FR2kM66P1+k/RwQdzgeawPvxREJ36Q+JBdY0FQ50uW +ToFyUmCpm0Ts4eM5v5J2kfWVRgPlvj/oq1SVJ105d/CyJP3XDt/nd6aKgLUdVDCy +/AfbniwOqpmslij8MdKhwA2aFo828N/ecVKTBdX/mNZoA9/dhdu6ty1Q7DZzUv3e +hkF2XcWzTIEUUjxVPXUT6H1WjVgT/ViOjbqln30GmKSxgEG6qXbGLAwIQVX03UvI +Ta5uc4Jl9LYETu0iz9CVxhkzvS9ISXApBJD31sDuWBXZjmDQ1/Ky0Gd0WGogzYpz +LnEqspax4sylH8b+/nlYLbdiBgRNxkc5PzaZTYD/AA== +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/name-constraints.cnf b/test/fixtures/keys/name-constraints.cnf new file mode 100644 index 000000000000..1588ce437ac5 --- /dev/null +++ b/test/fixtures/keys/name-constraints.cnf @@ -0,0 +1,34 @@ +[ req ] +distinguished_name = req_distinguished_name +prompt = no +x509_extensions = ca + +[ req_distinguished_name ] +CN = Node.js name constraints test CA + +[ ca ] +basicConstraints = critical,CA:TRUE +keyUsage = critical,keyCertSign,cRLSign +nameConstraints = critical,permitted;DNS:.example.com + +[ permitted ] +basicConstraints = critical,CA:FALSE +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth + +[ excluded ] +basicConstraints = critical,CA:FALSE +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth + +[ dns-san ] +basicConstraints = critical,CA:FALSE +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = DNS:www.example.com + +[ email-san ] +basicConstraints = critical,CA:FALSE +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = email:san@example.com diff --git a/test/parallel/test-tls-name-constraints-cn.js b/test/parallel/test-tls-name-constraints-cn.js new file mode 100644 index 000000000000..0aae9502eeb0 --- /dev/null +++ b/test/parallel/test-tls-name-constraints-cn.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); + +const { isBoringSSL } = require('../common/crypto'); +// BoringSSL rejects DNS-like CNs without SANs under name constraints, +// even when the CN is permitted by those constraints. +if (isBoringSSL) common.skip('requires OpenSSL CN name constraints'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const ca = fixtures.readKey('name-constraints-ca-cert.pem'); +const key = fixtures.readKey('agent1-key.pem'); + +// A DNS SAN prevents CN fallback; an email SAN does not. Name constraints +// must cover the CN whenever hostname verification can use it. +for (const version of ['TLSv1.2', 'TLSv1.3']) { + for (const [name, servername, valid] of [ + ['permitted', 'www.example.com', true], + ['excluded', 'outside.invalid', false], + ['dns-san', 'www.example.com', true], + ['email-san', 'outside.invalid', false], + ]) { + for (const rejectUnauthorized of [true, false]) { + const rejected = !valid && rejectUnauthorized; + const server = tls.createServer({ + key, + cert: fixtures.readKey(`name-constraints-${name}-cert.pem`), + minVersion: version, + maxVersion: version, + }, (socket) => socket.end()); + server.on('tlsClientError', () => {}); + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + ca, + servername, + rejectUnauthorized, + }); + client.on('secureConnect', rejected ? common.mustNotCall() : common.mustCall(() => { + assert.strictEqual(client.authorized, valid); + if (!valid) assert.strictEqual(client.authorizationError, 'UNSPECIFIED'); + client.end(); + })); + client.on('error', rejected ? common.mustCall((err) => { + assert.strictEqual(err.code, 'UNSPECIFIED'); + assert.match(err.message, /permitted subtree violation/); + }) : common.mustNotCall()); + client.on('close', common.mustCall(() => server.close())); + })); + } + } +}