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