• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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