index.js

import {log, normalizeArgs, select} from './utils.js'
import {CertificateAuthority} from './ca.js'
import net from './net.js'
import tls from 'tls'

const PRIVATE = new WeakMap()

/**
 * @callback SecureConnectionCallback
 * @param {tls.TLSSocket} sock The server side of the socket that was connected.
 * @returns {void}
 */

/**
 * @callback PSKCallback
 * @param {string} [hint] Optional message sent from the server to help client
 *   decide which identity to use during negotiation. Always null if TLS 1.3
 *   is used.
 * @returns {tls.PSKCallbackNegotation|null} Or null to stop the
 *   negotiation process. PSK must be compatible with the selected cipher's
 *   digest.
 */

/**
 * @callback SecureContextCallback
 * @param {Error} [error] An error ocurred.
 * @param {tls.SecureContext} [context] The context to use, if desired.
 * @returns {void}
 */

/**
 * @callback SNICallback
 * @param {string} serverName The name passed by the client.
 * @param {SecureContextCallback} callback Choose a security context
 *   for this connection.
 * @returns {void}
 */

/**
 * @typedef {object} ServerOpts
 * @property {CertificateAuthority} [authority] A CA instance to use.  If you
 *   are creating lots of servers, create one CA to pass into each of them, so
 *   that certs don't need to be created from scratch for each.
 * @property {SecureConnectionCallback} [secureConnectionListener] Installed
 *   as a listener for the 'secureConnection' event.
 * @property {Date} [notafter='Now + 5m'] Certificate not valid after this
 *   date.
 * @property {Date} [notbefore='Now - 1s'] Certificate not valid before this
 *   date.
 * @property {string} [subject] Subject Distinguished Name (DN).
 * @property {string[]} [names=['localhost']] DNS Subject Alternative Names
 *   for the cert.  Ignored for the CA cert.
 * @property {boolean} [allowHalfOpen=false] If set to false, then the socket
 *   will automatically end the writable side when the readable side ends.
 * @property {boolean} requestCert Whether to authenticate the remote peer by
 *   requesting a certificate. Clients always request a server certificate.
 *   Servers (isServer is true) may set requestCert to true to request a
 *   client certificate.
 * @property {boolean} rejectUnauthorized See tls.createServer().
 * @property {string[]|Buffer[]|DataView[]|Buffer|DataView} [ALPNProtocols] An
 *   array of strings, Buffers or DataViews, or a single Buffer or DataView
 *   containing the supported ALPN protocols. Buffers should have the format
 *   [len][name][len][name]... E.g. '\x08http/1.1\x08http/1.0', where the len
 *   byte is the length of the next protocol name. Passing an array is usually
 *   much simpler, e.g. ['http/1.1', 'http/1.0']. Protocols earlier in the
 *   list have higher preference than those later.
 * @property {SNICallback} SNICallback See tls.createServer().
 * @property {Buffer} [session] A Buffer instance, containing TLS session.
 * @property {boolean} requestOCSP If true, specifies that the OCSP status
 *   request extension will be added to the client hello and an 'OCSPResponse'
 *   event will be emitted on the socket before establishing a secure
 *   communication.
 * @property {tls.SecureContext} [secureContext] TLS context object created
 *   with tls.createSecureContext(). If a secureContext is not provided, one
 *   will be created by passing the entire options object to
 *   tls.createSecureContext().
 * @property {string[]|string|Buffer[]|Buffer} [ca] Optionally override the
 *   trusted CA certificates. Default is to trust the well-known CAs curated
 *   by Mozilla. Mozilla's CAs are completely replaced when CAs are explicitly
 *   specified using this option.
 * @property {string[]|string|Buffer[]|Buffer} [cert] Cert chains in PEM
 *   format. One cert chain should be provided per private key. Each cert
 *   chain should consist of the PEM formatted certificate for a provided
 *   private key, followed by the PEM formatted intermediate certificates (if
 *   any), in order, and not including the root CA (the root CA must be
 *   pre-known to the peer, see ca). When providing multiple cert chains, they
 *   do not have to be in the same order as their private keys in key. If the
 *   intermediate certificates are not provided, the peer will not be able to
 *   validate the certificate, and the handshake will fail.
 * @property {string} [sigalgs] Colon-separated list of supported signature
 *   algorithms. The list can contain digest algorithms (SHA256, MD5 etc.),
 *   public key algorithms (RSA-PSS, ECDSA etc.), combination of both
 *   ('RSA+SHA384') or TLS v1.3 scheme names (rsa_pss_pss_sha512). See OpenSSL
 *   man pages for more info.
 * @property {string} [ciphers] Cipher suite specification, replacing the
 *   default. Cipher names are separated by colons, and must be uppercased in
 *   order for OpenSSL to accept them.
 * @property {string} [clientCertEngine] Name of an OpenSSL engine which can
 *   provide the client certificate.
 * @property {string|string[]|Buffer|Buffer[]} [crl] PEM formatted CRLs
 *   (Certificate Revocation Lists).
 * @property {string|Buffer} [dhparam] Diffie-Hellman parameters, required for
 *   perfect forward secrecy. Use openssl dhparam to create the parameters.
 *   The key length must be greater than or equal to 1024 bits or else an
 *   error will be thrown. Although 1024 bits is permissible, use 2048 bits or
 *   larger for stronger security. If omitted or invalid, the parameters are
 *   silently discarded and DHE ciphers will not be available.
 * @property {string} [ecdhCurve] A string describing a named curve or a colon
 *   separated list of curve NIDs or names, for example P-521:P-384:P-256, to
 *   use for ECDH key agreement. Set to auto to select the curve
 *   automatically. Use crypto.getCurves() to obtain a list of available curve
 *   names. On recent releases, openssl ecparam -list_curves will also display
 *   the name and description of each available elliptic curve. Default:
 *   tls.DEFAULT_ECDH_CURVE.
 * @property {boolean} [honorCipherOrder] Attempt to use the server's cipher
 *   suite preferences instead of the client's. When true, causes
 *   SSL_OP_CIPHER_SERVER_PREFERENCE to be set in secureOptions, see OpenSSL
 *   Options for more information.
 * @property {string|string[]|Buffer|Buffer[]|object[]} [key] Private keys in
 *   PEM format. PEM allows the option of private keys being encrypted.
 *   Encrypted keys will be decrypted with options.passphrase. Multiple keys
 *   using different algorithms can be provided either as an array of
 *   unencrypted key strings or buffers, or an array of objects in the form
 *   {pem:string|buffer[, passphrase: string]}. The object form can only occur
 *   in an array. Passphrase is optional. Encrypted keys will be decrypted
 *   with object.passphrase if provided, or options.passphrase if it is not.
 * @property {string} [privateKeyEngine] Name of an OpenSSL engine to get
 *   private key from. Should be used together with privateKeyIdentifier.
 * @property {string} [privateKeyIdentifier] Identifier of a private key
 *   managed by an OpenSSL engine. Should be used together with
 *   privateKeyEngine. Should not be set together with key, because both
 *   options define a private key in different ways.
 * @property {string} [maxVersion=tls.DEFAULT_MAX_VERSION] Optionally set the
 *   maximum TLS version to allow. One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or
 *   'TLSv1'. Cannot be specified along with the secureProtocol option; use
 *   one or the other.
 * @property {string} [minVersion=tls.DEFAULT_MIN_VERSION] Optionally set the
 *   minimum TLS version to allow. One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or
 *   'TLSv1'. Cannot be specified along with the secureProtocol option; use
 *   one or the other. Avoid setting to less than TLSv1.2, but it may be
 *   required for interoperability.
 * @property {string} [passphrase] Shared passphrase used for a single private
 *   key and/or a PFX.
 * @property {string|string[]|Buffer|Buffer[]|object[]} [pfx] PFX or PKCS12
 *   encoded private key and certificate chain. PFX is an alternative to
 *   providing key and cert individually. PFX is usually encrypted, if it is,
 *   passphrase will be used to decrypt it.
 * @property {number} [secureOptions] Optionally affect the OpenSSL protocol
 *   behavior, which is not usually necessary. This should be used carefully
 *   if at all! Value is a numeric bitmask of the SSL_OP_* options from
 *   OpenSSL Options.
 * @property {string} [secureProtocol] Legacy mechanism to select the TLS
 *   protocol version to use, it does not support independent control of the
 *   minimum and maximum version, and does not support limiting the protocol
 *   to TLSv1.3. Use minVersion and maxVersion instead. The possible values
 *   are listed as SSL_METHODS, use the function names as strings. For
 *   example, use 'TLSv1_1_method' to force TLS version 1.1, or 'TLS_method'
 *   to allow any TLS protocol version up to TLSv1.3. It is not recommended to
 *   use TLS versions less than 1.2, but it may be required for
 *   interoperability. Default: none, see minVersion.
 * @property {string} [sessionIdContext] Opaque identifier used by servers to
 *   ensure session state is not shared between applications. Unused by
 *   clients.
 * @property {Buffer} [ticketKeys] 48-bytes of cryptographically strong
 *   pseudorandom data. See Session Resumption for more information.
 * @property {number} [sessionTimeout=300] The number of seconds after which a
 *   TLS session created by the server will no longer be resumable. See
 *   Session Resumption for more information.
 */

