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