Skip to content

Commit 461a0c6

Browse files
tniessenkumarak
authored andcommittedJan 7, 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#305 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent df1b2c3 commit 461a0c6

7 files changed

+85
-54
lines changed
 

Diff for: ‎doc/api/tls.md

+12
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,11 @@ decrease overall server throughput.
13331333
## `tls.checkServerIdentity(hostname, cert)`
13341334
<!-- YAML
13351335
added: v0.8.4
1336+
changes:
1337+
- version: REPLACEME
1338+
pr-url: https://github.com/nodejs-private/node-private/pull/300
1339+
description: Support for `uniformResourceIdentifier` subject alternative
1340+
names has been disabled in response to CVE-2021-44531.
13361341
-->
13371342

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

1361+
Earlier versions of Node.js incorrectly accepted certificates for a given
1362+
`hostname` if a matching `uniformResourceIdentifier` subject alternative name
1363+
was present (see [CVE-2021-44531][]). Applications that wish to accept
1364+
`uniformResourceIdentifier` subject alternative names can use a custom
1365+
`options.checkServerIdentity` function that implements the desired behavior.
1366+
13561367
## `tls.connect(options[, callback])`
13571368
<!-- YAML
13581369
added: v0.11.3
@@ -1997,6 +2008,7 @@ added: v11.4.0
19972008
`'TLSv1.3'`. If multiple of the options are provided, the lowest minimum is
19982009
used.
19992010

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

Diff for: ‎lib/tls.js

+4-29
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,9 @@ const { isArrayBufferView } = require('internal/util/types');
5050

5151
const net = require('net');
5252
const { getOptionValue } = require('internal/options');
53-
const url = require('url');
5453
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
5554
const { Buffer } = require('buffer');
5655
const EventEmitter = require('events');
57-
const { URL } = require('internal/url');
5856
const DuplexPair = require('internal/streams/duplexpair');
5957
const { canonicalizeIP } = internalBinding('cares_wrap');
6058
const _tls_common = require('_tls_common');
@@ -263,12 +261,10 @@ function splitEscapedAltNames(altNames) {
263261
return result;
264262
}
265263

266-
let urlWarningEmitted = false;
267264
exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
268265
const subject = cert.subject;
269266
const altNames = cert.subjectaltname;
270267
const dnsNames = [];
271-
const uriNames = [];
272268
const ips = [];
273269

