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 ArrayPrototypePush, 28 JSONParse, 29 ObjectDefineProperty, 30 ObjectFreeze, 31 RegExpPrototypeExec, 32 StringFromCharCode, 33 StringPrototypeCharCodeAt, 34 StringPrototypeIncludes, 35 StringPrototypeIndexOf, 36 StringPrototypeReplace, 37 StringPrototypeSplit, 38 StringPrototypeSubstring, 39} = primordials; 40 41const { 42 ERR_TLS_CERT_ALTNAME_FORMAT, 43 ERR_TLS_CERT_ALTNAME_INVALID, 44 ERR_OUT_OF_RANGE 45} = require('internal/errors').codes; 46const internalUtil = require('internal/util'); 47const internalTLS = require('internal/tls'); 48internalUtil.assertCrypto(); 49const { isArrayBufferView } = require('internal/util/types'); 50 51const net = require('net'); 52const { getOptionValue } = require('internal/options'); 53const url = require('url'); 54const { getRootCertificates, getSSLCiphers } = internalBinding('crypto'); 55const { Buffer } = require('buffer'); 56const EventEmitter = require('events'); 57const { URL } = require('internal/url'); 58const DuplexPair = require('internal/streams/duplexpair'); 59const { canonicalizeIP } = internalBinding('cares_wrap'); 60const _tls_common = require('_tls_common'); 61const _tls_wrap = require('_tls_wrap'); 62 63// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations 64// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more 65// renegotiations are seen. The settings are applied to all remote client 66// connections. 67exports.CLIENT_RENEG_LIMIT = 3; 68exports.CLIENT_RENEG_WINDOW = 600; 69 70exports.DEFAULT_CIPHERS = getOptionValue('--tls-cipher-list'); 71 72exports.DEFAULT_ECDH_CURVE = 'auto'; 73 74if (getOptionValue('--tls-min-v1.0')) 75 exports.DEFAULT_MIN_VERSION = 'TLSv1'; 76else if (getOptionValue('--tls-min-v1.1')) 77 exports.DEFAULT_MIN_VERSION = 'TLSv1.1'; 78else if (getOptionValue('--tls-min-v1.2')) 79 exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; 80else if (getOptionValue('--tls-min-v1.3')) 81 exports.DEFAULT_MIN_VERSION = 'TLSv1.3'; 82else 83 exports.DEFAULT_MIN_VERSION = 'TLSv1.2'; 84 85if (getOptionValue('--tls-max-v1.3')) 86 exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; 87else if (getOptionValue('--tls-max-v1.2')) 88 exports.DEFAULT_MAX_VERSION = 'TLSv1.2'; 89else 90 exports.DEFAULT_MAX_VERSION = 'TLSv1.3'; // Will depend on node version. 91 92 93exports.getCiphers = internalUtil.cachedResult( 94 () => internalUtil.filterDuplicateStrings(getSSLCiphers(), true) 95); 96 97let rootCertificates; 98 99function cacheRootCertificates() { 100 rootCertificates = ObjectFreeze(getRootCertificates()); 101} 102 103ObjectDefineProperty(exports, 'rootCertificates', { 104 configurable: false, 105 enumerable: true, 106 get: () => { 107 // Out-of-line caching to promote inlining the getter. 108 if (!rootCertificates) cacheRootCertificates(); 109 return rootCertificates; 110 }, 111}); 112 113// Convert protocols array into valid OpenSSL protocols list 114// ("\x06spdy/2\x08http/1.1\x08http/1.0") 115function convertProtocols(protocols) { 116 const lens = new Array(protocols.length); 117 const buff = Buffer.allocUnsafe(protocols.reduce((p, c, i) => { 118 const len = Buffer.byteLength(c); 119 if (len > 255) { 120 throw new ERR_OUT_OF_RANGE('The byte length of the protocol at index ' + 121 `${i} exceeds the maximum length.`, '<= 255', len, true); 122 } 123 lens[i] = len; 124 return p + 1 + len; 125 }, 0)); 126 127 let offset = 0; 128 for (let i = 0, c = protocols.length; i < c; i++) { 129 buff[offset++] = lens[i]; 130 buff.write(protocols[i], offset); 131 offset += lens[i]; 132 } 133 134 return buff; 135} 136 137exports.convertALPNProtocols = function convertALPNProtocols(protocols, out) { 138 // If protocols is Array - translate it into buffer 139 if (ArrayIsArray(protocols)) { 140 out.ALPNProtocols = convertProtocols(protocols); 141 } else if (isArrayBufferView(protocols)) { 142 // Copy new buffer not to be modified by user. 143 out.ALPNProtocols = Buffer.from(protocols); 144 } 145}; 146 147function unfqdn(host) { 148 return host.replace(/[.]$/, ''); 149} 150 151// String#toLowerCase() is locale-sensitive so we use 152// a conservative version that only lowercases A-Z. 153function toLowerCase(c) { 154 return StringFromCharCode(32 + StringPrototypeCharCodeAt(c, 0)); 155} 156 157function splitHost(host) { 158 return StringPrototypeSplit( 159 StringPrototypeReplace(unfqdn(host), /[A-Z]/g, toLowerCase), 160 '.' 161 ); 162} 163 164function check(hostParts, pattern, wildcards) { 165 // Empty strings, null, undefined, etc. never match. 166 if (!pattern) 167 return false; 168 169 const patternParts = splitHost(pattern); 170 171 if (hostParts.length !== patternParts.length) 172 return false; 173 174 // Pattern has empty components, e.g. "bad..example.com". 175 if (patternParts.includes('')) 176 return false; 177 178 // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no 179 // good way to detect their encoding or normalize them so we simply 180 // reject them. Control characters and blanks are rejected as well 181 // because nothing good can come from accepting them. 182 const isBad = (s) => /[^\u0021-\u007F]/u.test(s); 183 if (patternParts.some(isBad)) 184 return false; 185 186 // Check host parts from right to left first. 187 for (let i = hostParts.length - 1; i > 0; i -= 1) { 188 if (hostParts[i] !== patternParts[i]) 189 return false; 190 } 191 192 const hostSubdomain = hostParts[0]; 193 const patternSubdomain = patternParts[0]; 194 const patternSubdomainParts = patternSubdomain.split('*'); 195 196 // Short-circuit when the subdomain does not contain a wildcard. 197 // RFC 6125 does not allow wildcard substitution for components 198 // containing IDNA A-labels (Punycode) so match those verbatim. 199 if (patternSubdomainParts.length === 1 || patternSubdomain.includes('xn--')) 200 return hostSubdomain === patternSubdomain; 201 202 if (!wildcards) 203 return false; 204 205 // More than one wildcard is always wrong. 206 if (patternSubdomainParts.length > 2) 207 return false; 208 209 // *.tld wildcards are not allowed. 210 if (patternParts.length <= 2) 211 return false; 212 213 const [prefix, suffix] = patternSubdomainParts; 214 215 if (prefix.length + suffix.length > hostSubdomain.length) 216 return false; 217 218 if (!hostSubdomain.startsWith(prefix)) 219 return false; 220 221 if (!hostSubdomain.endsWith(suffix)) 222 return false; 223 224 return true; 225} 226 227// This pattern is used to determine the length of escaped sequences within 228// the subject alt names string. It allows any valid JSON string literal. 229// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly. 230const jsonStringPattern = 231 // eslint-disable-next-line no-control-regex 232 /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/; 233 234function splitEscapedAltNames(altNames) { 235 const result = []; 236 let currentToken = ''; 237 let offset = 0; 238 while (offset !== altNames.length) { 239 const nextSep = StringPrototypeIndexOf(altNames, ', ', offset); 240 const nextQuote = StringPrototypeIndexOf(altNames, '"', offset); 241 if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { 242 // There is a quote character and there is no separator before the quote. 243 currentToken += StringPrototypeSubstring(altNames, offset, nextQuote); 244 const match = RegExpPrototypeExec( 245 jsonStringPattern, StringPrototypeSubstring(altNames, nextQuote)); 246 if (!match) { 247 throw new ERR_TLS_CERT_ALTNAME_FORMAT(); 248 } 249 currentToken += JSONParse(match[0]); 250 offset = nextQuote + match[0].length; 251 } else if (nextSep !== -1) { 252 // There is a separator and no quote before it. 253 currentToken += StringPrototypeSubstring(altNames, offset, nextSep); 254 ArrayPrototypePush(result, currentToken); 255 currentToken = ''; 256 offset = nextSep + 2; 257 } else { 258 currentToken += StringPrototypeSubstring(altNames, offset); 259 offset = altNames.length; 260 } 261 } 262 ArrayPrototypePush(result, currentToken); 263 return result; 264} 265 266let urlWarningEmitted = false; 267exports.checkServerIdentity = function checkServerIdentity(hostname, cert) { 268 const subject = cert.subject; 269 const altNames = cert.subjectaltname; 270 const dnsNames = []; 271 const uriNames = []; 272 const ips = []; 273 274 hostname = '' + hostname; 275 276 if (altNames) { 277 const splitAltNames = StringPrototypeIncludes(altNames, '"') ? 278 splitEscapedAltNames(altNames) : 279 StringPrototypeSplit(altNames, ', '); 280 for (const name of splitAltNames) { 281 if (name.startsWith('DNS:')) { 282 dnsNames.push(name.slice(4)); 283 } else if (process.REVERT_CVE_2021_44531 && name.startsWith('URI:')) { 284 let uri; 285 try { 286 uri = new URL(name.slice(4)); 287 } catch { 288 uri = url.parse(name.slice(4)); 289 if (!urlWarningEmitted && !process.noDeprecation) { 290 urlWarningEmitted = true; 291 process.emitWarning( 292 `The URI ${name.slice(4)} found in cert.subjectaltname ` + 293 'is not a valid URI, and is supported in the tls module ' + 294 'solely for compatibility.', 295 'DeprecationWarning', 'DEP0109'); 296 } 297 } 298 uriNames.push(uri.hostname); // TODO(bnoordhuis) Also use scheme. 299 } else if (name.startsWith('IP Address:')) { 300 ips.push(canonicalizeIP(name.slice(11))); 301 } 302 } 303 } 304 305 let valid = false; 306 let reason = 'Unknown reason'; 307 308 const hasAltNames = 309 dnsNames.length > 0 || ips.length > 0 || uriNames.length > 0; 310 311 hostname = unfqdn(hostname); // Remove trailing dot for error messages. 312 313 if (net.isIP(hostname)) { 314 valid = ips.includes(canonicalizeIP(hostname)); 315 if (!valid) 316 reason = `IP: ${hostname} is not in the cert's list: ${ips.join(', ')}`; 317 // TODO(bnoordhuis) Also check URI SANs that are IP addresses. 318 } else if ((process.REVERT_CVE_2021_44531 && (hasAltNames || subject)) || 319 (dnsNames.length > 0 || (subject && subject.CN))) { 320 const hostParts = splitHost(hostname); 321 const wildcard = (pattern) => check(hostParts, pattern, true); 322 323 if ((process.REVERT_CVE_2021_44531 && hasAltNames) || 324 (dnsNames.length > 0)) { 325 const noWildcard = (pattern) => check(hostParts, pattern, false); 326 valid = dnsNames.some(wildcard) || uriNames.some(noWildcard); 327 if (!valid) 328 reason = 329 `Host: ${hostname}. is not in the cert's altnames: ${altNames}`; 330 } else { 331 // Match against Common Name only if no supported identifiers exist. 332 const cn = subject.CN; 333 334 if (ArrayIsArray(cn)) 335 valid = cn.some(wildcard); 336 else if (cn) 337 valid = wildcard(cn); 338 339 if (!valid) 340 reason = `Host: ${hostname}. is not cert's CN: ${cn}`; 341 } 342 } else { 343 reason = 'Cert does not contain a DNS name'; 344 } 345 346 if (!valid) { 347 return new ERR_TLS_CERT_ALTNAME_INVALID(reason, hostname, cert); 348 } 349}; 350 351 352class SecurePair extends EventEmitter { 353 constructor(secureContext = exports.createSecureContext(), 354 isServer = false, 355 requestCert = !isServer, 356 rejectUnauthorized = false, 357 options = {}) { 358 super(); 359 const { socket1, socket2 } = new DuplexPair(); 360 361 this.server = options.server; 362 this.credentials = secureContext; 363 364 this.encrypted = socket1; 365 this.cleartext = new exports.TLSSocket(socket2, { 366 secureContext, 367 isServer, 368 requestCert, 369 rejectUnauthorized, 370 ...options 371 }); 372 this.cleartext.once('secure', () => this.emit('secure')); 373 } 374 375 destroy() { 376 this.cleartext.destroy(); 377 this.encrypted.destroy(); 378 } 379} 380 381 382exports.parseCertString = internalUtil.deprecate( 383 internalTLS.parseCertString, 384 'tls.parseCertString() is deprecated. ' + 385 'Please use querystring.parse() instead.', 386 'DEP0076'); 387 388exports.createSecureContext = _tls_common.createSecureContext; 389exports.SecureContext = _tls_common.SecureContext; 390exports.TLSSocket = _tls_wrap.TLSSocket; 391exports.Server = _tls_wrap.Server; 392exports.createServer = _tls_wrap.createServer; 393exports.connect = _tls_wrap.connect; 394 395exports.createSecurePair = internalUtil.deprecate( 396 function createSecurePair(...args) { 397 return new SecurePair(...args); 398 }, 399 'tls.createSecurePair() is deprecated. Please use ' + 400 'tls.TLSSocket instead.', 'DEP0064'); 401