• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const common = require('../common');
4if (!common.hasCrypto)
5  common.skip('missing crypto');
6
7const assert = require('assert');
8const tls = require('tls');
9const fixtures = require('../common/fixtures');
10
11const { hasOpenSSL3 } = common;
12
13// Test that all certificate chains provided by the reporter are rejected.
14{
15  const rootPEM = fixtures.readSync('x509-escaping/google/root.pem');
16  const intermPEM = fixtures.readSync('x509-escaping/google/intermediate.pem');
17  const keyPEM = fixtures.readSync('x509-escaping/google/key.pem');
18
19  const numLeaves = 5;
20
21  for (let i = 0; i < numLeaves; i++) {
22    const name = `x509-escaping/google/leaf${i}.pem`;
23    const leafPEM = fixtures.readSync(name, 'utf8');
24
25    const server = tls.createServer({
26      key: keyPEM,
27      cert: leafPEM + intermPEM,
28    }, common.mustNotCall()).listen(common.mustCall(() => {
29      const { port } = server.address();
30      const socket = tls.connect(port, {
31        ca: rootPEM,
32        servername: 'nodejs.org',
33      }, common.mustNotCall());
34      socket.on('error', common.mustCall());
35    })).unref();
36  }
37}
38
39// Test escaping rules for subject alternative names.
40{
41  const expectedSANs = [
42    'DNS:"good.example.com\\u002c DNS:evil.example.com"',
43    // URIs should not require escaping.
44    'URI:http://example.com/',
45    'URI:http://example.com/?a=b&c=d',
46    // Unless they contain commas.
47    'URI:"http://example.com/a\\u002cb"',
48    // Percent encoding should not require escaping.
49    'URI:http://example.com/a%2Cb',
50    // Malicious attempts should be escaped.
51    'URI:"http://example.com/a\\u002c DNS:good.example.com"',
52    // Non-ASCII characters in DNS names should be treated as Latin-1.
53    'DNS:"ex\\u00e4mple.com"',
54    // It should not be possible to cause unescaping without escaping.
55    'DNS:"\\"evil.example.com\\""',
56    // IPv4 addresses should be represented as usual.
57    'IP Address:8.8.8.8',
58    'IP Address:8.8.4.4',
59    // For backward-compatibility, include invalid IP address lengths.
60    hasOpenSSL3 ? 'IP Address:<invalid length=5>' : 'IP Address:<invalid>',
61    hasOpenSSL3 ? 'IP Address:<invalid length=6>' : 'IP Address:<invalid>',
62    // IPv6 addresses are represented as OpenSSL does.
63    'IP Address:A0B:C0D:E0F:0:0:0:7A7B:7C7D',
64    // Regular email addresses don't require escaping.
65    'email:foo@example.com',
66    // ... but should be escaped if they contain commas.
67    'email:"foo@example.com\\u002c DNS:good.example.com"',
68    'DirName:/C=DE/L=Hannover',
69    // TODO(tniessen): support UTF8 in DirName
70    'DirName:"/C=DE/L=M\\\\xC3\\\\xBCnchen"',
71    'DirName:"/C=DE/L=Berlin\\u002c DNS:good.example.com"',
72    'DirName:"/C=DE/L=Berlin\\u002c DNS:good.example.com\\\\x00' +
73      'evil.example.com"',
74    'DirName:"/C=DE/L=Berlin\\u002c DNS:good.example.com\\\\\\\\x00' +
75      'evil.example.com"',
76    // These next two tests might be surprising. OpenSSL applies its own rules
77    // first, which introduce backslashes, which activate node's escaping.
78    // Unfortunately, there are also differences between OpenSSL 1.1.1 and 3.0.
79    'DirName:"/C=DE/L=Berlin\\\\x0D\\\\x0A"',
80    hasOpenSSL3 ?
81      'DirName:"/C=DE/L=Berlin\\\\/CN=good.example.com"' :
82      'DirName:/C=DE/L=Berlin/CN=good.example.com',
83    // TODO(tniessen): even OIDs that are well-known (such as the following,
84    // which is sha256WithRSAEncryption) should be represented numerically only.
85    'Registered ID:sha256WithRSAEncryption',
86    // This is an OID that will likely never be assigned to anything, thus
87    // OpenSSL should not know it.
88    'Registered ID:1.3.9999.12.34',
89    hasOpenSSL3 ?
90      'othername: XmppAddr::abc123' :
91      'othername:<unsupported>',
92    hasOpenSSL3 ?
93      'othername:" XmppAddr::abc123\\u002c DNS:good.example.com"' :
94      'othername:<unsupported>',
95    hasOpenSSL3 ?
96      'othername:" XmppAddr::good.example.com\\u0000abc123"' :
97      'othername:<unsupported>',
98    // This is unsupported because the OID is not recognized.
99    'othername:<unsupported>',
100    hasOpenSSL3 ? 'othername: SRVName::abc123' : 'othername:<unsupported>',
101    // This is unsupported because it is an SRVName with a UTF8String value,
102    // which is not allowed for SRVName.
103    'othername:<unsupported>',
104    hasOpenSSL3 ?
105      'othername:" SRVName::abc\\u0000def"' :
106      'othername:<unsupported>',
107  ];
108
109  const serverKey = fixtures.readSync('x509-escaping/server-key.pem', 'utf8');
110
111  for (let i = 0; i < expectedSANs.length; i++) {
112    const pem = fixtures.readSync(`x509-escaping/alt-${i}-cert.pem`, 'utf8');
113
114    // X509Certificate interface is not supported in v12.x & v14.x. Disable
115    // checks for subjectAltName with expectedSANs. The testcase is ported
116    // from v17.x
117    //
118    // Test the subjectAltName property of the X509Certificate API.
119    // const cert = new X509Certificate(pem);
120    // assert.strictEqual(cert.subjectAltName, expectedSANs[i]);
121
122    // Test that the certificate obtained by checkServerIdentity has the correct
123    // subjectaltname property.
124    const server = tls.createServer({
125      key: serverKey,
126      cert: pem,
127    }, common.mustCall((conn) => {
128      conn.destroy();
129      server.close();
130    })).listen(common.mustCall(() => {
131      const { port } = server.address();
132      tls.connect(port, {
133        ca: pem,
134        servername: 'example.com',
135        checkServerIdentity: (hostname, peerCert) => {
136          assert.strictEqual(hostname, 'example.com');
137          assert.strictEqual(peerCert.subjectaltname, expectedSANs[i]);
138        },
139      }, common.mustCall());
140    }));
141  }
142}
143
144// Test escaping rules for authority info access.
145{
146  const expectedInfoAccess = [
147    {
148      text: 'OCSP - URI:"http://good.example.com/\\u000a' +
149            'OCSP - URI:http://evil.example.com/"',
150      legacy: {
151        'OCSP - URI': [
152          'http://good.example.com/\nOCSP - URI:http://evil.example.com/',
153        ],
154      },
155    },
156    {
157      text: 'CA Issuers - URI:"http://ca.example.com/\\u000a' +
158            'OCSP - URI:http://evil.example.com"\n' +
159            'OCSP - DNS:"good.example.com\\u000a' +
160            'OCSP - URI:http://ca.nodejs.org/ca.cert"',
161      legacy: {
162        'CA Issuers - URI': [
163          'http://ca.example.com/\nOCSP - URI:http://evil.example.com',
164        ],
165        'OCSP - DNS': [
166          'good.example.com\nOCSP - URI:http://ca.nodejs.org/ca.cert',
167        ],
168      },
169    },
170    {
171      text: '1.3.9999.12.34 - URI:http://ca.example.com/',
172      legacy: {
173        '1.3.9999.12.34 - URI': [
174          'http://ca.example.com/',
175        ],
176      },
177    },
178    hasOpenSSL3 ? {
179      text: 'OCSP - othername: XmppAddr::good.example.com\n' +
180            'OCSP - othername:<unsupported>\n' +
181            'OCSP - othername: SRVName::abc123',
182      legacy: {
183        'OCSP - othername': [
184          ' XmppAddr::good.example.com',
185          '<unsupported>',
186          ' SRVName::abc123',
187        ],
188      },
189    } : {
190      text: 'OCSP - othername:<unsupported>\n' +
191            'OCSP - othername:<unsupported>\n' +
192            'OCSP - othername:<unsupported>',
193      legacy: {
194        'OCSP - othername': [
195          '<unsupported>',
196          '<unsupported>',
197          '<unsupported>',
198        ],
199      },
200    },
201    hasOpenSSL3 ? {
202      text: 'OCSP - othername:" XmppAddr::good.example.com\\u0000abc123"',
203      legacy: {
204        'OCSP - othername': [
205          ' XmppAddr::good.example.com\0abc123',
206        ],
207      },
208    } : {
209      text: 'OCSP - othername:<unsupported>',
210      legacy: {
211        'OCSP - othername': [
212          '<unsupported>',
213        ],
214      },
215    },
216  ];
217
218  const serverKey = fixtures.readSync('x509-escaping/server-key.pem', 'utf8');
219
220  for (let i = 0; i < expectedInfoAccess.length; i++) {
221    const pem = fixtures.readSync(`x509-escaping/info-${i}-cert.pem`, 'utf8');
222    const expected = expectedInfoAccess[i];
223
224    // X509Certificate interface is not supported in v12.x & v14.x. Disable
225    // checks for cert.infoAccess with expected text. The testcase is ported
226    // from v17.x
227    // Test the subjectAltName property of the X509Certificate API.
228    // const cert = new X509Certificate(pem);
229    // assert.strictEqual(cert.infoAccess,
230    //                   `${expected.text}${hasOpenSSL3 ? '' : '\n'}`);
231
232    // Test that the certificate obtained by checkServerIdentity has the correct
233    // subjectaltname property.
234    const server = tls.createServer({
235      key: serverKey,
236      cert: pem,
237    }, common.mustCall((conn) => {
238      conn.destroy();
239      server.close();
240    })).listen(common.mustCall(() => {
241      const { port } = server.address();
242      tls.connect(port, {
243        ca: pem,
244        servername: 'example.com',
245        checkServerIdentity: (hostname, peerCert) => {
246          assert.strictEqual(hostname, 'example.com');
247          assert.deepStrictEqual(peerCert.infoAccess,
248                                 Object.assign(Object.create(null),
249                                               expected.legacy));
250        },
251      }, common.mustCall());
252    }));
253  }
254}
255
256// Test escaping rules for the subject field.
257{
258  const expectedSubjects = [
259    {
260      text: 'L=Somewhere\nCN=evil.example.com',
261      legacy: {
262        L: 'Somewhere',
263        CN: 'evil.example.com',
264      },
265    },
266    {
267      text: 'L=Somewhere\\00evil.example.com',
268      legacy: {
269        L: 'Somewhere\0evil.example.com',
270      },
271    },
272    {
273      text: 'L=Somewhere\\0ACN=evil.example.com',
274      legacy: {
275        L: 'Somewhere\nCN=evil.example.com'
276      },
277    },
278    {
279      text: 'L=Somewhere\\, CN = evil.example.com',
280      legacy: {
281        L: 'Somewhere, CN = evil.example.com'
282      },
283    },
284    {
285      text: 'L=Somewhere/CN=evil.example.com',
286      legacy: {
287        L: 'Somewhere/CN=evil.example.com'
288      },
289    },
290    {
291      text: 'L=München\\\\\\0ACN=evil.example.com',
292      legacy: {
293        L: 'München\\\nCN=evil.example.com'
294      }
295    },
296    {
297      text: 'L=Somewhere + CN=evil.example.com',
298      legacy: {
299        L: 'Somewhere',
300        CN: 'evil.example.com',
301      }
302    },
303    {
304      text: 'L=Somewhere \\+ CN=evil.example.com',
305      legacy: {
306        L: 'Somewhere + CN=evil.example.com'
307      }
308    },
309    // Observe that the legacy representation cannot properly distinguish
310    // between multi-value RDNs and multiple single-value RDNs.
311    {
312      text: 'L=L1 + L=L2\nL=L3',
313      legacy: {
314        L: ['L1', 'L2', 'L3']
315      },
316    },
317    {
318      text: 'L=L1\nL=L2\nL=L3',
319      legacy: {
320        L: ['L1', 'L2', 'L3']
321      },
322    },
323  ];
324
325  const serverKey = fixtures.readSync('x509-escaping/server-key.pem', 'utf8');
326
327  for (let i = 0; i < expectedSubjects.length; i++) {
328    const pem = fixtures.readSync(`x509-escaping/subj-${i}-cert.pem`, 'utf8');
329    const expected = expectedSubjects[i];
330
331    // X509Certificate interface is not supported in v12.x & v14.x. Disable
332    // checks for certX509.subject and certX509.issuer with expected
333    // text. The testcase is ported from v17.x
334    //
335    // Test the subject property of the X509Certificate API.
336    // const cert = new X509Certificate(pem);
337    // assert.strictEqual(cert.subject, expected.text);
338    // The issuer MUST be the same as the subject since the cert is self-signed.
339    // assert.strictEqual(cert.issuer, expected.text);
340
341    // Test that the certificate obtained by checkServerIdentity has the correct
342    // subject property.
343    const server = tls.createServer({
344      key: serverKey,
345      cert: pem,
346    }, common.mustCall((conn) => {
347      conn.destroy();
348      server.close();
349    })).listen(common.mustCall(() => {
350      const { port } = server.address();
351      tls.connect(port, {
352        ca: pem,
353        servername: 'example.com',
354        checkServerIdentity: (hostname, peerCert) => {
355          assert.strictEqual(hostname, 'example.com');
356          const expectedObject = Object.assign(Object.create(null),
357                                               expected.legacy);
358          assert.deepStrictEqual(peerCert.subject, expectedObject);
359          // The issuer MUST be the same as the subject since the cert is
360          // self-signed. Otherwise, OpenSSL would have already rejected the
361          // certificate while connecting to the TLS server.
362          assert.deepStrictEqual(peerCert.issuer, expectedObject);
363        },
364      }, common.mustCall());
365    }));
366  }
367}
368
369// The internal parsing logic must match the JSON specification exactly.
370{
371  // This list is partially based on V8's own JSON tests.
372  const invalidJSON = [
373    '"\\a invalid escape"',
374    '"\\v invalid escape"',
375    '"\\\' invalid escape"',
376    '"\\x42 invalid escape"',
377    '"\\u202 invalid escape"',
378    '"\\012 invalid escape"',
379    '"Unterminated string',
380    '"Unterminated string\\"',
381    '"Unterminated string\\\\\\"',
382    '"\u0000 control character"',
383    '"\u001e control character"',
384    '"\u001f control character"',
385  ];
386
387  for (const invalidStringLiteral of invalidJSON) {
388    // Usually, checkServerIdentity returns an error upon verification failure.
389    // In this case, however, it should throw an error since this is not a
390    // verification error. Node.js itself will never produce invalid JSON string
391    // literals, so this can only happen when users construct invalid subject
392    // alternative name strings (that do not follow escaping rules).
393    assert.throws(() => {
394      tls.checkServerIdentity('example.com', {
395        subjectaltname: `DNS:${invalidStringLiteral}`,
396      });
397    }, {
398      code: 'ERR_TLS_CERT_ALTNAME_FORMAT',
399      message: 'Invalid subject alternative name string'
400    });
401  }
402}
403
404// While node does not produce commas within SAN entries, it should parse them
405// correctly (i.e., not simply split at commas).
406{
407  // Regardless of the quotes, splitting this SAN string at commas would
408  // cause checkServerIdentity to see 'DNS:b.example.com' and thus to accept
409  // the certificate for b.example.com.
410  const san = 'DNS:"a.example.com, DNS:b.example.com, DNS:c.example.com"';
411
412  // This is what node used to do, and which is not correct!
413  const hostname = 'b.example.com';
414  assert.strictEqual(san.split(', ')[1], `DNS:${hostname}`);
415
416  // The new implementation should parse the string correctly.
417  const err = tls.checkServerIdentity(hostname, { subjectaltname: san });
418  assert(err);
419  assert.strictEqual(err.code, 'ERR_TLS_CERT_ALTNAME_INVALID');
420  assert.strictEqual(err.message, 'Hostname/IP does not match certificate\'s ' +
421                                  'altnames: Host: b.example.com. is not in ' +
422                                  'the cert\'s altnames: DNS:"a.example.com, ' +
423                                  'DNS:b.example.com, DNS:c.example.com"');
424}
425
426// The subject MUST be ignored if a dNSName subject alternative name exists.
427{
428  const key = fixtures.readKey('incorrect_san_correct_subject-key.pem');
429  const cert = fixtures.readKey('incorrect_san_correct_subject-cert.pem');
430
431  // The hostname is the CN, but not a SAN entry.
432  const servername = 'good.example.com';
433
434  // X509Certificate interface is not supported in v12.x & v14.x. Disable
435  // checks for certX509.subject and certX509.subjectAltName with expected
436  // value. The testcase is ported from v17.x
437  //
438  // const certX509 = new X509Certificate(cert);
439  // assert.strictEqual(certX509.subject, `CN=${servername}`);
440  // assert.strictEqual(certX509.subjectAltName, 'DNS:evil.example.com');
441
442  // Try connecting to a server that uses the self-signed certificate.
443  const server = tls.createServer({ key, cert }, common.mustNotCall());
444  server.listen(common.mustCall(() => {
445    const { port } = server.address();
446    const socket = tls.connect(port, {
447      ca: cert,
448      servername,
449    }, common.mustNotCall());
450    socket.on('error', common.mustCall((err) => {
451      assert.strictEqual(err.code, 'ERR_TLS_CERT_ALTNAME_INVALID');
452      assert.strictEqual(err.message, 'Hostname/IP does not match ' +
453                                      "certificate's altnames: Host: " +
454                                      "good.example.com. is not in the cert's" +
455                                      ' altnames: DNS:evil.example.com');
456    }));
457  })).unref();
458}
459
460// The subject MUST NOT be ignored if no dNSName subject alternative name
461// exists, even if other subject alternative names exist.
462{
463  const key = fixtures.readKey('irrelevant_san_correct_subject-key.pem');
464  const cert = fixtures.readKey('irrelevant_san_correct_subject-cert.pem');
465
466  // The hostname is the CN, but there is no dNSName SAN entry.
467  const servername = 'good.example.com';
468
469  // X509Certificate interface is not supported in v12.x & v14.x. Disable
470  // checks for certX509.subject and certX509.subjectAltName with expected
471  // value. The testcase is ported from v17.x
472  //
473  // const certX509 = new X509Certificate(cert);
474  // assert.strictEqual(certX509.subject, `CN=${servername}`);
475  // assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4');
476
477  // Connect to a server that uses the self-signed certificate.
478  const server = tls.createServer({ key, cert }, common.mustCall((socket) => {
479    socket.destroy();
480    server.close();
481  })).listen(common.mustCall(() => {
482    const { port } = server.address();
483    tls.connect(port, {
484      ca: cert,
485      servername,
486    }, common.mustCall(() => {
487      // Do nothing, the server will close the connection.
488    }));
489  }));
490}
491