Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@
'src/crypto/crypto_spkac.h',
'src/crypto/crypto_util.h',
'src/crypto/crypto_cipher.h',
'src/crypto/crypto_client_hello.h',
'src/crypto/crypto_common.h',
'src/crypto/crypto_dsa.h',
'src/crypto/crypto_hash.h',
Expand Down
248 changes: 248 additions & 0 deletions src/crypto/crypto_client_hello.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#ifndef SRC_CRYPTO_CRYPTO_CLIENT_HELLO_H_
#define SRC_CRYPTO_CRYPTO_CLIENT_HELLO_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <openssl/ssl.h>
#include <openssl/tls1.h>
#include <algorithm>
#include <cstdint>
#include <optional>
#include <span>
#include <string_view>

namespace node::crypto {

// Support for the early ClientHello callback, which the TLS library invokes
// once a complete ClientHello has been received but before version
// negotiation and before any session or ticket resumption. This lets us
// inspect what the client asks for and prepare our configuration (with JS
// involvement potentially) before we actually process the handshake.
//
// OpenSSL and BoringSSL disagree on various details, which are centralized
// here so callers can ignore the differences.

// What the callback wants the TLS library to do next.
enum class ClientHelloResult {
// Carry on with the handshake.
kContinue,
// Unwind out of the TLS library, leaving the handshake suspended. The
// callback runs again from the start when the handshake is resumed.
kRetry,
// Abort the handshake.
kFail,
};

// The ClientHello being processed, and the inputs from it that are usable
// this early. Instances are only valid for the duration of the callback.
class ClientHelloContext final {
public:
#ifdef OPENSSL_IS_BORINGSSL
using Handle = const SSL_CLIENT_HELLO*;
using Result = ssl_select_cert_result_t;
#else
using Handle = SSL*;
using Result = int;
#endif

explicit ClientHelloContext(Handle handle, int* alert = nullptr)
: handle_(handle), alert_(alert) {}

inline SSL* ssl() const {
#ifdef OPENSSL_IS_BORINGSSL
return handle_->ssl;
#else
return handle_;
#endif
}

// The legacy_session_id. Note that a TLS 1.3 ClientHello carries a fake
// one for middlebox compatibility, so a non-empty value here does not
// mean the client is attempting session-id resumption.
inline std::span<const uint8_t> session_id() const {
#ifdef OPENSSL_IS_BORINGSSL
return {handle_->session_id, handle_->session_id_len};
#else
const uint8_t* id = nullptr;
size_t len = SSL_client_hello_get0_session_id(handle_, &id);
if (id == nullptr) return {};
return {id, len};
#endif
}

inline bool has_session_ticket() const {
auto ext = extension(TLSEXT_TYPE_session_ticket);
return ext.has_value() && !ext->empty();
}

// The host_name from the server_name extension, or an empty view when the
// client did not send a usable one. SSL_get_servername() cannot be used
// this early because the servername callback has not run yet, so this
// applies the same constraints that callback's parser would: exactly one
// entry, of type host_name, of a sane length and free of NULs. Anything
// else reads as absent here and is rejected outright by the TLS stack a
// moment later, once it parses extensions for itself.
inline std::string_view servername() const {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are rather large to be inline definitions. Would much prefer the impls to be moved out to crypto_client_hello.cc

auto ext = extension(TLSEXT_TYPE_server_name);
if (!ext.has_value()) return {};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... this does not distinguish between not provided and zero length? Should it? e.g. by returning std::optional<std::string_view> instead.

// RFC 6066: a 16-bit list length, then one entry of an 8-bit name type,
// a 16-bit name length and that many name bytes.
auto list = ReadVector16(*ext);
if (!list.has_value() || list->size() < 3) return {};
if ((*list)[0] != TLSEXT_NAMETYPE_host_name) return {};
auto name = ReadVector16(list->subspan(1));
if (!name.has_value()) return {};
// The entry must be the only one, and must be a plausible host name.
if (name->size() + 3 != list->size()) return {};
if (name->empty() || name->size() > TLSEXT_MAXLEN_host_name) return {};
if (std::find(name->begin(), name->end(), 0) != name->end()) return {};
return {reinterpret_cast<const char*>(name->data()), name->size()};
}

// The protocols the client offered, in ALPN wire format: each entry is a
// length byte followed by that many name bytes. Empty when the client did
// not offer ALPN. This is the format SelectNextProtocol() expects.
inline std::span<const uint8_t> alpn_protocols() const {
auto ext = extension(TLSEXT_TYPE_application_layer_protocol_negotiation);
if (!ext.has_value()) return {};
// RFC 7301: a 16-bit list length, then the protocol names.
return ReadVector16(*ext).value_or(std::span<const uint8_t>());
}

// The raw body of an extension the client sent, or nothing when the
// client did not send that extension.
inline std::optional<std::span<const uint8_t>> extension(
unsigned int type) const {
const uint8_t* data = nullptr;
size_t len = 0;
#ifdef OPENSSL_IS_BORINGSSL
if (!SSL_early_callback_ctx_extension_get(handle_, type, &data, &len)) {
return std::nullopt;
}
#else
if (SSL_client_hello_get0_ext(handle_, type, &data, &len) != 1) {
return std::nullopt;
}
#endif
return std::span<const uint8_t>(data, len);
}

// Sets the TLS alert to send if the callback fails. Ignored by TLS
// libraries that choose the alert themselves.
inline void set_alert(int alert) const {
if (alert_ != nullptr) *alert_ = alert;
}

static inline Result Encode(ClientHelloResult result) {
#ifdef OPENSSL_IS_BORINGSSL

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could simplify this using defines in the same #ifdef that has using Result = to include #define ResultSuccess = ssl_select_cert_success; etc .. then you could eliminate the #ifdef here.

switch (result) {
case ClientHelloResult::kContinue:
return ssl_select_cert_success;
case ClientHelloResult::kRetry:
return ssl_select_cert_retry;
case ClientHelloResult::kFail:
break;
}
return ssl_select_cert_error;
#else
switch (result) {
case ClientHelloResult::kContinue:
return SSL_CLIENT_HELLO_SUCCESS;
case ClientHelloResult::kRetry:
return SSL_CLIENT_HELLO_RETRY;
case ClientHelloResult::kFail:
break;
}
return SSL_CLIENT_HELLO_ERROR;
#endif
}

private:
// Reads a 16-bit length prefixed vector from the front of data. Returns
// nothing if the length does not fit within data.
static inline std::optional<std::span<const uint8_t>> ReadVector16(
std::span<const uint8_t> data) {
if (data.size() < 2) return std::nullopt;
const size_t len = (static_cast<size_t>(data[0]) << 8) | data[1];
if (data.size() < 2 + len) return std::nullopt;
return data.subspan(2, len);
}

Handle handle_;
int* alert_;
};

// Adapts a portable callback into the one the linked TLS library expects,
// e.g. SSL_CTX_set_client_hello_cb(ctx, ClientHelloCallback<Fn>::Invoke,
// nullptr).
template <ClientHelloResult (*Fn)(const ClientHelloContext&)>
struct ClientHelloCallback final {
#ifdef OPENSSL_IS_BORINGSSL
static ssl_select_cert_result_t Invoke(const SSL_CLIENT_HELLO* hello) {
return ClientHelloContext::Encode(Fn(ClientHelloContext(hello)));
}
#else
static int Invoke(SSL* ssl, int* alert, void* arg) {
return ClientHelloContext::Encode(Fn(ClientHelloContext(ssl, alert)));
}
#endif
};

// Registers Fn as the early ClientHello callback on ctx.
template <ClientHelloResult (*Fn)(const ClientHelloContext&)>
inline void SetClientHelloCallback(SSL_CTX* ctx) {
#ifdef OPENSSL_IS_BORINGSSL
SSL_CTX_set_select_certificate_cb(ctx, ClientHelloCallback<Fn>::Invoke);
#else
SSL_CTX_set_client_hello_cb(ctx, ClientHelloCallback<Fn>::Invoke, nullptr);
#endif
}

// Whether protocols, a list in ALPN wire format, contains protocol.
inline bool AlpnListContains(std::span<const uint8_t> protocols,
std::string_view protocol) {
for (size_t n = 0; n < protocols.size();) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I wonder if we should guard against protocols.size() being unreasonably large... someone could send a rather large payload of [1, 'a', 1, 'a', 1, 'a', ... ]. Some kind of defensive bound might be worthwhile.

const size_t len = protocols[n];
if (len == 0 || n + 1 + len > protocols.size()) return false;
if (std::string_view(
reinterpret_cast<const char*>(protocols.data() + n + 1), len) ==
protocol) {
return true;
}
n += 1 + len;
}
return false;
}

