1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23 24const { 25 Array, 26 ArrayIsArray, 27 ArrayPrototypeForEach, 28 ArrayPrototypeIncludes, 29 ArrayPrototypeJoin, 30 ArrayPrototypePush, 31 ArrayPrototypeReduce, 32 ArrayPrototypeSome, 33 JSONParse, 34 ObjectDefineProperty, 35 ObjectFreeze, 36 RegExpPrototypeExec, 37 RegExpPrototypeSymbolReplace, 38 StringFromCharCode, 39 StringPrototypeCharCodeAt, 40 StringPrototypeEndsWith, 41 StringPrototypeIncludes, 42 StringPrototypeIndexOf, 43 StringPrototypeSlice, 44 StringPrototypeSplit, 45 StringPrototypeStartsWith, 46 StringPrototypeSubstring, 47} = primordials; 48 49const { 50 ERR_TLS_CERT_ALTNAME_FORMAT, 51 ERR_TLS_CERT_ALTNAME_INVALID, 52 ERR_OUT_OF_RANGE, 53} = require('internal/errors').codes; 54const internalUtil = require('internal/util'); 55internalUtil.assertCrypto(); 56const { 57 isArrayBufferView, 58 isUint8Array, 59} = require('internal/util/types'); 60 61const net = require('net'); 62const { getOptionValue } = require('internal/options'); 63const { getRootCertificates, getSSLCiphers } = internalBinding('crypto'); 64const { Buffer } = require('buffer'); 65const { canonicalizeIP } = internalBinding('cares_wrap'); 66const _tls_common = require('_tls_common'); 67const _tls_wrap = require('_tls_wrap'); 68const { createSecurePair } = require('internal/tls/secure-pair'); 69 70// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations 71// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more 72// renegotiations are seen. The settings are applied to all remote client 73// connections. 74exports.CLIENT_RENEG_LIMIT = 3; 75exports.CLIENT_RENEG_WINDOW = 600; 76 77exports.DEFAULT_CIPHERS = getOptionValue('--tls-cipher-list'); 78 79exports.DEFAULT_ECDH_CURVE = 'auto'; 80 81if (getOptionValue('--tls-min-v1.0')) 82 exports.DEFAULT_MIN_VERSION = 'TLSv1'; 83else if (getOptionValue('--tls-min-v1.1')) 84 exports.DEFAULT_MIN_VERSION = 'TLSv1.1'; 85else if (getOptionValue('--tls-min-v1.2')) 86 exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; 87else if (getOptionValue('--tls-min-v1.3')) 88 exports.DEFAULT_MIN_VERSION = 'TLSv1.3'; 89else 90 exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; 91 92if (getOptionValue('--tls-max-v1.3')) 93 exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; 94else if (getOptionValue('--tls-max-v1.2')) 95 exports.DEFAULT_MAX_VERSION = 'TLSv1.2'; 96else 97 exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; // Will depend on node version. 98 99 100exports.getCiphers = internalUtil.cachedResult( 101 () => internalUtil.filterDuplicateStrings(getSSLCiphers(), true), 102); 103 104let rootCertificates; 105 106function cacheRootCertificates() { 107 rootCertificates = ObjectFreeze(getRootCertificates()); 108} 109 110ObjectDefineProperty(exports, 'rootCertificates', { 111 __proto__: null, 112 configurable: false, 113 enumerable: true, 114 get: () => { 115 // Out-of-line caching to promote inlining the getter. 116 if (!rootCertificates) cacheRootCertificates(); 117 return rootCertificates; 118 }, 119}); 120 121// Convert protocols array into valid OpenSSL protocols list 122// ("\x06spdy/2\x08http/1.1\x08http/1.0") 123function convertProtocols(protocols) { 124 const lens = new Array(protocols.length); 125 const buff = Buffer.allocUnsafe(ArrayPrototypeReduce(protocols, (p, c, i) => { 126 const len = Buffer.byteLength(c); 127 if (len > 255) { 128 throw new ERR_OUT_OF_RANGE('The byte length of the protocol at index ' + 129 `${i} exceeds the maximum length.`, '<= 255', len, true); 130 } 131 lens[i] = len; 132 return p + 1 + len; 133 }, 0)); 134 135 let offset = 0; 136 for (let i = 0, c = protocols.length; i < c; i++) { 137 buff[offset++] = lens[i]; 138 buff.write(protocols[i], offset); 139 offset += lens[i]; 140 } 141 142 return buff; 143} 144 145exports.convertALPNProtocols = function convertALPNProtocols(protocols, out) { 146 // If protocols is Array - translate it into buffer 147 if (ArrayIsArray(protocols)) { 148 out.ALPNProtocols = convertProtocols(protocols); 149 } else if (isUint8Array(protocols)) { 150 // Copy new buffer not to be modified by user. 151 out.ALPNProtocols = Buffer.from(protocols); 152 } else if (isArrayBufferView(protocols)) { 153 out.ALPNProtocols = Buffer.from(protocols.buffer.slice( 154 protocols.byteOffset, 155 protocols.byteOffset + protocols.byteLength, 156 )); 157 } 158}; 159 160function unfqdn(host) { 161 return RegExpPrototypeSymbolReplace(/[.]$/, host, ''); 162} 163 164// String#toLowerCase() is locale-sensitive so we use 165// a conservative version that only lowercases A-Z. 166function toLowerCase(c) { 167 return StringFromCharCode(32 + StringPrototypeCharCodeAt(c, 0)); 168} 169 170function splitHost(host) { 171 return StringPrototypeSplit( 172 RegExpPrototypeSymbolReplace(/[A-Z]/g, unfqdn(host), toLowerCase), 173 '.', 174 ); 175} 176 177function check(hostParts, pattern, wildcards) { 178 // Empty strings, null, undefined, etc. never match. 179 if (!pattern) 180 return false; 181 182 const patternParts = splitHost(pattern); 183 184 if (hostParts.length !== patternParts.length) 185 return false; 186 187 // Pattern has empty components, e.g. "bad..example.com". 188 if (ArrayPrototypeIncludes(patternParts, '')) 189 return false; 190 191 // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no 192 // good way to detect their encoding or normalize them so we simply 193 // reject them. Control characters and blanks are rejected as well 194 // because nothing good can come from accepting them. 195 const isBad = (s) => RegExpPrototypeExec(/[^\u0021-\u007F]/u, s) !== null; 196 if (ArrayPrototypeSome(patternParts, isBad)) 197 return false; 198 199 // Check host parts from right to left first. 200 for (let i = hostParts.length - 1; i > 0; i -= 1) { 201 if (hostParts[i] !== patternParts[i]) 202 return false; 203 } 204 205 const hostSubdomain = hostParts[0]; 206 const patternSubdomain = patternParts[0]; 207 const patternSubdomainParts = StringPrototypeSplit(patternSubdomain, '*'); 208 209 // Short-circuit when the subdomain does not contain a wildcard. 210 // RFC 6125 does not allow wildcard substitution for components 211 // containing IDNA A-labels (Punycode) so match those verbatim. 212 if (patternSubdomainParts.length === 1 || 213 StringPrototypeIncludes(patternSubdomain, 'xn--')) 214 return hostSubdomain === patternSubdomain; 215 216 if (!wildcards) 217 return false; 218 219 // More than one wildcard is always wrong. 220 if (patternSubdomainParts.length > 2) 221 return false; 222 223 // *.tld wildcards are not allowed. 224 if (patternParts.length <= 2) 225 return false; 226 227 const { 0: prefix, 1: suffix } = patternSubdomainParts; 228 229 if (prefix.length + suffix.length > hostSubdomain.length) 230 return false; 231 232 if (!StringPrototypeStartsWith(hostSubdomain, prefix)) 233 return false; 234 235 if (!StringPrototypeEndsWith(hostSubdomain, suffix)) 236 return false; 237 238 return true; 239} 240 241// This pattern is used to determine the length of escaped sequences within 242// the subject alt names string. It allows any valid JSON string literal. 243// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly. 244const jsonStringPattern = 245 // eslint-disable-next-line no-control-regex 246 /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/; 247 248function splitEscapedAltNames(altNames) { 249 const result = []; 250 let currentToken = ''; 251 let offset = 0; 252 while (offset !== altNames.length) { 253 const nextSep = StringPrototypeIndexOf(altNames, ', ', offset); 254 const nextQuote = StringPrototypeIndexOf(altNames, '"', offset); 255 if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { 256 // There is a quote character and there is no separator before the quote. 257 currentToken += StringPrototypeSubstring(altNames, offset, nextQuote); 258 const match = RegExpPrototypeExec( 259 jsonStringPattern, StringPrototypeSubstring(altNames, nextQuote)); 260 if (!match) { 261 throw new ERR_TLS_CERT_ALTNAME_FORMAT(); 262 } 263 currentToken += JSONParse(match[0]); 264 offset = nextQuote + match[0].length; 265 } else if (nextSep !== -1) { 266 // There is a separator and no quote before it. 267 currentToken += StringPrototypeSubstring(altNames, offset, nextSep); 268 ArrayPrototypePush(result, currentToken); 269 currentToken = ''; 270 offset = nextSep + 2; 271 } else { 272 currentToken += StringPrototypeSubstring(altNames, offset); 273 offset = altNames.length; 274 } 275 } 276 ArrayPrototypePush(result, currentToken); 277 return result; 278} 279 280exports.checkServerIdentity = function checkServerIdentity(hostname, cert) { 281 const subject = cert.subject; 282 const altNames = cert.subjectaltname; 283 const dnsNames = []; 284 const ips = []; 285 286 hostname = '' + hostname; 287 288 if (altNames) { 289 const splitAltNames = StringPrototypeIncludes(altNames, '"') ? 290 splitEscapedAltNames(altNames) : 291 StringPrototypeSplit(altNames, ', '); 292 ArrayPrototypeForEach(splitAltNames, (name) => { 293 if (StringPrototypeStartsWith(name, 'DNS:')) { 294 ArrayPrototypePush(dnsNames, StringPrototypeSlice(name, 4)); 295 } else if (StringPrototypeStartsWith(name, 'IP Address:')) { 296 ArrayPrototypePush(ips, canonicalizeIP(StringPrototypeSlice(name, 11))); 297 } 298 }); 299 } 300 301 let valid = false; 302 let reason = 'Unknown reason'; 303 304 hostname = unfqdn(hostname); // Remove trailing dot for error messages. 305 306 if (net.isIP(hostname)) { 307 valid = ArrayPrototypeIncludes(ips, canonicalizeIP(hostname)); 308 if (!valid) 309 reason = `IP: ${hostname} is not in the cert's list: ` + 310 ArrayPrototypeJoin(ips, ', '); 311 } else if (dnsNames.length > 0 || subject?.CN) { 312 const hostParts = splitHost(hostname); 313 const wildcard = (pattern) => check(hostParts, pattern, true); 314 315 if (dnsNames.length > 0) { 316 valid = ArrayPrototypeSome(dnsNames, wildcard); 317 if (!valid) 318 reason = 319 `Host: ${hostname}. is not in the cert's altnames: ${altNames}`; 320 } else { 321 // Match against Common Name only if no supported identifiers exist. 322 const cn = subject.CN; 323 324 if (ArrayIsArray(cn)) 325 valid = ArrayPrototypeSome(cn, wildcard); 326 else if (cn) 327 valid = wildcard(cn); 328 329 if (!valid) 330 reason = `Host: ${hostname}. is not cert's CN: ${cn}`; 331 } 332 } else { 333 reason = 'Cert does not contain a DNS name'; 334 } 335 336 if (!valid) { 337 return new ERR_TLS_CERT_ALTNAME_INVALID(reason, hostname, cert); 338 } 339}; 340 341exports.createSecureContext = _tls_common.createSecureContext; 342exports.SecureContext = _tls_common.SecureContext; 343exports.TLSSocket = _tls_wrap.TLSSocket; 344exports.Server = _tls_wrap.Server; 345exports.createServer = _tls_wrap.createServer; 346exports.connect = _tls_wrap.connect; 347 348exports.createSecurePair = internalUtil.deprecate( 349 createSecurePair, 350 'tls.createSecurePair() is deprecated. Please use ' + 351 'tls.TLSSocket instead.', 'DEP0064'); 352