current commit
261025.11 · crypto: remove legacy OpenSSL code paths
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <base_object.h>
#include <crypto/crypto_client_hello.h>
#include <crypto/crypto_context.h>
#include <crypto/crypto_keys.h>
#include <memory_tracker.h>
#include <ncrypto.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_ossl.h>
#include <unordered_map>
#include "bindingdata.h"
#include "data.h"
#include "defs.h"
#include "sessionticket.h"

namespace node::quic {

class Session;
class TLSContext;

#ifdef OPENSSL_IS_BORINGSSL
static_assert(false,
              "This implementation of tlscontext relies on OpenSSL APIS "
              "that are not available in BoringSSL. An alternative impl "
              "using the equivalent BoringSSL APIs is possible but not "
              "yet implemented.");
#endif

// The OSSLContext is a wrapper around the ngtcp2_crypto_ossl_ctx that
// holds the state for the OpenSSL TLS session. The ngtcp2_crypto_ossl_ctx
// is required by ngtcp2's adapter for OpenSSL.
class OSSLContext final {
 public:
  OSSLContext();
  ~OSSLContext();
  DISALLOW_COPY_AND_MOVE(OSSLContext)

  operator SSL*() const;
  operator ngtcp2_crypto_ossl_ctx*() const;

  void Initialize(SSL* ssl,
                  ngtcp2_crypto_conn_ref* ref,
                  ngtcp2_conn* connection,
                  SSL_CTX* ssl_ctx);

  std::string_view get_cipher_name() const;
  std::string get_selected_alpn() const;
  std::string_view get_negotiated_group() const;

  bool set_alpn_protocols(std::string_view protocols) const;
  bool set_hostname(std::string_view hostname) const;
  bool set_verify_hostname(std::string_view hostname) const;
  bool set_early_data_enabled() const;
  bool set_transport_params(const ngtcp2_vec& tp) const;

  bool get_early_data_accepted() const;
  bool get_early_data_rejected() const;
  bool get_early_data_attempted() const;

  // Sets the session ticket for 0-RTT resumption. Returns true if the
  // ticket was set successfully and the ticket supports early data.
  bool set_session_ticket(const ncrypto::SSLSessionPointer& ticket);

  bool ConfigureServer() const;
  bool ConfigureClient() const;

  void reset();
  inline operator bool() const { return ctx_ != nullptr; }

 private:
  ngtcp2_crypto_ossl_ctx* ctx_;
  ngtcp2_conn* connection_ = nullptr;
};

// Every QUIC Session has exactly one TLSSession that maintains the state
// of the TLS handshake and negotiated keys after the handshake has been
// completed. It is separated out from the main Session class only as a
// convenience to help make the code more maintainable and understandable.
// A TLSSession is created from a TLSContext and maintains a reference to
// the context.
class TLSSession final : public MemoryRetainer {
 public:
  // TODO(@jasnell): In order to support using QUIC with host embedders
  // like Electron, we need to abstract the TLSContext and TLSSession
  // more so that they can use either the OpenSSL 3.5 or BoringSSL APIs.
  // This can be done by creatig an internal TLSSession::Impl and
  // TLSContext::Impl abstractions that can be implemented using either
  // of the two APIs, keeping the TLSSession and TLSContext APIs the same
  // for each.

  // Gets the TLSSession from the SSL pointer app data.
  static TLSSession& From(const SSL* ssl);

  // The constructor is public in order to satisfy the call to std::make_unique
  // in TLSContext::NewSession. It should not be called directly.
  TLSSession(Session* session,
             std::shared_ptr<TLSContext> context,
             const std::optional<SessionTicket>& maybeSessionTicket);
  DISALLOW_COPY_AND_MOVE(TLSSession)

  inline bool is_valid() const { return ossl_context_; }
  inline operator bool() const { return is_valid(); }
  inline Session& session() const { return *session_; }
  inline TLSContext& context() const { return *context_; }

