Skip to content

Commit a2cbfa9

Browse files
tniessenkumarak
authored andcommittedJan 10, 2022
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 <iit.akshay@gmail.com> CVE-ID: CVE-2021-44531 Backport-PR-URL: nodejs-private/node-private#304 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent e52882d commit a2cbfa9

7 files changed

+80
-28
lines changed
 

‎doc/api/tls.md

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

14541454
<!-- YAML
14551455
added: v0.8.4
1456+
changes:
1457+
- version: REPLACEME
1458+
pr-url: https://github.com/nodejs-private/node-private/pull/300
1459+
description: Support for `uniformResourceIdentifier` subject alternative
1460+
names has been disabled in response to CVE-2021-44531.
14561461
-->
14571462

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

1481+
Earlier versions of Node.js incorrectly accepted certificates for a given
1482+
`hostname` if a matching `uniformResourceIdentifier` subject alternative name
1483+
was present (see [CVE-2021-44531][]). Applications that wish to accept
1484+
`uniformResourceIdentifier` subject alternative names can use a custom
1485+
`options.checkServerIdentity` function that implements the desired behavior.
1486+
14761487
## `tls.connect(options[, callback])`
14771488

14781489
<!-- YAML
@@ -2134,6 +2145,7 @@ added: v11.4.0
21342145
`'TLSv1.3'`. If multiple of the options are provided, the lowest minimum is
21352146
used.
21362147

2148+
[CVE-2021-44531]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44531
21372149
[Chrome's 'modern cryptography' setting]: https://www.chromium.org/Home/chromium-security/education/tls#TOC-Cipher-Suites
21382150
[DHE]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
21392151
[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)
Please sign in to comment.