Skip to content

Commit b11b4cc

Browse files
tniessenkumarak
authored andcommitted
tls: drop support for URI alternative names
Previously, Node.js incorrectly accepted uniformResourceIdentifier (URI) subject alternative names in checkServerIdentity regardless of the application protocol. This was incorrect even in the most common cases. For example, RFC 2818 specifies (and RFC 6125 confirms) that HTTP over TLS only uses dNSName and iPAddress subject alternative names, but not uniformResourceIdentifier subject alternative names. Additionally, name constrained certificate authorities might not be constrained to specific URIs, allowing them to issue certificates for URIs that specify hosts that they would not be allowed to issue dNSName certificates for. Even for application protocols that make use of URI subject alternative names (such as SIP, see RFC 5922), Node.js did not implement the required checks correctly, for example, because checkServerIdentity ignores the URI scheme. As a side effect, this also fixes an edge case. When a hostname is not an IP address and no dNSName subject alternative name exists, the subject's Common Name should be considered even when an iPAddress subject alternative name exists. It remains possible for users to pass a custom checkServerIdentity function to the TLS implementation in order to implement custom identity verification logic. This addresses CVE-2021-44531. Co-authored-by: Akshay K <[email protected]> CVE-ID: CVE-2021-44531 Backport-PR-URL: nodejs-private/node-private#303 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 2e2c455 commit b11b4cc

7 files changed

+80
-28
lines changed

doc/api/tls.md

+12
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,11 @@ decrease overall server throughput.
14601460

14611461
<!-- YAML
14621462
added: v0.8.4
1463+
changes:
1464+
- version: REPLACEME
1465+
pr-url: https://github.com/nodejs-private/node-private/pull/300
1466+
description: Support for `uniformResourceIdentifier` subject alternative
1467+
names has been disabled in response to CVE-2021-44531.
14631468
-->
14641469

14651470
* `hostname` {string} The host name or IP address to verify the certificate
@@ -1480,6 +1485,12 @@ the checks done with additional verification.
14801485
This function is only called if the certificate passed all other checks, such as
14811486
being issued by trusted CA (`options.ca`).
14821487

1488+
Earlier versions of Node.js incorrectly accepted certificates for a given
1489+
`hostname` if a matching `uniformResourceIdentifier` subject alternative name
1490+
was present (see [CVE-2021-44531][]). Applications that wish to accept
1491+
`uniformResourceIdentifier` subject alternative names can use a custom
1492+
`options.checkServerIdentity` function that implements the desired behavior.
1493+
14831494
## `tls.connect(options[, callback])`
14841495

14851496
<!-- YAML
@@ -2143,6 +2154,7 @@ added: v11.4.0
21432154
`'TLSv1.3'`. If multiple of the options are provided, the lowest minimum is
21442155
used.
21452156