  // Returns true if the handshake has been completed and early data was
  // accepted by the TLS session. This will assert if the handshake has
  // not been completed.
  bool early_data_was_accepted() const;
  bool early_data_was_rejected() const;
  bool early_data_was_attempted() const;

  v8::MaybeLocal<v8::Object> cert(Environment* env) const;
  v8::MaybeLocal<v8::Object> peer_cert(Environment* env) const;
  v8::MaybeLocal<v8::Object> ephemeral_key(Environment* env) const;
  v8::MaybeLocal<v8::Value> cipher_name(Environment* env) const;
  v8::MaybeLocal<v8::Value> cipher_version(Environment* env) const;

  // The SNI (server name) negotiated for the session
  const std::string_view servername() const;

  // The ALPN (protocol name) negotiated for the session
  const std::string protocol() const;

  // Server-side early selection. A server picks the SNI context and the
  // ALPN protocol from the ClientHello itself, before the TLS library
  // reaches ticket decryption and early data, and holds the handshake
  // there while the Session is handed to JavaScript. The results are
  // cached here because the SSL object does not carry them until the
  // later servername/ALPN callbacks run, which is after the pause.
  //
  // The state is explicit because the ClientHello callback can run more
  // than once per handshake: the TLS stack restarts ClientHello
  // processing when the handshake is resumed, and ngtcp2 can hand it
  // further CRYPTO data - a reordered chunk replayed by
  // conn_emit_pending_crypto_data, another CRYPTO frame in the same
  // packet - while the handshake is still suspended. Only kPending does
  // the work; kSelected keeps the handshake suspended until the Session
  // has been surfaced.
  enum class EarlySelection : uint8_t {
    // Nothing selected yet.
    kPending,
    // Selected, and the handshake is suspended waiting for the Session
    // to be handed to JavaScript.
    kSelected,
    // The Session has been surfaced; the handshake may run on.
    kComplete,
  };
  inline EarlySelection early_selection() const { return early_selection_; }
  inline void set_early_selection(EarlySelection state) {
    early_selection_ = state;
  }
  inline void set_servername(std::string_view name) { servername_ = name; }
  inline void set_alpn(std::string_view alpn) { alpn_ = alpn; }
  inline const std::string& alpn() const { return alpn_; }

  // Triggers key update to begin. This will fail and return false if either a
  // previous key update is in progress or if the initial handshake has not yet
  // been confirmed.
  bool InitiateKeyUpdate();

  struct PeerIdentityValidationError {
    v8::MaybeLocal<v8::Value> reason;
    v8::MaybeLocal<v8::Value> code;
  };

  // Checks the peer identity against the configured CA and CRL. If the peer
  // certificate is valid, std::nullopt is returned. Otherwise a
  // PeerIdentityValidationError is returned with the reason and code for the
  // failure. If there was an error setting the reason or code, the fields
  // will be empty but the PeerIdentityValidationError will still be returned.
  // This is an indication that an exception has been scheduled by v8.
  std::optional<PeerIdentityValidationError> VerifyPeerIdentity(
      Environment* env);

  inline std::string_view validation_error() const { return validation_error_; }

  void MemoryInfo(MemoryTracker* tracker) const override;
  SET_MEMORY_INFO_NAME(TLSSession)
  SET_SELF_SIZE(TLSSession)

 private:
  operator SSL*() const;
  void Initialize(const std::optional<SessionTicket>& maybeSessionTicket);

  static ngtcp2_conn* connection(ngtcp2_crypto_conn_ref* ref);

