// 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 { Array, ArrayIsArray, ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeReduce, ArrayPrototypeSome, JSONParse, ObjectDefineProperty, ObjectFreeze, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, StringFromCharCode, StringPrototypeCharCodeAt, StringPrototypeEndsWith, StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeSubstring, } = primordials; const { ERR_TLS_CERT_ALTNAME_FORMAT, ERR_TLS_CERT_ALTNAME_INVALID, ERR_OUT_OF_RANGE } = require('internal/errors').codes; const internalUtil = require('internal/util'); internalUtil.assertCrypto(); const { isArrayBufferView, isUint8Array, } = require('internal/util/types'); const net = require('net'); const { getOptionValue } = require('internal/options'); const { getRootCertificates, getSSLCiphers } = internalBinding('crypto'); const { Buffer } = require('buffer'); const { canonicalizeIP } = internalBinding('cares_wrap'); const _tls_common = require('_tls_common'); const _tls_wrap = require('_tls_wrap'); const { createSecurePair } = require('internal/tls/secure-pair'); // Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations // every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more // renegotiations are seen. The settings are applied to all remote client // connections. exports.CLIENT_RENEG_LIMIT = 3; exports.CLIENT_RENEG_WINDOW = 600; exports.DEFAULT_CIPHERS = getOptionValue('--tls-cipher-list'); exports.DEFAULT_ECDH_CURVE = 'auto'; if (getOptionValue('--tls-min-v1.0')) exports.DEFAULT_MIN_VERSION = 'TLSv1'; else if (getOptionValue('--tls-min-v1.1')) exports.DEFAULT_MIN_VERSION = 'TLSv1.1'; else if (getOptionValue('--tls-min-v1.2')) exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; else if (getOptionValue('--tls-min-v1.3')) exports.DEFAULT_MIN_VERSION = 'TLSv1.3'; else exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; if (getOptionValue('--tls-max-v1.3')) exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; else if (getOptionValue('--tls-max-v1.2')) exports.DEFAULT_MAX_VERSION = 'TLSv1.2'; else exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; // Will depend on node version. exports.getCiphers = internalUtil.cachedResult( () => internalUtil.filterDuplicateStrings(getSSLCiphers(), true) ); let rootCertificates; function cacheRootCertificates() { rootCertificates = ObjectFreeze(getRootCertificates()); } ObjectDefineProperty(exports, 'rootCertificates', { __proto__: null, configurable: false, enumerable: true, get: () => { // Out-of-line caching to promote inlining the getter. if (!rootCertificates) cacheRootCertificates(); return rootCertificates; }, }); // Convert protocols array into valid OpenSSL protocols list // ("\x06spdy/2\x08http/1.1\x08http/1.0") function convertProtocols(protocols) { const lens = new Array(protocols.length); const buff = Buffer.allocUnsafe(ArrayPrototypeReduce(protocols, (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]; buff.write(protocols[i], offset); offset += lens[i]; } return buff; } exports.convertALPNProtocols = function convertALPNProtocols(protocols, out) { // If protocols is Array - translate it into buffer if (ArrayIsArray(protocols)) { out.ALPNProtocols = convertProtocols(protocols); } 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 )); } }; function unfqdn(host) { return RegExpPrototypeSymbolReplace(/[.]$/, host, ''); } // String#toLowerCase() is locale-sensitive so we use // a conservative version that only lowercases A-Z. function toLowerCase(c) { return StringFromCharCode(32 + StringPrototypeCharCodeAt(c, 0)); } function splitHost(host) { return StringPrototypeSplit( RegExpPrototypeSymbolReplace(/[A-Z]/g, unfqdn(host), toLowerCase), '.' ); } function check(hostParts, pattern, wildcards) { // 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 (ArrayPrototypeIncludes(patternParts, '')) 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) => RegExpPrototypeExec(/[^\u0021-\u007F]/u, s) !== null; if (ArrayPrototypeSome(patternParts, 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]; const patternSubdomain = patternParts[0]; const patternSubdomainParts = StringPrototypeSplit(patternSubdomain, '*'); // 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 || StringPrototypeIncludes(patternSubdomain, '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; if (prefix.length + suffix.length > hostSubdomain.length) return false; if (!StringPrototypeStartsWith(hostSubdomain, prefix)) return false; if (!StringPrototypeEndsWith(hostSubdomain, 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) { const result = []; let currentToken = ''; let offset = 0; while (offset !== altNames.length) { const nextSep = StringPrototypeIndexOf(altNames, ', ', offset); const nextQuote = StringPrototypeIndexOf(altNames, '"', offset); if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { // There is a quote character and there is no separator before the quote. currentToken += StringPrototypeSubstring(altNames, offset, nextQuote); const match = RegExpPrototypeExec( jsonStringPattern, StringPrototypeSubstring(altNames, nextQuote)); if (!match) { throw new ERR_TLS_CERT_ALTNAME_FORMAT(); } currentToken += JSONParse(match[0]); offset = nextQuote + match[0].length; } else if (nextSep !== -1) { // There is a separator and no quote before it. currentToken += StringPrototypeSubstring(altNames, offset, nextSep); ArrayPrototypePush(result, currentToken); currentToken = ''; offset = nextSep + 2; } else { currentToken += StringPrototypeSubstring(altNames, offset); offset = altNames.length; } } ArrayPrototypePush(result, currentToken); return result; } exports.checkServerIdentity = function checkServerIdentity(hostname, cert) { const subject = cert.subject; const altNames = cert.subjectaltname; const dnsNames = []; const ips = []; hostname = '' + hostname; if (altNames) { const splitAltNames = StringPrototypeIncludes(altNames, '"') ? splitEscapedAltNames(altNames) : StringPrototypeSplit(altNames, ', '); ArrayPrototypeForEach(splitAltNames, (name) => { if (StringPrototypeStartsWith(name, 'DNS:')) { ArrayPrototypePush(dnsNames, StringPrototypeSlice(name, 4)); } else if (StringPrototypeStartsWith(name, 'IP Address:')) { ArrayPrototypePush(ips, canonicalizeIP(StringPrototypeSlice(name, 11))); } }); } let valid = false; let reason = 'Unknown reason'; hostname = unfqdn(hostname); // Remove trailing dot for error messages. if (net.isIP(hostname)) { valid = ArrayPrototypeIncludes(ips, canonicalizeIP(hostname)); if (!valid) reason = `IP: ${hostname} is not in the cert's list: ` + ArrayPrototypeJoin(ips, ', '); } else if (dnsNames.length > 0 || subject?.CN) { const hostParts = splitHost(hostname); const wildcard = (pattern) => check(hostParts, pattern, true); if (dnsNames.length > 0) { valid = ArrayPrototypeSome(dnsNames, 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 (ArrayIsArray(cn)) valid = ArrayPrototypeSome(cn, 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); } }; exports.createSecureContext = _tls_common.createSecureContext; exports.SecureContext = _tls_common.SecureContext; exports.TLSSocket = _tls_wrap.TLSSocket; exports.Server = _tls_wrap.Server; exports.createServer = _tls_wrap.createServer; exports.connect = _tls_wrap.connect; exports.createSecurePair = internalUtil.deprecate( createSecurePair, 'tls.createSecurePair() is deprecated. Please use ' + 'tls.TLSSocket instead.', 'DEP0064');