274270
hostname = '' + hostname;
@@ -280,23 +276,6 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
280276
for (const name of splitAltNames) {
281277
if (name.startsWith('DNS:')) {
282278
dnsNames.push(name.slice(4));
283-
} else if (name.startsWith('URI:')) {
284-
let uri;
285-
try {
286-
uri = new URL(name.slice(4));
287-
} catch {
288-
uri = url.parse(name.slice(4));
289-
if (!urlWarningEmitted && !process.noDeprecation) {
290-
urlWarningEmitted = true;
291-
process.emitWarning(
292-
`The URI ${name.slice(4)} found in cert.subjectaltname ` +
293-
'is not a valid URI, and is supported in the tls module ' +
294-
'solely for compatibility.',
295-
'DeprecationWarning', 'DEP0109');
296-
}
297-
}
298-
299-
uriNames.push(uri.hostname); // TODO(bnoordhuis) Also use scheme.
300279
} else if (name.startsWith('IP Address:')) {
301280
ips.push(canonicalizeIP(name.slice(11)));
302281
}
@@ -306,23 +285,19 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
306285
let valid = false;
307286
let reason = 'Unknown reason';
308287

309-
const hasAltNames =
310-
dnsNames.length > 0 || ips.length > 0 || uriNames.length > 0;
311-
312288
hostname = unfqdn(hostname); // Remove trailing dot for error messages.
313289

314290
if (net.isIP(hostname)) {
315291
valid = ips.includes(canonicalizeIP(hostname));
316292
if (!valid)
317293
reason = `IP: ${hostname} is not in the cert's list: ${ips.join(', ')}`;
318294
// TODO(bnoordhuis) Also check URI SANs that are IP addresses.
319-
} else if (hasAltNames || subject) {
295+
} else if (dnsNames.length > 0 || (subject && subject.CN)) {
320296
const hostParts = splitHost(hostname);
321297
const wildcard = (pattern) => check(hostParts, pattern, true);
322298

323-
if (hasAltNames) {
324-
const noWildcard = (pattern) => check(hostParts, pattern, false);
325-
valid = dnsNames.some(wildcard) || uriNames.some(noWildcard);
299+
if (dnsNames.length > 0) {
300+
valid = dnsNames.some(wildcard);
326301
if (!valid)
327302
reason =
328303
`Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
@@ -339,7 +314,7 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
339314
reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
340315
}
341316
} else {
342-
reason = 'Cert is empty';
317+
reason = 'Cert does not contain a DNS name';
343318
}
344319

345320
if (!valid) {

Diff for: ‎test/fixtures/keys/Makefile

+14
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ all: \
7777
x448_public.pem \
7878
incorrect_san_correct_subject-cert.pem \
7979
incorrect_san_correct_subject-key.pem \
80+
irrelevant_san_correct_subject-cert.pem \
81+
irrelevant_san_correct_subject-key.pem \
8082

8183
#
8284
# Create Certificate Authority: ca1
@@ -747,6 +749,18 @@ incorrect_san_correct_subject-cert.pem: incorrect_san_correct_subject-key.pem
747749
incorrect_san_correct_subject-key.pem:
748750
openssl ecparam -name prime256v1 -genkey -noout -out incorrect_san_correct_subject-key.pem
749751

752+
irrelevant_san_correct_subject-cert.pem: irrelevant_san_correct_subject-key.pem
753+
openssl req -x509 \
754+
-key irrelevant_san_correct_subject-key.pem \
755+
-out irrelevant_san_correct_subject-cert.pem \
756+
-sha256 \
757+
-days 3650 \
758+
-subj "/CN=good.example.com" \
759+
-addext "subjectAltName = IP:1.2.3.4"
760+
761+
irrelevant_san_correct_subject-key.pem:
762+
openssl ecparam -name prime256v1 -genkey -noout -out irrelevant_san_correct_subject-key.pem
763+
750764
clean:
751765
rm -f *.pfx *.pem *.srl ca2-database.txt ca2-serial fake-startcom-root-serial *.print *.old fake-startcom-root-issued-certs/*.pem
752766
@> 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-----

Diff for: ‎test/parallel/test-tls-check-server-identity.js

+7-21
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,6 @@ const util = require('util');
3030

3131
const tls = require('tls');
3232

33-
common.expectWarning('DeprecationWarning', [
34-
['The URI http://[a.b.a.com]/ found in cert.subjectaltname ' +
35-
'is not a valid URI, and is supported in the tls module ' +
36-
'solely for compatibility.',
37-
'DEP0109'],
38-
]);
39-
4033
const tests = [
4134
// False-y values.
4235
{
@@ -141,7 +134,7 @@ const tests = [
141134
{
142135
host: 'a.com',
143136
cert: { },
144-
error: 'Cert is empty'
137+
error: 'Cert does not contain a DNS name'
145138
},
146139

147140
// Empty Subject w/DNS name
@@ -155,7 +148,8 @@ const tests = [
155148
{
156149
host: 'a.b.a.com', cert: {
157150
subjectaltname: 'URI:http://a.b.a.com/',
158-
}
151+
},
152+
error: 'Cert does not contain a DNS name'
159153
},
160154

161155
// Multiple CN fields
@@ -272,31 +266,23 @@ const tests = [
272266
host: 'a.b.a.com', cert: {
273267
subjectaltname: 'URI:http://a.b.a.com/',
274268
subject: {}
275-
}
269+
},
270+
error: 'Cert does not contain a DNS name'
276271
},
277272
{
278273
host: 'a.b.a.com', cert: {
279274
subjectaltname: 'URI:http://*.b.a.com/',
280275
subject: {}
281276
},
282-
error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' +
283-
'URI:http://*.b.a.com/'
284-
},
285-
// Invalid URI
286-
{
287-
host: 'a.b.a.com', cert: {
288-
subjectaltname: 'URI:http://[a.b.a.com]/',
289-
subject: {}
290-
}
277+
error: 'Cert does not contain a DNS name'
291278
},
292279
// IP addresses
293280
{
294281
host: 'a.b.a.com', cert: {
295282
subjectaltname: 'IP Address:127.0.0.1',
296283
subject: {}
297284
},
298-
error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' +
299-
'IP Address:127.0.0.1'
285+
error: 'Cert does not contain a DNS name'
300286
},
301287
{
302288
host: '127.0.0.1', cert: {

Diff for: ‎test/parallel/test-x509-escaping.js

+32-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ const { hasOpenSSL3 } = common;
1919
const numLeaves = 5;
2020

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

@@ -347,3 +343,35 @@ const { hasOpenSSL3 } = common;
347343
}));
348344
})).unref();
349345
}
346+
347+
// The subject MUST NOT be ignored if no dNSName subject alternative name
348+
// exists, even if other subject alternative names exist.
349+
{
350+
const key = fixtures.readKey('irrelevant_san_correct_subject-key.pem');
351+
const cert = fixtures.readKey('irrelevant_san_correct_subject-cert.pem');
352+
353+
// The hostname is the CN, but there is no dNSName SAN entry.
354+
const servername = 'good.example.com';
355+
356+
// X509Certificate interface is not supported in v12.x & v14.x. Disable
357+
// checks for certX509.subject and certX509.subjectAltName with expected
358+
// value. The testcase is ported from v17.x
359+
//
360+
// const certX509 = new X509Certificate(cert);
361+
// assert.strictEqual(certX509.subject, `CN=${servername}`);
362+
// assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4');
363+
364+
// Connect to a server that uses the self-signed certificate.
365+
const server = tls.createServer({ key, cert }, common.mustCall((socket) => {
366+
socket.destroy();
367+
server.close();
368+
})).listen(common.mustCall(() => {
369+
const { port } = server.address();
370+
tls.connect(port, {
371+
ca: cert,
372+
servername,
373+
}, common.mustCall(() => {
374+
// Do nothing, the server will close the connection.
375+
}));
376+
}));
377+
}

0 commit comments

Comments
 (0)
Please sign in to comment.