  ngtcp2_crypto_conn_ref ref_;
  std::shared_ptr<TLSContext> context_;
  OSSLContext ossl_context_;
  Session* session_;
  ncrypto::BIOPointer bio_trace_;
  std::string validation_error_ = "";
  std::string servername_;
  std::string alpn_;
  EarlySelection early_selection_ = EarlySelection::kPending;
};

// The TLSContext is used to create a TLSSession. For the client, there is
// typically only a single TLSContext for each TLSSession. For the server,
// there is a single TLSContext for the server and a TLSSession for every
// QUIC session created by that server.
class TLSContext final : public MemoryRetainer,
                         public std::enable_shared_from_this<TLSContext> {
 public:
  static constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:"
                                          "TLS_AES_256_GCM_SHA384:"
                                          "TLS_CHACHA20_POLY1305_"
                                          "SHA256:TLS_AES_128_CCM_SHA256";
  static constexpr auto DEFAULT_GROUPS = "X25519:P-256:P-384:P-521";

  struct Options final : public MemoryRetainer {
    // The SNI servername to use for this session. This option is only used by
    // the client.
    std::string servername = "localhost";

    // The ALPN protocol identifier(s) in wire format (length-prefixed,
    // concatenated). For clients this is a single entry. For servers
    // this may contain multiple entries in preference order.
    std::string alpn = NGHTTP3_ALPN_H3;

    // The list of TLS ciphers to use for this session.
    std::string ciphers = DEFAULT_CIPHERS;

    // The list of TLS groups to use for this session.
    std::string groups = DEFAULT_GROUPS;

    // TLS certificate compression (RFC 8879) preference, packed by the JS
    // layer as (length | alg0<<8 | alg1<<16 | alg2<<24). Algorithm IDs match
    // OpenSSL's TLSEXT_comp_cert_zlib (1), _brotli (2), _zstd (3). A value of
    // 0 means certificate compression is disabled (the default). Certificate
    // compression is particularly valuable for QUIC because it reduces the
    // size of the server's Certificate message, which is otherwise likely to
    // exceed the anti-amplification limit and force an extra handshake round
    // trip. JavaScript option name "certificateCompression".
    uint32_t certificate_compression = 0;

    // When true, enables keylog output for the session.
    bool keylog = false;

    // When true, the peer certificate is verified against the list of supplied
    // CA. If verification fails, the connection will be refused. When set,
    // instructs the server session to request a client auth certificate. This
    // option is only used by the server side.
    bool verify_client = false;

    // When true (the default), client certificates that fail chain
    // validation are rejected during the handshake. When false, the
    // handshake completes and the validation result is passed to JS
    // via the handshake callback for the application to decide.
    // This option is only used by the server side.
    bool reject_unauthorized = true;

    // When true, the client will set SSL_VERIFY_PEER so that OpenSSL
    // aborts the handshake if the server's certificate fails validation.
    // This is the "strict" verify_peer mode. When false (the default),
    // the handshake completes regardless and VerifyPeerIdentity is
    // called after to surface errors to JS. This option is only used
    // by the client side.
    bool verify_peer_strict = false;

    // When true, OpenSSL verifies that the server's certificate matches
    // the servername (hostname verification via SSL_set1_host). Should
    // be true for 'strict' and 'auto' verifyPeer modes, false for
    // 'manual'. Without this, a valid certificate for any domain would
    // be accepted. This option is only used by the client side.
    bool verify_hostname = false;

    // When true (the default), the server accepts 0-RTT early data
    // from clients with valid session tickets. When false, early data
    // is disabled and clients must complete a full handshake before
    // sending application data. Disabling early data prevents replay
    // attacks at the cost of an additional round trip.
    // This option is only used by the server side.
    bool enable_early_data = true;

    // When true, enables TLS tracing for the session. This should only be used
    // for debugging.
    // JavaScript option name "tlsTrace".
    bool enable_tls_trace = false;

    // When true, causes the private key passed in for the session to be
    // verified.
    // JavaScript option name "verifyPrivateKey"
    bool verify_private_key = false;

    // The TLS private key(s) to use for this session.
    // JavaScript option name "keys"
    std::vector<crypto::KeyObjectData> keys;

    // Collection of certificates to use for this session.
    // JavaScript option name "certs"
    std::vector<Store> certs;

    // Optional certificate authority overrides to use.
    // JavaScript option name "ca"
    std::vector<Store> ca;

    // Optional certificate revocation lists to use.
    // JavaScript option name "crl"
    std::vector<Store> crl;

    // The port to advertise in ORIGIN frames for this hostname.
    // Defaults to 443 (the standard HTTPS port). Only relevant for
    // server-side SNI entries used with HTTP/3.
    uint16_t port = 443;

    // Whether this hostname should be included in ORIGIN frames.
    // Only relevant for server-side SNI entries.
    bool authoritative = true;

    void MemoryInfo(MemoryTracker* tracker) const override;
    SET_MEMORY_INFO_NAME(TLSContext::Options)
    SET_SELF_SIZE(Options)

    // The default TLS configuration.
    static const Options kDefault;

    static v8::Maybe<Options> From(Environment* env,
                                   v8::Local<v8::Value> value);

    std::string ToString() const;
  };

  static std::shared_ptr<TLSContext> CreateClient(Environment* env,
                                                  const Options& options);
  static std::shared_ptr<TLSContext> CreateServer(Environment* env,
                                                  const Options& options);

  TLSContext(Environment* env, Side side, const Options& options);
  DISALLOW_COPY_AND_MOVE(TLSContext)

  // Each QUIC Session has exactly one TLSSession. Each TLSSession maintains
  // a reference to the TLSContext used to create it.
  std::unique_ptr<TLSSession> NewSession(
      Session* session, const std::optional<SessionTicket>& maybeSessionTicket);

  bool AddSNIContext(Environment* env,
                     const std::string& hostname,
                     const Options& options);

  bool SetSNIContexts(Environment* env,
                      const std::unordered_map<std::string, Options>& entries);

  inline Side side() const { return side_; }
  inline const Options& options() const { return options_; }
  inline operator bool() const { return ctx_ != nullptr; }
  inline operator const ncrypto::SSLCtxPointer&() const { return ctx_; }

  inline const std::string_view validation_error() const {
    return validation_error_;
  }

  void MemoryInfo(MemoryTracker* tracker) const override;
  SET_MEMORY_INFO_NAME(TLSContext)
  SET_SELF_SIZE(TLSContext)

 private:
  ncrypto::SSLCtxPointer Initialize(Environment* env);
  operator SSL_CTX*() const;

  // Returns the context to use for the requested host name, which is this
  // context when nothing more specific matches. Returns nullptr when the
  // connection cannot be served at all.
  TLSContext* SelectSNIContext(std::string_view servername);

  // Performs the server's early selection: SNI, then ALPN, then the
  // Application, and then suspends the handshake. See the comment on
  // TLSSession::EarlySelection.
  static crypto::ClientHelloResult OnClientHello(
      const crypto::ClientHelloContext& hello);

  static void OnKeylog(const SSL* ssl, const char* line);
  static int OnNewSession(SSL* ssl, SSL_SESSION* session);
  static int OnSelectAlpn(SSL* ssl,
                          const unsigned char** out,
                          unsigned char* outlen,
                          const unsigned char* in,
                          unsigned int inlen,
                          void* arg);
  static int OnVerifyClientCertificate(int preverify_ok, X509_STORE_CTX* ctx);
  static int OnSNI(SSL* ssl, int* ad, void* arg);

  // Lets sni_contexts_ be looked up by string_view, so a connection that
  // sends SNI does not have to build a std::string key to find its identity.
  struct StringHash {
    using is_transparent = void;
    size_t operator()(std::string_view value) const {
      return std::hash<std::string_view>{}(value);
    }
  };

  Side side_;
  Options options_;
  ncrypto::X509Pointer cert_;
  ncrypto::X509Pointer issuer_;
  std::string validation_error_ = "";
  ncrypto::SSLCtxPointer ctx_;
  std::unordered_map<std::string,
                     std::shared_ptr<TLSContext>,
                     StringHash,
                     std::equal_to<>>
      sni_contexts_;

  friend class TLSSession;
};

}  // namespace node::quic

#endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS