// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 // // 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. import type { PeerCertificate } from 'node:tls'; import { isIP } from 'node-internal:internal_net'; import { default as urlUtil } from 'node-internal:url'; import { ERR_TLS_CERT_ALTNAME_INVALID, ERR_TLS_CERT_ALTNAME_FORMAT, ERR_OUT_OF_RANGE, } from 'node-internal:internal_errors'; import { isUint8Array, isArrayBufferView } from 'node-internal:internal_types'; // String#toLowerCase() is locale-sensitive so we use // a conservative version that only lowercases A-Z. function toLowerCase(c: string): string { return String.fromCharCode(32 + c.charCodeAt(0)); } function unfqdn(host: string): string { return host.replace(/[.]$/, ''); } function splitHost(host: string): string[] { return unfqdn(host).replace(/[A-Z]/g, toLowerCase).split('.'); } function check( hostParts: string[], pattern: string | undefined | null, wildcards: boolean ): boolean { // Empty strings, null, undefined, etc. never match. if (!pattern) return false; const patternParts = splitHost(pattern); if (hostParts.length !== patternParts.length) return false; // Pattern has empty components, e.g. "bad..example.com". if (patternParts.includes('')) return false; // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no // good way to detect their encoding or normalize them so we simply // reject them. Control characters and blanks are rejected as well // because nothing good can come from accepting them. const isBad = (s: string): boolean => /[^\u0021-\u007F]/u.test(s); if (patternParts.some(isBad)) return false; // Check host parts from right to left first. for (let i = hostParts.length - 1; i > 0; i -= 1) { if (hostParts[i] !== patternParts[i]) return false; } const hostSubdomain = hostParts[0] as string; const patternSubdomain = patternParts[0] as string; const patternSubdomainParts = patternSubdomain.split('*', 3); // Short-circuit when the subdomain does not contain a wildcard. // RFC 6125 does not allow wildcard substitution for components // containing IDNA A-labels (Punycode) so match those verbatim. if (patternSubdomainParts.length === 1 || patternSubdomain.includes('xn--')) return hostSubdomain === patternSubdomain; if (!wildcards) return false; // More than one wildcard is always wrong. if (patternSubdomainParts.length > 2) return false; // *.tld wildcards are not allowed. if (patternParts.length <= 2) return false; const { 0: prefix, 1: suffix } = patternSubdomainParts as [string, string]; if (prefix.length + suffix.length > hostSubdomain.length) return false; if (!hostSubdomain.startsWith(prefix)) return false; if (!hostSubdomain.endsWith(suffix)) return false; return true; } // This pattern is used to determine the length of escaped sequences within // the subject alt names string. It allows any valid JSON string literal. // This MUST match the JSON specification (ECMA-404 / RFC8259) exactly. const jsonStringPattern = // eslint-disable-next-line no-control-regex /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/; function splitEscapedAltNames(altNames: string): string[] { const result = []; let currentToken = ''; let offset = 0; while (offset !== altNames.length) { const nextSep = altNames.indexOf(',', offset); const nextQuote = altNames.indexOf('"', offset); if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { // There is a quote character and there is no separator before the quote. currentToken += altNames.substring(offset, nextQuote); const match = jsonStringPattern.exec(altNames.substring(nextQuote)); if (!match) { throw new ERR_TLS_CERT_ALTNAME_FORMAT(); } currentToken += JSON.parse(match[0]) as string; offset = nextQuote + match[0].length; } else if (nextSep !== -1) { // There is a separator and no quote before it. currentToken += altNames.substring(offset, nextSep); result.push(currentToken); currentToken = ''; offset = nextSep + 2; } else { currentToken += altNames.substring(offset); offset = altNames.length; } } result.push(currentToken); return result; } export function checkServerIdentity( hostname: string, cert: Partial ): Error | undefined { const subject = cert.subject; const altNames = cert.subjectaltname; const dnsNames: string[] = []; const ips: string[] = []; // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion hostname = '' + hostname; if (altNames) { const splitAltNames = altNames.includes('"') ? splitEscapedAltNames(altNames) : altNames.split(', '); splitAltNames.forEach((name) => { if (name.startsWith('DNS:')) { dnsNames.push(name.slice(4)); } else if (name.startsWith('IP Address:')) { ips.push(urlUtil.canonicalizeIp(name.slice(11))); } }); } let valid = false; let reason = 'Unknown reason'; hostname = unfqdn(hostname); // Remove trailing dot for error messages. if (isIP(hostname)) { valid = ips.includes(urlUtil.canonicalizeIp(hostname)); if (!valid) reason = `IP: ${hostname} is not in the cert's list: ` + ips.join(', '); } else if (dnsNames.length > 0 || subject?.CN) { const hostParts = splitHost(hostname); const wildcard = (pattern: string): boolean => check(hostParts, pattern, true); if (dnsNames.length > 0) { valid = dnsNames.some(wildcard); if (!valid) reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`; } else { // Match against Common Name only if no supported identifiers exist. const cn = subject?.CN; if (Array.isArray(cn)) valid = cn.some(wildcard); else if (cn) valid = wildcard(cn); if (!valid) reason = `Host: ${hostname}. is not cert's CN: ${cn}`; } } else { reason = 'Cert does not contain a DNS name'; } if (!valid) { return new ERR_TLS_CERT_ALTNAME_INVALID(reason, hostname, cert); } return undefined; } // Convert protocols array into valid OpenSSL protocols list // ("\x06spdy/2\x08http/1.1\x08http/1.0") function convertProtocols(protocols: string[]): Buffer { const lens = Array.from({ length: protocols.length }, () => 0); const buff = Buffer.allocUnsafe( protocols.reduce((p, c, i) => { const len = Buffer.byteLength(c); if (len > 255) { throw new ERR_OUT_OF_RANGE( 'The byte length of the protocol at index ' + `${i} exceeds the maximum length.`, '<= 255', len, true ); } lens[i] = len; return p + 1 + len; }, 0) ); let offset = 0; for (let i = 0, c = protocols.length; i < c; i++) { buff[offset++] = lens[i] as number; buff.write(protocols[i] as string, offset); offset += lens[i] as number; } return buff; } export function convertALPNProtocols( protocols: unknown, out: { ALPNProtocols: Buffer; } ): void { // If protocols is Array - translate it into buffer if (Array.isArray(protocols)) { out.ALPNProtocols = convertProtocols(protocols as string[]); } else if (isUint8Array(protocols)) { // Copy new buffer not to be modified by user. out.ALPNProtocols = Buffer.from(protocols); } else if (isArrayBufferView(protocols)) { out.ALPNProtocols = Buffer.from( protocols.buffer.slice( protocols.byteOffset, protocols.byteOffset + protocols.byteLength ) ); } } export function createServer(): void { throw new Error('Not implemented'); } export function Server(): void { throw new Error('Not implemented'); } export function getCiphers(): void { throw new Error('Not implemented'); }