inline void SetSelectedProtocol(const unsigned char** out,
unsigned char* outlen,
std::string_view protocol) {
*out = reinterpret_cast<const unsigned char*>(protocol.data());
*outlen = static_cast<unsigned char>(protocol.size());
}

// Selects the protocol to use from the offered and supported lists, both in
// ALPN wire format. Returns nothing when the two lists do not overlap.
inline std::optional<std::string_view> SelectNextProtocol(
std::span<const uint8_t> supported, std::span<const uint8_t> offered) {
if (supported.empty() || offered.empty()) return std::nullopt;
uint8_t* selected = nullptr;
uint8_t selected_len = 0;
if (SSL_select_next_proto(&selected,
&selected_len,
supported.data(),
static_cast<unsigned int>(supported.size()),
offered.data(),
static_cast<unsigned int>(offered.size())) !=
OPENSSL_NPN_NEGOTIATED) {
return std::nullopt;
}
return std::string_view(reinterpret_cast<const char*>(selected),
selected_len);
}

} // namespace node::crypto

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#endif // SRC_CRYPTO_CRYPTO_CLIENT_HELLO_H_
8 changes: 0 additions & 8 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1650,14 +1650,6 @@ void SecureContext::SetNewSessionCallback(NewSessionCb cb) {
SSL_CTX_sess_set_new_cb(ctx_.get(), cb);
}