/**
 * A TLS server that mocks the network, but all of the crypto code is real.
 */
export class MockTLSServer extends net.Server {
  /**
   * Create a MockTLSServer instance.
   *
   * @param {ServerOpts|SecureConnectionCallback} [options] Server options.
   * @param {SecureConnectionCallback} [secureConnectionListener] Installed as
   *   a listener for the 'secureConnection' event.
   */
  constructor(options, secureConnectionListener) {
    const opts = normalizeArgs(options, {secureConnectionListener})
    const [local_opts, cert_opts, socket_opts, tls_opts] = select(
      opts,
      ['authority', 'secureConnectionListener'],
      ['notafter', 'notbefore', 'subject', 'names'],
      ['allowHalfOpen']
    )

    super(socket_opts, sock => {
      // Turn connection into TLS sock and call secureConnectionListener
      // @ts-ignore TS2345: Duplex should be allowed.
      const tsock = new tls.TLSSocket(sock, {
        isServer: true,
        cert: this.cert,
        key: PRIVATE.get(this),
        ...tls_opts,
      })
      const temit = tsock.emit
      tsock.emit = (eventName, ...args) => {
        log('tls', eventName, ...args.map(
          o => ((typeof o === 'object') ? o.constructor.name || o.toString() : o)
        ))
        return temit.call(tsock, eventName, ...args)
      }
      tsock.on('secure', () => {
        process.nextTick(() => this.emit('secureConnection', tsock))
      })
    })

    if (opts.cert) {
      // User-supplied cert and key.  Don't spin up a CA instance.
      this.cert = tls_opts.cert
      this.ca = tls_opts.ca
      PRIVATE.set(this, tls_opts.key)
    } else {
      const authority = local_opts.authority || new CertificateAuthority()
      const serverProps = authority.issue(cert_opts)
      this.cert = serverProps.cert
      this.ca = serverProps.ca
      PRIVATE.set(this, serverProps.key)
    }

    if (local_opts.secureConnectionListener) {
      this.on('secureConnection', local_opts.secureConnectionListener)
    }
    // Create a real TLSSocket for each connection.
  }

