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