void SecureContext::SetClientHelloCallback(ClientHelloCb cb) {
#ifdef OPENSSL_IS_BORINGSSL
SSL_CTX_set_select_certificate_cb(ctx_.get(), cb);
#else
SSL_CTX_set_client_hello_cb(ctx_.get(), cb, nullptr);
#endif
}

void SecureContext::SetGetSessionCallback(GetSessionCb cb) {
SSL_CTX_sess_set_get_cb(ctx_.get(), cb);
}
Expand Down
6 changes: 0 additions & 6 deletions src/crypto/crypto_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ class SecureContext final : public BaseObject {
using KeylogCb = void (*)(const SSL*, const char*);
using NewSessionCb = int (*)(SSL*, SSL_SESSION*);
using SelectSNIContextCb = int (*)(SSL*, int*, void*);
#ifdef OPENSSL_IS_BORINGSSL
using ClientHelloCb = ssl_select_cert_result_t (*)(const SSL_CLIENT_HELLO*);
#else
using ClientHelloCb = int (*)(SSL*, int*, void*);
#endif

~SecureContext() override;

Expand Down Expand Up @@ -79,7 +74,6 @@ class SecureContext final : public BaseObject {

ncrypto::SSLPointer CreateSSL();

void SetClientHelloCallback(ClientHelloCb cb);
void SetGetSessionCallback(GetSessionCb cb);
void SetKeylogCallback(KeylogCb cb);
void SetNewSessionCallback(NewSessionCb cb);
Expand Down
Loading
Loading