• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Flags: --security-revert=CVE-2021-44532
2'use strict';
3
4const common = require('../common');
5if (!common.hasCrypto)
6  common.skip('missing crypto');
7
8const assert = require('assert');
9const tls = require('tls');
10const fixtures = require('../common/fixtures');
11
12const { hasOpenSSL3 } = common;
13
14// Test escaping rules for subject alternative names.
15{
16  const expectedSANs = [
17    'DNS:good.example.com, DNS:evil.example.com',
18    // URIs should not require escaping.
19    'URI:http://example.com/',
20    'URI:http://example.com/?a=b&c=d',
21    // Unless they contain commas.
22    'URI:http://example.com/a,b',
23    // Percent encoding should not require escaping.
24    'URI:http://example.com/a%2Cb',
25    // Malicious attempts should be escaped.
26    'URI:http://example.com/a, DNS:good.example.com',
27    // Non-ASCII characters in DNS names should be treated as Latin-1.
28    'DNS:ex�mple.com',
29    // It should not be possible to cause unescaping without escaping.
30    'DNS:"evil.example.com"',
31    // IPv4 addresses should be represented as usual.
32    'IP Address:8.8.8.8',
33    'IP Address:8.8.4.4',
34    // For backward-compatibility, include invalid IP address lengths.
35    hasOpenSSL3 ? 'IP Address:<invalid length=5>' : 'IP Address:<invalid>',
36    hasOpenSSL3 ? 'IP Address:<invalid length=6>' : 'IP Address:<invalid>',
37    // IPv6 addresses are represented as OpenSSL does.
38    'IP Address:A0B:C0D:E0F:0:0:0:7A7B:7C7D',
39    // Regular email addresses don't require escaping.
40    'email:foo@example.com',
41    // ... but should be escaped if they contain commas.
42    'email:foo@example.com, DNS:good.example.com',
43    'DirName:/C=DE/L=Hannover',
44    // TODO(tniessen): support UTF8 in DirName
45    'DirName:/C=DE/L=M\\xC3\\xBCnchen',
46    'DirName:/C=DE/L=Berlin, DNS:good.example.com',
47    'DirName:/C=DE/L=Berlin, DNS:good.example.com\\x00' +
48      'evil.example.com',
49    'DirName:/C=DE/L=Berlin, DNS:good.example.com\\\\x00' +
50      'evil.example.com',
51    // These next two tests might be surprising. OpenSSL applies its own rules
52    // first, which introduce backslashes, which activate node's escaping.
53    // Unfortunately, there are also differences between OpenSSL 1.1.1 and 3.0.
54    'DirName:/C=DE/L=Berlin\\x0D\\x0A',
55    hasOpenSSL3 ?
56      'DirName:/C=DE/L=Berlin\\/CN=good.example.com' :
57      'DirName:/C=DE/L=Berlin/CN=good.example.com',
58    // TODO(tniessen): even OIDs that are well-known (such as the following,
59    // which is sha256WithRSAEncryption) should be represented numerically only.
60    'Registered ID:sha256WithRSAEncryption',
61    // This is an OID that will likely never be assigned to anything, thus
62    // OpenSSL hould not know it.
63    'Registered ID:1.3.9999.12.34',
64    hasOpenSSL3 ?
65      'othername: XmppAddr::abc123' :
66      'othername:<unsupported>',
67    hasOpenSSL3 ?
68      'othername: XmppAddr::abc123, DNS:good.example.com' :
69      'othername:<unsupported>',
70    hasOpenSSL3 ?
71      null :
72      'othername:<unsupported>',
73    // This is unsupported because the OID is not recognized.
74    hasOpenSSL3 ?
75      'othername: 1.3.9999.12.34::abc123' :
76      'othername:<unsupported>',
77    hasOpenSSL3 ? 'othername: SRVName::abc123' : 'othername:<unsupported>',
78    // This is unsupported because it is an SRVName with a UTF8String value,
79    // which is not allowed for SRVName.
80    hasOpenSSL3 ?
81      null : 'othername:<unsupported>',
82    hasOpenSSL3 ?
83      null :
84      'othername:<unsupported>',
85  ];
86
87  const serverKey = fixtures.readSync('x509-escaping/server-key.pem', 'utf8');
88
89  for (let i = 0; i < expectedSANs.length; i++) {
90    const pem = fixtures.readSync(`x509-escaping/alt-${i}-cert.pem`, 'utf8');
91
92    // X509Certificate interface is not supported in v12.x & v14.x. Disable
93    // checks for subjectAltName with expectedSANs. The testcase is ported
94    // from v17.x
95    //
96    // Test the subjectAltName property of the X509Certificate API.
97    // const cert = new X509Certificate(pem);
98    // assert.strictEqual(cert.subjectAltName, expectedSANs[i]);
99
100    // Test that the certificate obtained by checkServerIdentity has the correct
101    // subjectaltname property.
102    const server = tls.createServer({
103      key: serverKey,
104      cert: pem,
105    }, common.mustCall((conn) => {
106      conn.destroy();
107      server.close();
108    })).listen(common.mustCall(() => {
109      const { port } = server.address();
110      tls.connect(port, {
111        ca: pem,
112        servername: 'example.com',
113        checkServerIdentity: (hostname, peerCert) => {
114          assert.strictEqual(hostname, 'example.com');
115          assert.strictEqual(peerCert.subjectaltname, expectedSANs[i]);
116        },
117      }, common.mustCall());
118    }));
119  }
120}
121
122// Test escaping rules for authority info access.
123{
124  const expectedInfoAccess = [
125    {
126      text: 'OCSP - URI:http://good.example.com/\n' +
127            'OCSP - URI:http://evil.example.com/',
128      legacy: {
129        'OCSP - URI': [
130          'http://good.example.com/',
131          'http://evil.example.com/',
132        ],
133      },
134    },
135    {
136      text: 'CA Issuers - URI:http://ca.example.com/\n' +
137            'OCSP - URI:http://evil.example.com\n' +
138            'OCSP - DNS:good.example.com\n' +
139            'OCSP - URI:http://ca.nodejs.org/ca.cert',
140      legacy: {
141        'CA Issuers - URI': [
142          'http://ca.example.com/',
143        ],
144        'OCSP - DNS': [
145          'good.example.com',
146        ],
147        'OCSP - URI': [
148          'http://evil.example.com',
149          'http://ca.nodejs.org/ca.cert',
150        ],
151      },
152    },
153    {
154      text: '1.3.9999.12.34 - URI:http://ca.example.com/',
155      legacy: {
156        '1.3.9999.12.34 - URI': [
157          'http://ca.example.com/',
158        ],
159      },
160    },
161    hasOpenSSL3 ? {
162      text: 'OCSP - othername: XmppAddr::good.example.com\n' +
163            'OCSP - othername: 1.3.9999.12.34::abc123\n' +
164            'OCSP - othername: SRVName::abc123',
165      legacy: {
166        'OCSP - othername': [
167          ' XmppAddr::good.example.com',
168          ' 1.3.9999.12.34::abc123',
169          ' SRVName::abc123',
170        ],
171      },
172    } : {
173      text: 'OCSP - othername:<unsupported>\n' +
174            'OCSP - othername:<unsupported>\n' +
175            'OCSP - othername:<unsupported>',
176      legacy: {
177        'OCSP - othername': [
178          '<unsupported>',
179          '<unsupported>',
180          '<unsupported>',
181        ],
182      },
183    },
184    hasOpenSSL3 ? {
185      text: null,
186      legacy: null,
187    } : {
188      text: 'OCSP - othername:<unsupported>',
189      legacy: {
190        'OCSP - othername': [
191          '<unsupported>',
192        ]
193      },
194    },
195  ];
196
197  const serverKey = fixtures.readSync('x509-escaping/server-key.pem', 'utf8');
198
199  for (let i = 0; i < expectedInfoAccess.length; i++) {
200    const pem = fixtures.readSync(`x509-escaping/info-${i}-cert.pem`, 'utf8');
201    const expected = expectedInfoAccess[i];
202
203    // X509Certificate interface is not supported in v12.x & v14.x. Disable
204    // checks for cert.infoAccess with expected text. The testcase is ported
205    // from v17.x
206    //
207    // Test the subjectAltName property of the X509Certificate API.
208    // const cert = new X509Certificate(pem);
209    // assert.strictEqual(cert.infoAccess, expected.text ?
210    //  `${expected.text}${hasOpenSSL3 ? '' : '\n'}` :
211    //  expected.text);
212
213    // Test that the certificate obtained by checkServerIdentity has the correct
214    // subjectaltname property.
215    const server = tls.createServer({
216      key: serverKey,
217      cert: pem,
218    }, common.mustCall((conn) => {
219      conn.destroy();
220      server.close();
221    })).listen(common.mustCall(() => {
222      const { port } = server.address();
223      tls.connect(port, {
224        ca: pem,
225        servername: 'example.com',
226        checkServerIdentity: (hostname, peerCert) => {
227          assert.strictEqual(hostname, 'example.com');
228          assert.deepStrictEqual(peerCert.infoAccess,
229                                 expected.legacy ?
230                                   Object.assign(Object.create(null),
231                                                 expected.legacy) :
232                                   expected.legacy);
233        },
234      }, common.mustCall());
235    }));
236  }
237}
238
239// The internal parsing logic must match the JSON specification exactly.
240{
241  // This list is partially based on V8's own JSON tests.
242  const invalidJSON = [
243    '"\\a invalid escape"',
244    '"\\v invalid escape"',
245    '"\\\' invalid escape"',
246    '"\\x42 invalid escape"',
247    '"\\u202 invalid escape"',
248    '"\\012 invalid escape"',
249    '"Unterminated string',
250    '"Unterminated string\\"',
251    '"Unterminated string\\\\\\"',
252    '"\u0000 control character"',
253    '"\u001e control character"',
254    '"\u001f control character"',
255  ];
256
257  for (const invalidStringLiteral of invalidJSON) {
258    // Usually, checkServerIdentity returns an error upon verification failure.
259    // In this case, however, it should throw an error since this is not a
260    // verification error. Node.js itself will never produce invalid JSON string
261    // literals, so this can only happen when users construct invalid subject
262    // alternative name strings (that do not follow escaping rules).
263    assert.throws(() => {
264      tls.checkServerIdentity('example.com', {
265        subjectaltname: `DNS:${invalidStringLiteral}`,
266      });
267    }, {
268      code: 'ERR_TLS_CERT_ALTNAME_FORMAT',
269      message: 'Invalid subject alternative name string'
270    });
271  }
272}
273
274// While node does not produce commas within SAN entries, it should parse them
275// correctly (i.e., not simply split at commas).
276{
277  // Regardless of the quotes, splitting this SAN string at commas would
278  // cause checkServerIdentity to see 'DNS:b.example.com' and thus to accept
279  // the certificate for b.example.com.
280  const san = 'DNS:"a.example.com, DNS:b.example.com, DNS:c.example.com"';
281
282  // This is what node used to do, and which is not correct!
283  const hostname = 'b.example.com';
284  assert.strictEqual(san.split(', ')[1], `DNS:${hostname}`);
285
286  // The new implementation should parse the string correctly.
287  const err = tls.checkServerIdentity(hostname, { subjectaltname: san });
288  assert(err);
289  assert.strictEqual(err.code, 'ERR_TLS_CERT_ALTNAME_INVALID');
290  assert.strictEqual(err.message, 'Hostname/IP does not match certificate\'s ' +
291                                  'altnames: Host: b.example.com. is not in ' +
292                                  'the cert\'s altnames: DNS:"a.example.com, ' +
293                                  'DNS:b.example.com, DNS:c.example.com"');
294}
295
296// The subject MUST be ignored if a dNSName subject alternative name exists.
297{
298  const key = fixtures.readKey('incorrect_san_correct_subject-key.pem');
299  const cert = fixtures.readKey('incorrect_san_correct_subject-cert.pem');
300
301  // The hostname is the CN, but not a SAN entry.
302  const servername = 'good.example.com';
303
304  // X509Certificate interface is not supported in v12.x & v14.x. Disable
305  // checks for certX509.subject and certX509.subjectAltName with expected
306  // value. The testcase is ported from v17.x
307  //
308  // const certX509 = new X509Certificate(cert);
309  // assert.strictEqual(certX509.subject, `CN=${servername}`);
310  // assert.strictEqual(certX509.subjectAltName, 'DNS:evil.example.com');
311
312  // Try connecting to a server that uses the self-signed certificate.
313  const server = tls.createServer({ key, cert }, common.mustNotCall());
314  server.listen(common.mustCall(() => {
315    const { port } = server.address();
316    const socket = tls.connect(port, {
317      ca: cert,
318      servername,
319    }, common.mustNotCall());
320    socket.on('error', common.mustCall((err) => {
321      assert.strictEqual(err.code, 'ERR_TLS_CERT_ALTNAME_INVALID');
322      assert.strictEqual(err.message, 'Hostname/IP does not match ' +
323                                      "certificate's altnames: Host: " +
324                                      "good.example.com. is not in the cert's" +
325                                      ' altnames: DNS:evil.example.com');
326    }));
327  })).unref();
328}
329
330// The subject MUST NOT be ignored if no dNSName subject alternative name
331// exists, even if other subject alternative names exist.
332{
333  const key = fixtures.readKey('irrelevant_san_correct_subject-key.pem');
334  const cert = fixtures.readKey('irrelevant_san_correct_subject-cert.pem');
335
336  // The hostname is the CN, but there is no dNSName SAN entry.
337  const servername = 'good.example.com';
338
339  // X509Certificate interface is not supported in v12.x & v14.x. Disable
340  // checks for certX509.subject and certX509.subjectAltName with expected
341  // value. The testcase is ported from v17.x
342  //
343  // const certX509 = new X509Certificate(cert);
344  // assert.strictEqual(certX509.subject, `CN=${servername}`);
345  // assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4');
346
347  // Connect to a server that uses the self-signed certificate.
348  const server = tls.createServer({ key, cert }, common.mustCall((socket) => {
349    socket.destroy();
350    server.close();
351  })).listen(common.mustCall(() => {
352    const { port } = server.address();
353    tls.connect(port, {
354      ca: cert,
355      servername,
356    }, common.mustCall(() => {
357      // Do nothing, the server will close the connection.
358    }));
359  }));
360}
361