Skip to content

Commit e0fe6a6

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 <iit.akshay@gmail.com> CVE-ID: CVE-2021-44531 Backport-PR-URL: nodejs-private/node-private#306 PR-URL: nodejs-private/node-private#300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 19873ab commit e0fe6a6

File tree

7 files changed

+85
-54
lines changed

7 files changed

+85
-54
lines changed

doc/api/tls.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,11 @@ decrease overall server throughput.
13231323
## `tls.checkServerIdentity(hostname, cert)`
13241324
<!-- YAML
13251325
added: v0.8.4
1326+
changes:
1327+
- version: REPLACEME
1328+
pr-url: https://github.com/nodejs-private/node-private/pull/300
1329+
description: Support for `uniformResourceIdentifier` subject alternative
1330+
names has been disabled in response to CVE-2021-44531.
13261331
-->
13271332

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

1351+
Earlier versions of Node.js incorrectly accepted certificates for a given
1352+
`hostname` if a matching `uniformResourceIdentifier` subject alternative name
1353+
was present (see [CVE-2021-44531][]). Applications that wish to accept
1354+
`uniformResourceIdentifier` subject alternative names can use a custom
1355+
`options.checkServerIdentity` function that implements the desired behavior.
1356+
13461357
## `tls.connect(options[, callback])`
13471358
<!-- YAML
13481359
added: v0.11.3
@@ -2003,6 +2014,7 @@ added: v11.4.0
20032014
[`tls.createServer()`]: #tls_tls_createserver_options_secureconnectionlistener
20042015
[`tls.getCiphers()`]: #tls_tls_getciphers
20052016
[`tls.rootCertificates`]: #tls_tls_rootcertificates
2017+
[CVE-2021-44531]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44531
20062018
[Chrome's 'modern cryptography' setting]: https://www.chromium.org/Home/chromium-security/education/tls#TOC-Cipher-Suites
20072019
[DHE]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
20082020
[ECDHE]: https://en.wikipedia.org/wiki/Elliptic_curve_Diffie%E2%80%93Hellman

lib/tls.js

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,9 @@ const { isArrayBufferView } = require('internal/util/types');
4747

4848
const net = require('net');
4949
const { getOptionValue } = require('internal/options');
50-
const url = require('url');
5150
const { getRootCertificates, getSSLCiphers } = internalBinding('crypto');
5251
const { Buffer } = require('buffer');
5352
const EventEmitter = require('events');
54-
const { URL } = require('internal/url');
5553
const DuplexPair = require('internal/streams/duplexpair');
5654
const { canonicalizeIP } = internalBinding('cares_wrap');
5755
const _tls_common = require('_tls_common');
@@ -254,12 +252,10 @@ function splitEscapedAltNames(altNames) {
254252
return result;
255253
}
256254

257-
let urlWarningEmitted = false;
258255
exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
259256
const subject = cert.subject;
260257
const altNames = cert.subjectaltname;
261258
const dnsNames = [];
262-
const uriNames = [];
263259
const ips = [];
264260

265261
hostname = '' + hostname;
@@ -271,23 +267,6 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
271267
for (const name of splitAltNames) {
272268
if (name.startsWith('DNS:')) {
273269
dnsNames.push(name.slice(4));
274-
} else if (name.startsWith('URI:')) {
275-
let uri;
276-
try {
277-
uri = new URL(name.slice(4));
278-
} catch {
279-
uri = url.parse(name.slice(4));
280-
if (!urlWarningEmitted && !process.noDeprecation) {
281-
urlWarningEmitted = true;
282-
process.emitWarning(
283-
`The URI ${name.slice(4)} found in cert.subjectaltname ` +
284-
'is not a valid URI, and is supported in the tls module ' +
285-
'solely for compatibility.',
286-
'DeprecationWarning', 'DEP0109');
287-
}
288-
}
289-
290-
uriNames.push(uri.hostname); // TODO(bnoordhuis) Also use scheme.
291270
} else if (name.startsWith('IP Address:')) {
292271
ips.push(canonicalizeIP(name.slice(11)));
293272
}
@@ -297,23 +276,19 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
297276
let valid = false;
298277
let reason = 'Unknown reason';
299278

300-
const hasAltNames =
301-
dnsNames.length > 0 || ips.length > 0 || uriNames.length > 0;
302-
303279
hostname = unfqdn(hostname); // Remove trailing dot for error messages.
304280

305281
if (net.isIP(hostname)) {
306282
valid = ips.includes(canonicalizeIP(hostname));
307283
if (!valid)
308284
reason = `IP: ${hostname} is not in the cert's list: ${ips.join(', ')}`;
309285
// TODO(bnoordhuis) Also check URI SANs that are IP addresses.
310-
} else if (hasAltNames || subject) {
286+
} else if (dnsNames.length > 0 || (subject && subject.CN)) {
311287
const hostParts = splitHost(hostname);
312288
const wildcard = (pattern) => check(hostParts, pattern, true);
313289

314-
if (hasAltNames) {
315-
const noWildcard = (pattern) => check(hostParts, pattern, false);
316-
valid = dnsNames.some(wildcard) || uriNames.some(noWildcard);
290+
if (dnsNames.length > 0) {
291+
valid = dnsNames.some(wildcard);
317292
if (!valid)
318293
reason =
319294
`Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
@@ -330,7 +305,7 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
330305
reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
331306
}
332307
} else {
333-
reason = 'Cert is empty';
308+
reason = 'Cert does not contain a DNS name';
334309
}
335310

336311
if (!valid) {

test/fixtures/keys/Makefile

Lines changed: 14 additions & 0 deletions
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
Lines changed: 11 additions & 0 deletions
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-----
Lines changed: 5 additions & 0 deletions
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

Lines changed: 7 additions & 21 deletions
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
{
@@ -140,7 +133,7 @@ const tests = [
140133
{
141134
host: 'a.com',
142135
cert: { },
143-
error: 'Cert is empty'
136+
error: 'Cert does not contain a DNS name'
144137
},
145138

146139
// Empty Subject w/DNS name
@@ -154,7 +147,8 @@ const tests = [
154147
{
155148
host: 'a.b.a.com', cert: {
156149
subjectaltname: 'URI:http://a.b.a.com/',
157-
}
150+
},
151+
error: 'Cert does not contain a DNS name'
158152
},
159153

160154
// Multiple CN fields
@@ -271,31 +265,23 @@ const tests = [
271265
host: 'a.b.a.com', cert: {
272266
subjectaltname: 'URI:http://a.b.a.com/',
273267
subject: {}
274-
}
268+
},
269+
error: 'Cert does not contain a DNS name'
275270
},
276271
{
277272
host: 'a.b.a.com', cert: {
278273
subjectaltname: 'URI:http://*.b.a.com/',
279274
subject: {}
280275
},
281-
error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' +
282-
'URI:http://*.b.a.com/'
283-
},
284-
// Invalid URI
285-
{
286-
host: 'a.b.a.com', cert: {
287-
subjectaltname: 'URI:http://[a.b.a.com]/',
288-
subject: {}
289-
}
276+
error: 'Cert does not contain a DNS name'
290277
},
291278
// IP addresses
292279
{
293280
host: 'a.b.a.com', cert: {
294281
subjectaltname: 'IP Address:127.0.0.1',
295282
subject: {}
296283
},
297-
error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' +
298-
'IP Address:127.0.0.1'
284+
error: 'Cert does not contain a DNS name'
299285
},
300286
{
301287
host: '127.0.0.1', cert: {

test/parallel/test-x509-escaping.js

Lines changed: 32 additions & 4 deletions
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)