  /**
   * @typedef {object} ListenOptions
   * @property {number} port The port to listen on.
   * @property {Function} secureConnectionListener If it exists, installed as
   *   a listener for the 'secureConnection' event.
   */

  /**
   * Listen for incoming connections.  Whenever `connect` is called on this
   * server's port, create a mock socket wrapped in a TLSSocket.
   *
   * @param {number|ListenOptions} [options] Port number or options object.
   * @param {Function} [secureConnectionListener] If it exists, installed as
   *   a listener for the 'secureConnection' event.
   * @returns {MockTLSServer} This.
   */
  // @ts-ignore TS2416: This is compatible-enough with the base class.
  listen(options, secureConnectionListener) {
    const opts = normalizeArgs(options, {secureConnectionListener})

    if (opts.secureConnectionListener) {
      this.on('secureConnection', opts.secureConnectionListener)
    }
    super.listen(opts.port)
    return this
  }
}

/**
 * All of the options that can be passed in to connect(), with text copied
 * from the Node docs.
 *
 * @typedef {object} SocketConnectOpts
 * @property {number} port Listening server port to connect to.
 * @property {boolean} [allowHalfOpen=false] If set to false, then the socket
 *   will automatically end the writable side when the readable side ends.
 * @property {boolean} [rejectUnauthorized=true] If not false, the server
 *   certificate is verified against the list of supplied CAs. An 'error'
 *   event is emitted if verification fails; err.code contains the OpenSSL
 *   error code.
 * @property {PSKCallback} [pskCallback] When negotiating TLS-PSK (pre-shared
 *   keys), this function is called with optional identity hint provided by
 *   the server or null in case of TLS 1.3 where hint was removed. It will be
 *   necessary to provide a custom tls.checkServerIdentity() for the
 *   connection as the default one will try to check host name/IP of the
 *   server against the certificate but that's not applicable for PSK because
 *   there won't be a certificate present. More information can be found in
 *   the RFC 4279.
 * @property {string[]|Buffer[]|DataView[]|Buffer|DataView} [ALPNProtocols] An
 *   array of strings, Buffers or DataViews, or a single Buffer or DataView
 *   containing the supported ALPN protocols. Buffers should have the format
 *   [len][name][len][name]... E.g. '\x08http/1.1\x08http/1.0', where the len
 *   byte is the length of the next protocol name. Passing an array is usually
 *   much simpler, e.g. ['http/1.1', 'http/1.0']. Protocols earlier in the
 *   list have higher preference than those later.
 * @property {string} [servername] Server name for the SNI (Server Name
 *   Indication) TLS extension. It is the name of the host being connected to,
 *   and must be a host name, and not an IP address. It can be used by a
 *   multi-homed server to choose the correct certificate to present to the
 *   client, see the SNICallback option to tls.createServer().
 * @property {tls.checkServerIdentity} [checkServerIdentity] A callback
 *   function to be used (instead of the builtin tls.checkServerIdentity()
 *   function) when checking the server's host name (or the provided
 *   servername when explicitly set) against the certificate. This should
 *   return an <Error> if verification fails. The method should return
 *   undefined if the servername and cert are verified.
 * @property {Buffer} [session] A Buffer instance, containing TLS session.
 * @property {number} [minDHSize=1024] Minimum size of the DH parameter in
 *   bits to accept a TLS connection. When a server offers a DH parameter with
 *   a size less than minDHSize, the TLS connection is destroyed and an error
 *   is thrown.
 * @property {number} [highWaterMark=16 * 1024] Consistent with the readable
 *   stream highWaterMark parameter.
 * @property {tls.SecureContext} [secureContext] TLS context object created
 *   with tls.createSecureContext(). If a secureContext is not provided, one
 *   will be created by passing the entire options object to
 *   tls.createSecureContext().
 * @property {string[]|string|Buffer[]|Buffer} [ca] Optionally override the
 *   trusted CA certificates. Default is to trust the well-known CAs curated
 *   by Mozilla. Mozilla's CAs are completely replaced when CAs are explicitly
 *   specified using this option.
 * @property {string[]|string|Buffer[]|Buffer} [cert] Cert chains in PEM
 *   format. One cert chain should be provided per private key. Each cert
 *   chain should consist of the PEM formatted certificate for a provided
 *   private key, followed by the PEM formatted intermediate certificates (if
 *   any), in order, and not including the root CA (the root CA must be
 *   pre-known to the peer, see ca). When providing multiple cert chains, they
 *   do not have to be in the same order as their private keys in key. If the
 *   intermediate certificates are not provided, the peer will not be able to
 *   validate the certificate, and the handshake will fail.
 * @property {string} [sigalgs] Colon-separated list of supported signature
 *   algorithms. The list can contain digest algorithms (SHA256, MD5 etc.),
 *   public key algorithms (RSA-PSS, ECDSA etc.), combination of both
 *   ('RSA+SHA384') or TLS v1.3 scheme names (rsa_pss_pss_sha512). See OpenSSL
 *   man pages for more info.
 * @property {string} [ciphers] Cipher suite specification, replacing the
 *   default. Cipher names are separated by colons, and must be uppercased in
 *   order for OpenSSL to accept them.
 * @property {string} [clientCertEngine] Name of an OpenSSL engine which can
 *   provide the client certificate.
 * @property {string|string[]|Buffer|Buffer[]} [crl] PEM formatted CRLs
 *   (Certificate Revocation Lists).
 * @property {string|Buffer} [dhparam] Diffie-Hellman parameters, required for
 *   perfect forward secrecy. Use openssl dhparam to create the parameters.
 *   The key length must be greater than or equal to 1024 bits or else an
 *   error will be thrown. Although 1024 bits is permissible, use 2048 bits or
 *   larger for stronger security. If omitted or invalid, the parameters are
 *   silently discarded and DHE ciphers will not be available.
 * @property {string} [ecdhCurve] A string describing a named curve or a colon
 *   separated list of curve NIDs or names, for example P-521:P-384:P-256, to
 *   use for ECDH key agreement. Set to auto to select the curve
 *   automatically. Use crypto.getCurves() to obtain a list of available curve
 *   names. On recent releases, openssl ecparam -list_curves will also display
 *   the name and description of each available elliptic curve. Default:
 *   tls.DEFAULT_ECDH_CURVE.
 * @property {boolean} [honorCipherOrder] Attempt to use the server's cipher
 *   suite preferences instead of the client's. When true, causes
 *   SSL_OP_CIPHER_SERVER_PREFERENCE to be set in secureOptions, see OpenSSL
 *   Options for more information.
 * @property {string|string[]|Buffer|Buffer[]|object[]} [key] Private keys in
 *   PEM format. PEM allows the option of private keys being encrypted.
 *   Encrypted keys will be decrypted with options.passphrase. Multiple keys
 *   using different algorithms can be provided either as an array of
 *   unencrypted key strings or buffers, or an array of objects in the form
 *   {pem:string|buffer[, passphrase: string]}. The object form can only occur
 *   in an array. Passphrase is optional. Encrypted keys will be decrypted
 *   with object.passphrase if provided, or options.passphrase if it is not.
 * @property {string} [privateKeyEngine] Name of an OpenSSL engine to get
 *   private key from. Should be used together with privateKeyIdentifier.
 * @property {string} [privateKeyIdentifier] Identifier of a private key
 *   managed by an OpenSSL engine. Should be used together with
 *   privateKeyEngine. Should not be set together with key, because both
 *   options define a private key in different ways.
 * @property {string} [maxVersion=tls.DEFAULT_MAX_VERSION] Optionally set the
 *   maximum TLS version to allow. One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or
 *   'TLSv1'. Cannot be specified along with the secureProtocol option; use
 *   one or the other.
 * @property {string} [minVersion=tls.DEFAULT_MIN_VERSION] Optionally set the
 *   minimum TLS version to allow. One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or
 *   'TLSv1'. Cannot be specified along with the secureProtocol option; use
 *   one or the other. Avoid setting to less than TLSv1.2, but it may be
 *   required for interoperability.
 * @property {string} [passphrase] Shared passphrase used for a single private
 *   key and/or a PFX.
 * @property {string|string[]|Buffer|Buffer[]|object[]} [pfx] PFX or PKCS12
 *   encoded private key and certificate chain. PFX is an alternative to
 *   providing key and cert individually. PFX is usually encrypted, if it is,
 *   passphrase will be used to decrypt it.
 * @property {number} [secureOptions] Optionally affect the OpenSSL protocol
 *   behavior, which is not usually necessary. This should be used carefully
 *   if at all! Value is a numeric bitmask of the SSL_OP_* options from
 *   OpenSSL Options.
 * @property {string} [secureProtocol] Legacy mechanism to select the TLS
 *   protocol version to use, it does not support independent control of the
 *   minimum and maximum version, and does not support limiting the protocol
 *   to TLSv1.3. Use minVersion and maxVersion instead. The possible values
 *   are listed as SSL_METHODS, use the function names as strings. For
 *   example, use 'TLSv1_1_method' to force TLS version 1.1, or 'TLS_method'
 *   to allow any TLS protocol version up to TLSv1.3. It is not recommended to
 *   use TLS versions less than 1.2, but it may be required for
 *   interoperability. Default: none, see minVersion.
 * @property {string} [sessionIdContext] Opaque identifier used by servers to
 *   ensure session state is not shared between applications. Unused by
 *   clients.
 * @property {Buffer} [ticketKeys] 48-bytes of cryptographically strong
 *   pseudorandom data. See Session Resumption for more information.
 * @property {number} [sessionTimeout=300] The number of seconds after which a
 *   TLS session created by the server will no longer be resumable. See
 *   Session Resumption for more information.
 */

/**
 * Create a client connection to a listening server.
 *
 * @param {SocketConnectOpts|number} options Port to connect to.
 * @param {Function} [secureListener] If specified, function is
 *   registered for the 'secure' event.
 * @returns {tls.TLSSocket} Socket.
 */
export function connect(options, secureListener) {
  const {port, secureListener: listener, ...opts} =
    normalizeArgs(options, {secureListener})

  opts.socket = net.connect(port) // Emits error if connection failed
  if (!opts.ca) {
    const srv = net.getServer(port)
    if (srv && (srv instanceof MockTLSServer)) {
      opts.ca = srv.ca
    }
  }

  const secureSocket = tls.connect(opts)

  // Force-close on error.
  secureSocket.on('error', () => opts.socket.emit('close'))
  if (listener) {
    secureSocket.on('secure', listener)
  }
  return secureSocket
}

export const plainConnect = net.connect
export default {
  MockTLSServer,
  connect,
  plainConnect,
}