2157+
[CVE-2021-44531]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44531
21462158
[Chrome's 'modern cryptography' setting]: https://www.chromium.org/Home/chromium-security/education/tls#TOC-Cipher-Suites
21472159
[DHE]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
21482160
[ECDHE]: https://en.wikipedia.org/wiki/Elliptic_curve_Diffie%E2%80%93Hellman

lib/tls.js

+4-17
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const net = require('net');
6060
const { getOptionValue } = require('internal/options');
6161
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
6262
const { Buffer } = require('buffer');
63-
const { URL } = require('internal/url');
6463
const { canonicalizeIP } = internalBinding('cares_wrap');
6564
const _tls_common = require('_tls_common');
6665
const _tls_wrap = require('_tls_wrap');
@@ -275,7 +274,6 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
275274
const subject = cert.subject;
276275
const altNames = cert.subjectaltname;
277276
const dnsNames = [];
278-
const uriNames = [];
279277
const ips = [];
280278

281279
hostname = '' + hostname;
@@ -287,11 +285,6 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
287285
ArrayPrototypeForEach(splitAltNames, (name) => {
288286
if (StringPrototypeStartsWith(name, 'DNS:')) {
289287
ArrayPrototypePush(dnsNames, StringPrototypeSlice(name, 4));
290-
} else if (StringPrototypeStartsWith(name, 'URI:')) {
291-
const uri = new URL(StringPrototypeSlice(name, 4));
292-
293-
// TODO(bnoordhuis) Also use scheme.
294-
ArrayPrototypePush(uriNames, uri.hostname);
295288
} else if (StringPrototypeStartsWith(name, 'IP Address:')) {
296289
ArrayPrototypePush(ips, canonicalizeIP(StringPrototypeSlice(name, 11)));
297290
}
@@ -301,25 +294,19 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
301294
let valid = false;
302295
let reason = 'Unknown reason';
303296

304-
const hasAltNames =
305-
dnsNames.length > 0 || ips.length > 0 || uriNames.length > 0;
306-
307297
hostname = unfqdn(hostname); // Remove trailing dot for error messages.
308298

309299
if (net.isIP(hostname)) {
310300
valid = ArrayPrototypeIncludes(ips, canonicalizeIP(hostname));
311301
if (!valid)
312302
reason = `IP: ${hostname} is not in the cert's list: ` +
313303
ArrayPrototypeJoin(ips, ', ');
314-
// TODO(bnoordhuis) Also check URI SANs that are IP addresses.
315-
} else if (hasAltNames || subject) {
304+
} else if (dnsNames.length > 0 || subject?.CN) {
316305
const hostParts = splitHost(hostname);
317306
const wildcard = (pattern) => check(hostParts, pattern, true);
318307

319-
if (hasAltNames) {
320-
const noWildcard = (pattern) => check(hostParts, pattern, false);
321-
valid = ArrayPrototypeSome(dnsNames, wildcard) ||
322-
ArrayPrototypeSome(uriNames, noWildcard);
308+
if (dnsNames.length > 0) {
309+
valid = ArrayPrototypeSome(dnsNames, wildcard);
323310
if (!valid)
324311
reason =
325312
`Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
@@ -336,7 +323,7 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
336323
reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
337324
}
338325
} else {
339-
reason = 'Cert is empty';
326+
reason = 'Cert does not contain a DNS name';
340327
}
341328

342329
if (!valid) {

test/fixtures/keys/Makefile

+14
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ all: \
8787
ec_secp256k1_public.pem \
8888
incorrect_san_correct_subject-cert.pem \
8989
incorrect_san_correct_subject-key.pem \
90+
irrelevant_san_correct_subject-cert.pem \
91+
irrelevant_san_correct_subject-key.pem \
9092

9193
#
9294
# Create Certificate Authority: ca1
@@ -795,6 +797,18 @@ incorrect_san_correct_subject-cert.pem: incorrect_san_correct_subject-key.pem
795797
incorrect_san_correct_subject-key.pem:
796798
openssl ecparam -name prime256v1 -genkey -noout -out incorrect_san_correct_subject-key.pem
797799

800+
irrelevant_san_correct_subject-cert.pem: irrelevant_san_correct_subject-key.pem
801+
openssl req -x509 \
802+
-key irrelevant_san_correct_subject-key.pem \
803+
-out irrelevant_san_correct_subject-cert.pem \
804+
-sha256 \
805+
-days 3650 \
806+
-subj "/CN=good.example.com" \
807+
-addext "subjectAltName = IP:1.2.3.4"
808+
809+
irrelevant_san_correct_subject-key.pem:
810+
openssl ecparam -name prime256v1 -genkey -noout -out irrelevant_san_correct_subject-key.pem
811+
798812
clean:
799813
rm -f *.pfx *.pem *.srl ca2-database.txt ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem
800814
@> fake-startcom-root-database.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBnTCCAUKgAwIBAgIUa28EJmmQ7yZOq3WWNP3SLiJnzcAwCgYIKoZIzj0EAwIw
3+
GzEZMBcGA1UEAwwQZ29vZC5leGFtcGxlLmNvbTAeFw0yMTEyMTExNzE0NDVaFw0z
4+
MTEyMDkxNzE0NDVaMBsxGTAXBgNVBAMMEGdvb2QuZXhhbXBsZS5jb20wWTATBgcq
5+
hkjOPQIBBggqhkjOPQMBBwNCAATEKoJfDvKQ6dD+yvc4DaeH0ZlG8VuGJUVi6iIb
6+
ugY3dKHdmXUIuwwUScgztLc6W8FfvbTxfTF2q90ZBJlr/Klvo2QwYjAdBgNVHQ4E
7+
FgQUu55oRZI5tdQDmViwAvPEbzZuY2owHwYDVR0jBBgwFoAUu55oRZI5tdQDmViw
8+
AvPEbzZuY2owDwYDVR0TAQH/BAUwAwEB/zAPBgNVHREECDAGhwQBAgMEMAoGCCqG
9+
SM49BAMCA0kAMEYCIQDw8z8d7ToB14yxMJxEDF1dhUqMReJFFwPVnvzkr174igIh
10+
AKJ9XL+02sGOE7xZd5C0KqUXeHoIE9shnejnhm3WBrB/
11+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MHcCAQEEIDsijdVlHMNTvJ4eqeUbpjMMnl72+HLtEIEcbauckCP6oAoGCCqGSM49
3+
AwEHoUQDQgAExCqCXw7ykOnQ/sr3OA2nh9GZRvFbhiVFYuoiG7oGN3Sh3Zl1CLsM
4+
FEnIM7S3OlvBX7208X0xdqvdGQSZa/ypbw==
5+
-----END EC PRIVATE KEY-----

test/parallel/test-tls-check-server-identity.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ const tests = [
134134
{
135135
host: 'a.com',
136136
cert: { },
137-
error: 'Cert is empty'
137+
error: 'Cert does not contain a DNS name'
138138
},
139139

140140
// Empty Subject w/DNS name
@@ -148,7 +148,8 @@ const tests = [
148148
{
149149
host: 'a.b.a.com', cert: {
150150
subjectaltname: 'URI:http://a.b.a.com/',
151-
}
151+
},
152+
error: 'Cert does not contain a DNS name'
152153
},
153154

154155
// Multiple CN fields
@@ -265,24 +266,23 @@ const tests = [
265266
host: 'a.b.a.com', cert: {
266267
subjectaltname: 'URI:http://a.b.a.com/',
267268
subject: {}
268-
}
269+
},
270+
error: 'Cert does not contain a DNS name'
269271
},
270272
{
271273
host: 'a.b.a.com', cert: {
272274
subjectaltname: 'URI:http://*.b.a.com/',
273275
subject: {}
274276
},
275-
error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' +
276-
'URI:http://*.b.a.com/'
277+
error: 'Cert does not contain a DNS name'
277278
},
278279
// IP addresses
279280
{
280281
host: 'a.b.a.com', cert: {
281282
subjectaltname: 'IP Address:127.0.0.1',
282283
subject: {}
283284
},
284-
error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' +
285-
'IP Address:127.0.0.1'
285+
error: 'Cert does not contain a DNS name'
286286
},
287287
{
288288
host: '127.0.0.1', cert: {

test/parallel/test-x509-escaping.js

+27-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ const { hasOpenSSL3 } = common;
2020
const numLeaves = 5;
2121

2222
for (let i = 0; i < numLeaves; i++) {
23-
// TODO(tniessen): this test case requires proper handling of URI SANs,
24-
// which node currently does not implement.
25-
if (i === 3) continue;
26-
2723
const name = `x509-escaping/google/leaf${i}.pem`;
2824
const leafPEM = fixtures.readSync(name, 'utf8');
2925

@@ -336,3 +332,30 @@ const { hasOpenSSL3 } = common;
336332
}));
337333
})).unref();
338334
}
335+
336+
// The subject MUST NOT be ignored if no dNSName subject alternative name
337+
// exists, even if other subject alternative names exist.
338+
{
339+
const key = fixtures.readKey('irrelevant_san_correct_subject-key.pem');
340+
const cert = fixtures.readKey('irrelevant_san_correct_subject-cert.pem');
341+
342+
// The hostname is the CN, but there is no dNSName SAN entry.
343+
const servername = 'good.example.com';
344+
const certX509 = new X509Certificate(cert);
345+
assert.strictEqual(certX509.subject, `CN=${servername}`);
346+
assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4');
347+
348+
// Connect to a server that uses the self-signed certificate.
349+
const server = tls.createServer({ key, cert }, common.mustCall((socket) => {
350+
socket.destroy();
351+
server.close();
352+
})).listen(common.mustCall(() => {
353+
const { port } = server.address();
354+
tls.connect(port, {
355+
ca: cert,
356+
servername,
357+
}, common.mustCall(() => {
358+
// Do nothing, the server will close the connection.
359+
}));
360+
}));
361+
}

0 commit comments

Comments
 (0)