From 014fb6be8f0348926451ad5848019ed73fd93cb3 Mon Sep 17 00:00:00 2001 From: pfg Date: Wed, 4 Jun 2025 16:44:15 -0700 Subject: [PATCH] test-tls-check-server-identity (#20170) --- src/deps/c_ares.zig | 1 + src/js/node/tls.ts | 2 +- .../test-tls-check-server-identity.js | 338 ++++++++++++++++++ 3 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 test/js/node/test/parallel/test-tls-check-server-identity.js diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index 3b6096d7fd..babce5612f 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -2023,6 +2023,7 @@ pub fn Bun__canonicalizeIP_(globalThis: *JSC.JSGlobalObject, callframe: *JSC.Cal if (addr_str.len >= INET6_ADDRSTRLEN) { return .undefined; } + for (addr_str) |char| if (char == '/') return .undefined; // CIDR not allowed var ip_std_text: [INET6_ADDRSTRLEN + 1]u8 = undefined; // we need a null terminated string as input diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index c603536f02..946e4e2b7e 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -45,7 +45,7 @@ function unfqdn(host) { // String#toLowerCase() is locale-sensitive so we use // a conservative version that only lowercases A-Z. function toLowerCase(c) { - return StringFromCharCode.$call(32 + StringPrototypeCharCodeAt.$call(c, 0)); + return StringFromCharCode(32 + StringPrototypeCharCodeAt.$call(c, 0)); } function splitHost(host) { diff --git a/test/js/node/test/parallel/test-tls-check-server-identity.js b/test/js/node/test/parallel/test-tls-check-server-identity.js new file mode 100644 index 0000000000..3682aee37b --- /dev/null +++ b/test/js/node/test/parallel/test-tls-check-server-identity.js @@ -0,0 +1,338 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const util = require('util'); + +const tls = require('tls'); + +const tests = [ + // False-y values. + { + host: false, + cert: { subject: { CN: 'a.com' } }, + error: 'Host: false. is not cert\'s CN: a.com' + }, + { + host: null, + cert: { subject: { CN: 'a.com' } }, + error: 'Host: null. is not cert\'s CN: a.com' + }, + { + host: undefined, + cert: { subject: { CN: 'a.com' } }, + error: 'Host: undefined. is not cert\'s CN: a.com' + }, + + // Basic CN handling + { host: 'a.com', cert: { subject: { CN: 'a.com' } } }, + { host: 'a.com', cert: { subject: { CN: 'A.COM' } } }, + { + host: 'a.com', + cert: { subject: { CN: 'b.com' } }, + error: 'Host: a.com. is not cert\'s CN: b.com' + }, + { host: 'a.com', cert: { subject: { CN: 'a.com.' } } }, + { + host: 'a.com', + cert: { subject: { CN: '.a.com' } }, + error: 'Host: a.com. is not cert\'s CN: .a.com' + }, + + // IP address in CN. Technically allowed but so rare that we reject + // it anyway. If we ever do start allowing them, we should take care + // to only allow public (non-internal, non-reserved) IP addresses, + // because that's what the spec mandates. + { + host: '8.8.8.8', + cert: { subject: { CN: '8.8.8.8' } }, + error: 'IP: 8.8.8.8 is not in the cert\'s list: ' + }, + + // The spec suggests that a "DNS:" Subject Alternative Name containing an + // IP address is valid but it seems so suspect that we currently reject it. + { + host: '8.8.8.8', + cert: { subject: { CN: '8.8.8.8' }, subjectaltname: 'DNS:8.8.8.8' }, + error: 'IP: 8.8.8.8 is not in the cert\'s list: ' + }, + + // Likewise for "URI:" Subject Alternative Names. + // See also https://github.com/nodejs/node/issues/8108. + { + host: '8.8.8.8', + cert: { subject: { CN: '8.8.8.8' }, subjectaltname: 'URI:http://8.8.8.8/' }, + error: 'IP: 8.8.8.8 is not in the cert\'s list: ' + }, + + // An "IP Address:" Subject Alternative Name however is acceptable. + { + host: '8.8.8.8', + cert: { subject: { CN: '8.8.8.8' }, subjectaltname: 'IP Address:8.8.8.8' } + }, + + // But not when it's a CIDR. + { + host: '8.8.8.8', + cert: { + subject: { CN: '8.8.8.8' }, + subjectaltname: 'IP Address:8.8.8.0/24' + }, + error: 'IP: 8.8.8.8 is not in the cert\'s list: ' + }, + + // Wildcards in CN + { host: 'b.a.com', cert: { subject: { CN: '*.a.com' } } }, + { + host: 'ba.com', + cert: { subject: { CN: '*.a.com' } }, + error: 'Host: ba.com. is not cert\'s CN: *.a.com' + }, + { + host: '\n.b.com', + cert: { subject: { CN: '*n.b.com' } }, + error: 'Host: \n.b.com. is not cert\'s CN: *n.b.com' + }, + { host: 'b.a.com', + cert: { + subjectaltname: 'DNS:omg.com', + subject: { CN: '*.a.com' }, + }, + error: 'Host: b.a.com. is not in the cert\'s altnames: ' + + 'DNS:omg.com' }, + { + host: 'b.a.com', + cert: { subject: { CN: 'b*b.a.com' } }, + error: 'Host: b.a.com. is not cert\'s CN: b*b.a.com' + }, + + // Empty Cert + { + host: 'a.com', + cert: { }, + error: 'Cert does not contain a DNS name' + }, + + // Empty Subject w/DNS name + { + host: 'a.com', cert: { + subjectaltname: 'DNS:a.com', + } + }, + + // Empty Subject w/URI name + { + host: 'a.b.a.com', cert: { + subjectaltname: 'URI:http://a.b.a.com/', + }, + error: 'Cert does not contain a DNS name' + }, + + // Multiple CN fields + { + host: 'foo.com', cert: { + subject: { CN: ['foo.com', 'bar.com'] } // CN=foo.com; CN=bar.com; + } + }, + + // DNS names and CN + { + host: 'a.com', cert: { + subjectaltname: 'DNS:*', + subject: { CN: 'b.com' } + }, + error: 'Host: a.com. is not in the cert\'s altnames: ' + + 'DNS:*' + }, + { + host: 'a.com', cert: { + subjectaltname: 'DNS:*.com', + subject: { CN: 'b.com' } + }, + error: 'Host: a.com. is not in the cert\'s altnames: ' + + 'DNS:*.com' + }, + { + host: 'a.co.uk', cert: { + subjectaltname: 'DNS:*.co.uk', + subject: { CN: 'b.com' } + } + }, + { + host: 'a.com', cert: { + subjectaltname: 'DNS:*.a.com', + subject: { CN: 'a.com' } + }, + error: 'Host: a.com. is not in the cert\'s altnames: ' + + 'DNS:*.a.com' + }, + { + host: 'a.com', cert: { + subjectaltname: 'DNS:*.a.com', + subject: { CN: 'b.com' } + }, + error: 'Host: a.com. is not in the cert\'s altnames: ' + + 'DNS:*.a.com' + }, + { + host: 'a.com', cert: { + subjectaltname: 'DNS:a.com', + subject: { CN: 'b.com' } + } + }, + { + host: 'a.com', cert: { + subjectaltname: 'DNS:A.COM', + subject: { CN: 'b.com' } + } + }, + + // DNS names + { + host: 'a.com', cert: { + subjectaltname: 'DNS:*.a.com', + subject: {} + }, + error: 'Host: a.com. is not in the cert\'s altnames: ' + + 'DNS:*.a.com' + }, + { + host: 'b.a.com', cert: { + subjectaltname: 'DNS:*.a.com', + subject: {} + } + }, + { + host: 'c.b.a.com', cert: { + subjectaltname: 'DNS:*.a.com', + subject: {} + }, + error: 'Host: c.b.a.com. is not in the cert\'s altnames: ' + + 'DNS:*.a.com' + }, + { + host: 'b.a.com', cert: { + subjectaltname: 'DNS:*b.a.com', + subject: {} + } + }, + { + host: 'a-cb.a.com', cert: { + subjectaltname: 'DNS:*b.a.com', + subject: {} + } + }, + { + host: 'a.b.a.com', cert: { + subjectaltname: 'DNS:*b.a.com', + subject: {} + }, + error: 'Host: a.b.a.com. is not in the cert\'s altnames: ' + + 'DNS:*b.a.com' + }, + // Multiple DNS names + { + host: 'a.b.a.com', cert: { + subjectaltname: 'DNS:*b.a.com, DNS:a.b.a.com', + subject: {} + } + }, + // URI names + { + host: 'a.b.a.com', cert: { + subjectaltname: 'URI:http://a.b.a.com/', + subject: {} + }, + error: 'Cert does not contain a DNS name' + }, + { + host: 'a.b.a.com', cert: { + subjectaltname: 'URI:http://*.b.a.com/', + subject: {} + }, + error: 'Cert does not contain a DNS name' + }, + // IP addresses + { + host: 'a.b.a.com', cert: { + subjectaltname: 'IP Address:127.0.0.1', + subject: {} + }, + error: 'Cert does not contain a DNS name' + }, + { + host: '127.0.0.1', cert: { + subjectaltname: 'IP Address:127.0.0.1', + subject: {} + } + }, + { + host: '127.0.0.2', cert: { + subjectaltname: 'IP Address:127.0.0.1', + subject: {} + }, + error: 'IP: 127.0.0.2 is not in the cert\'s list: ' + + '127.0.0.1' + }, + { + host: '127.0.0.1', cert: { + subjectaltname: 'DNS:a.com', + subject: {} + }, + error: 'IP: 127.0.0.1 is not in the cert\'s list: ' + }, + { + host: 'localhost', cert: { + subjectaltname: 'DNS:a.com', + subject: { CN: 'localhost' } + }, + error: 'Host: localhost. is not in the cert\'s altnames: ' + + 'DNS:a.com' + }, + // IDNA + { + host: 'xn--bcher-kva.example.com', + cert: { subject: { CN: '*.example.com' } }, + }, + // RFC 6125, section 6.4.3: "[...] the client SHOULD NOT attempt to match + // a presented identifier where the wildcard character is embedded within + // an A-label [...]" + { + host: 'xn--bcher-kva.example.com', + cert: { subject: { CN: 'xn--*.example.com' } }, + error: 'Host: xn--bcher-kva.example.com. is not cert\'s CN: ' + + 'xn--*.example.com', + }, +]; + +tests.forEach(function(test, i) { + const err = tls.checkServerIdentity(test.host, test.cert); + assert.strictEqual(err?.reason, + test.error, + `Test# ${i} failed: ${util.inspect(test)} \n` + + `${test.error} != ${(err?.reason)}`); +});