1'use strict';
2
3const {
4  ArrayIsArray,
5  ArrayPrototypeFilter,
6  ArrayPrototypeForEach,
7  ArrayPrototypeJoin,
8  StringPrototypeSplit,
9  StringPrototypeStartsWith,
10} = primordials;
11
12const {
13  codes: {
14    ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED,
15    ERR_INVALID_ARG_TYPE,
16    ERR_INVALID_ARG_VALUE,
17  },
18} = require('internal/errors');
19
20const {
21  kEmptyObject,
22} = require('internal/util');
23
24const {
25  isArrayBufferView,
26} = require('internal/util/types');
27
28const {
29  validateBuffer,
30  validateInt32,
31  validateObject,
32  validateString,
33} = require('internal/validators');
34
35const {
36  toBuf,
37} = require('internal/crypto/util');
38
39const {
40  crypto: {
41    TLS1_2_VERSION,
42    TLS1_3_VERSION,
43  },
44} = internalBinding('constants');
45
46function getDefaultEcdhCurve() {
47  // We do it this way because DEFAULT_ECDH_CURVE can be
48  // changed by users, so we need to grab the current
49  // value, but we want the evaluation to be lazy.
50  return require('tls').DEFAULT_ECDH_CURVE || 'auto';
51}
52
53function getDefaultCiphers() {
54  // We do it this way because DEFAULT_CIPHERS can be
55  // changed by users, so we need to grab the current
56  // value, but we want the evaluation to be lazy.
57  return require('tls').DEFAULT_CIPHERS;
58}
59
60function addCACerts(context, certs, name) {
61  ArrayPrototypeForEach(certs, (cert) => {
62    validateKeyOrCertOption(name, cert);
63    context.addCACert(cert);
64  });
65}
66
67function setCerts(context, certs, name) {
68  ArrayPrototypeForEach(certs, (cert) => {
69    validateKeyOrCertOption(name, cert);
70    context.setCert(cert);
71  });
72}
73
74function validateKeyOrCertOption(name, value) {
75  if (typeof value !== 'string' && !isArrayBufferView(value)) {
76    throw new ERR_INVALID_ARG_TYPE(
77      name,
78      [
79        'string',
80        'Buffer',
81        'TypedArray',
82        'DataView',
83      ],
84      value,
85    );
86  }
87}
88
89function setKey(context, key, passphrase, name) {
90  validateKeyOrCertOption(`${name}.key`, key);
91  if (passphrase !== undefined && passphrase !== null)
92    validateString(passphrase, `${name}.passphrase`);
93  context.setKey(key, passphrase);
94}
95
96function processCiphers(ciphers, name) {
97  ciphers = StringPrototypeSplit(ciphers || getDefaultCiphers(), ':');
98
99  const cipherList =
100    ArrayPrototypeJoin(
101      ArrayPrototypeFilter(
102        ciphers,
103        (cipher) => {
104          return cipher.length > 0 &&
105            !StringPrototypeStartsWith(cipher, 'TLS_');
106        }), ':');
107
108  const cipherSuites =
109    ArrayPrototypeJoin(
110      ArrayPrototypeFilter(
111        ciphers,
112        (cipher) => {
113          return cipher.length > 0 &&
114            StringPrototypeStartsWith(cipher, 'TLS_');
115        }), ':');
116
117  // Specifying empty cipher suites for both TLS1.2 and TLS1.3 is invalid, its
118  // not possible to handshake with no suites.
119  if (cipherSuites === '' && cipherList === '')
120    throw new ERR_INVALID_ARG_VALUE(name, ciphers);
121
122  return { cipherList, cipherSuites };
123}
124
125function configSecureContext(context, options = kEmptyObject, name = 'options') {
126  validateObject(options, name);
127
128  const {
129    ca,
130    cert,
131    ciphers = getDefaultCiphers(),
132    clientCertEngine,
133    crl,
134    dhparam,
135    ecdhCurve = getDefaultEcdhCurve(),
136    key,
137    passphrase,
138    pfx,
139    privateKeyIdentifier,
140    privateKeyEngine,
141    sessionIdContext,
142    sessionTimeout,
143    sigalgs,
144    ticketKeys,
145  } = options;
146
147  // Set the cipher list and cipher suite before anything else because
148  // @SECLEVEL=<n> changes the security level and that affects subsequent
149  // operations.
150  if (ciphers !== undefined && ciphers !== null)
151    validateString(ciphers, `${name}.ciphers`);
152
153  // Work around an OpenSSL API quirk. cipherList is for TLSv1.2 and below,
154  // cipherSuites is for TLSv1.3 (and presumably any later versions). TLSv1.3
155  // cipher suites all have a standard name format beginning with TLS_, so split
156  // the ciphers and pass them to the appropriate API.
157  const {
158    cipherList,
159    cipherSuites,
160  } = processCiphers(ciphers, `${name}.ciphers`);
161
162  if (cipherSuites !== '')
163    context.setCipherSuites(cipherSuites);
164  context.setCiphers(cipherList);
165
166  if (cipherList === '' &&
167      context.getMinProto() < TLS1_3_VERSION &&
168      context.getMaxProto() > TLS1_2_VERSION) {
169    context.setMinProto(TLS1_3_VERSION);
170  }
171
172  // Add CA before the cert to be able to load cert's issuer in C++ code.
173  // NOTE(@jasnell): ca, cert, and key are permitted to be falsy, so do not
174  // change the checks to !== undefined checks.
175  if (ca) {
176    addCACerts(context, ArrayIsArray(ca) ? ca : [ca], `${name}.ca`);
177  } else {
178    context.addRootCerts();
179  }
180
181  if (cert) {
182    setCerts(context, ArrayIsArray(cert) ? cert : [cert], `${name}.cert`);
183  }
184
185  // Set the key after the cert.
186  // `ssl_set_pkey` returns `0` when the key does not match the cert, but
187  // `ssl_set_cert` returns `1` and nullifies the key in the SSL structure
188  // which leads to the crash later on.
189  if (key) {
190    if (ArrayIsArray(key)) {
191      for (let i = 0; i < key.length; ++i) {
192        const val = key[i];
193        const pem = (
194          val?.pem !== undefined ? val.pem : val);
195        const pass = (
196          val?.passphrase !== undefined ? val.passphrase : passphrase);
197        setKey(context, pem, pass, name);
198      }
199    } else {
200      setKey(context, key, passphrase, name);
201    }
202  }
203
204  if (sigalgs !== undefined && sigalgs !== null) {
205    validateString(sigalgs, `${name}.sigalgs`);
206
207    if (sigalgs === '')
208      throw new ERR_INVALID_ARG_VALUE(`${name}.sigalgs`, sigalgs);
209
210    context.setSigalgs(sigalgs);
211  }
212
213  if (privateKeyIdentifier !== undefined && privateKeyIdentifier !== null) {
214    if (privateKeyEngine === undefined || privateKeyEngine === null) {
215      // Engine is required when privateKeyIdentifier is present
216      throw new ERR_INVALID_ARG_VALUE(`${name}.privateKeyEngine`,
217                                      privateKeyEngine);
218    }
219    if (key) {
220      // Both data key and engine key can't be set at the same time
221      throw new ERR_INVALID_ARG_VALUE(`${name}.privateKeyIdentifier`,
222                                      privateKeyIdentifier);
223    }
224
225    if (typeof privateKeyIdentifier === 'string' &&
226        typeof privateKeyEngine === 'string') {
227      if (context.setEngineKey)
228        context.setEngineKey(privateKeyIdentifier, privateKeyEngine);
229      else
230        throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED();
231    } else if (typeof privateKeyIdentifier !== 'string') {
232      throw new ERR_INVALID_ARG_TYPE(`${name}.privateKeyIdentifier`,
233                                     ['string', 'null', 'undefined'],
234                                     privateKeyIdentifier);
235    } else {
236      throw new ERR_INVALID_ARG_TYPE(`${name}.privateKeyEngine`,
237                                     ['string', 'null', 'undefined'],
238                                     privateKeyEngine);
239    }
240  }
241
242  validateString(ecdhCurve, `${name}.ecdhCurve`);
243  context.setECDHCurve(ecdhCurve);
244
245  if (dhparam !== undefined && dhparam !== null) {
246    validateKeyOrCertOption(`${name}.dhparam`, dhparam);
247    const warning = context.setDHParam(dhparam === 'auto' || dhparam);
248    if (warning)
249      process.emitWarning(warning, 'SecurityWarning');
250  }
251
252  if (crl !== undefined && crl !== null) {
253    if (ArrayIsArray(crl)) {
254      for (const val of crl) {
255        validateKeyOrCertOption(`${name}.crl`, val);
256        context.addCRL(val);
257      }
258    } else {
259      validateKeyOrCertOption(`${name}.crl`, crl);
260      context.addCRL(crl);
261    }
262  }
263
264  if (sessionIdContext !== undefined && sessionIdContext !== null) {
265    validateString(sessionIdContext, `${name}.sessionIdContext`);
266    context.setSessionIdContext(sessionIdContext);
267  }
268
269  if (pfx !== undefined && pfx !== null) {
270    if (ArrayIsArray(pfx)) {
271      ArrayPrototypeForEach(pfx, (val) => {
272        const raw = val.buf || val;
273        const pass = val.passphrase || passphrase;
274        if (pass !== undefined && pass !== null) {
275          context.loadPKCS12(toBuf(raw), toBuf(pass));
276        } else {
277          context.loadPKCS12(toBuf(raw));
278        }
279      });
280    } else if (passphrase) {
281      context.loadPKCS12(toBuf(pfx), toBuf(passphrase));
282    } else {
283      context.loadPKCS12(toBuf(pfx));
284    }
285  }
286
287  if (typeof clientCertEngine === 'string') {
288    if (typeof context.setClientCertEngine !== 'function')
289      throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED();
290    else
291      context.setClientCertEngine(clientCertEngine);
292  } else if (clientCertEngine !== undefined && clientCertEngine !== null) {
293    throw new ERR_INVALID_ARG_TYPE(`${name}.clientCertEngine`,
294                                   ['string', 'null', 'undefined'],
295                                   clientCertEngine);
296  }
297
298  if (ticketKeys !== undefined && ticketKeys !== null) {
299    validateBuffer(ticketKeys, `${name}.ticketKeys`);
300    if (ticketKeys.byteLength !== 48) {
301      throw new ERR_INVALID_ARG_VALUE(
302        `${name}.ticketKeys`,
303        ticketKeys.byteLength,
304        'must be exactly 48 bytes');
305    }
306    context.setTicketKeys(ticketKeys);
307  }
308
309  if (sessionTimeout !== undefined && sessionTimeout !== null) {
310    validateInt32(sessionTimeout, `${name}.sessionTimeout`);
311    context.setSessionTimeout(sessionTimeout);
312  }
313}
314
315module.exports = {
316  configSecureContext,
317};
318