Skip to content
This repository has been archived by the owner on Aug 11, 2020. It is now read-only.

Commit

Permalink
quic: begin implementing clientHello and cert events
Browse files Browse the repository at this point in the history
The `'clientHello'` and `'cert'` events emit optionally during
the TLS handshake to allow usercode the opportunity to modify
TLS handshake properties and behavior at multiple points through
the handshake process. To maximize performance, these are not
called by default.

PR-URL: #31
  • Loading branch information
jasnell committed Aug 19, 2019
1 parent e304f87 commit d11fc22
Show file tree
Hide file tree
Showing 9 changed files with 497 additions and 21 deletions.
86 changes: 72 additions & 14 deletions lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ const {
IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT,
IDX_QUIC_SESSION_MAX_ACK_DELAY,
IDX_QUIC_SESSION_STATE_CONNECTION_ID_COUNT,
IDX_QUIC_SESSION_STATE_CERT_ENABLED,
IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED,
IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED,
ERR_INVALID_REMOTE_TRANSPORT_PARAMS,
ERR_INVALID_TLS_SESSION_TICKET,
Expand All @@ -121,6 +123,8 @@ const emit = EventEmitter.prototype.emit;

const kAddSession = Symbol('kAddSession');
const kAddStream = Symbol('kAddStream');
const kCert = Symbol('kCert');
const kClientHello = Symbol('kClientHello');
const kContinueBind = Symbol('kContinueBind');
const kContinueConnect = Symbol('kContinueConnect');
const kContinueListen = Symbol('kContinueListen');
Expand Down Expand Up @@ -218,6 +222,14 @@ function onSessionClose(code, family) {
this[owner_symbol].destroy();
}

function onSessionClientHello(alpn, servername, ciphers, callback) {
this[owner_symbol][kClientHello](alpn, servername, ciphers, callback);
}

function onSessionCert(servername, ocsp, callback) {
this[owner_symbol][kCert](servername, ocsp, callback);
}

function onSessionHandshake(
servername,
alpn,
Expand Down Expand Up @@ -307,6 +319,8 @@ setCallbacks({
onSocketClose,
onSocketError,
onSessionReady,
onSessionCert,
onSessionClientHello,
onSessionClose,
onSessionError,
onSessionExtend,
Expand Down Expand Up @@ -421,22 +435,38 @@ function createSecureContext(options, init_cb) {
return sc;
}

function onNewKeylogListener(event) {
if (event !== 'keylog' ||
this[kHandle] === undefined ||
this.listenerCount('keylog') !== 0) {
function onNewListener(event) {
if (this[kHandle] === undefined || this.listenerCount(event) !== 0)
return;

switch (event) {
case 'keylog':
this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 1;
break;
case 'clientHello':
this[kHandle].state[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = 1;
break;
case 'cert':
this[kHandle].state[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 1;
break;
}
this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 1;
}

function onRemoveKeylogListener(event) {
if (event !== 'keylog' ||
this[kHandle] === undefined ||
this.listenerCount('keylog') !== 0) {
function onRemoveListener(event) {
if (this[kHandle] === undefined || this.listenerCount(event) !== 0)
return;

switch (event) {
case 'keylog':
this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 0;
break;
case 'clientHello':
this[kHandle].state[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = 0;
break;
case 'cert':
this[kHandle].state[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 0;
break;
}
this[kHandle].state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = 0;
}

// QuicSocket wraps a UDP socket plus the associated TLS context and QUIC
Expand Down Expand Up @@ -762,6 +792,10 @@ class QuicSocket extends EventEmitter {
return this;
}

get serverSecureContext() {
return this.#serverSecureContext;
}

get address() {
const out = {};
if (this.#state !== kSocketDestroyed) {
Expand Down Expand Up @@ -892,13 +926,21 @@ class QuicSession extends EventEmitter {

constructor(socket, servername) {
super();
this.on('newListener', onNewKeylogListener);
this.on('removeListener', onRemoveKeylogListener);
this.on('newListener', onNewListener);
this.on('removeListener', onRemoveListener);
this.#socket = socket;
socket[kAddSession](this);
this.#servername = servername;
}

[kCert](servername, oscp, callback) {
this.emit(
'cert',
servername,
oscp,
callback.bind(this[kHandle]));
}

[kSetCloseCode](code, family) {
this.#closeCode = code;
this.#closeFamily = family;
Expand Down Expand Up @@ -1061,8 +1103,8 @@ class QuicSession extends EventEmitter {
for (const stream of this.#streams.values())
stream.destroy(error);

this.removeListener('newListener', onNewKeylogListener);
this.removeListener('removeListener', onRemoveKeylogListener);
this.removeListener('newListener', onNewListener);
this.removeListener('removeListener', onRemoveListener);

const handle = this[kHandle];
if (handle !== undefined) {
Expand Down Expand Up @@ -1174,6 +1216,22 @@ class QuicServerSession extends QuicSession {
handle[owner_symbol] = this;
}

[kClientHello](alpn, servername, ciphers, callback) {
const { serverSecureContext } = this.socket;
if (!serverSecureContext)
callback(null, null);
const { context } = serverSecureContext;

this.emit(
'clientHello',
alpn,
servername,
ciphers,
context.getCertificate(),
context.getIssuer(),
callback.bind(this[kHandle]));
}

[kReady]() {
process.nextTick(emit.bind(this, 'ready'));
}
Expand Down
2 changes: 2 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ constexpr size_t kFsStatsBufferLength =
V(quic_on_socket_close_function, v8::Function) \
V(quic_on_socket_error_function, v8::Function) \
V(quic_on_session_ready_function, v8::Function) \
V(quic_on_session_cert_function, v8::Function) \
V(quic_on_session_client_hello_function, v8::Function) \
V(quic_on_session_close_function, v8::Function) \
V(quic_on_session_error_function, v8::Function) \
V(quic_on_session_extend_function, v8::Function) \
Expand Down
6 changes: 0 additions & 6 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,6 @@ struct StackOfXASN1Deleter {
};
using StackOfASN1 = std::unique_ptr<STACK_OF(ASN1_OBJECT), StackOfXASN1Deleter>;

// OPENSSL_free is a macro, so we need a wrapper function.
struct OpenSSLBufferDeleter {
void operator()(char* pointer) const { OPENSSL_free(pointer); }
};
using OpenSSLBuffer = std::unique_ptr<char[], OpenSSLBufferDeleter>;

static const char* const root_certs[] = {
#include "node_root_certs.h" // NOLINT(build/include_order)
};
Expand Down
6 changes: 6 additions & 0 deletions src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ using ECPointPointer = DeleteFnPtr<EC_POINT, EC_POINT_free>;
using ECKeyPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
using DHPointer = DeleteFnPtr<DH, DH_free>;

// OPENSSL_free is a macro, so we need a wrapper function.
struct OpenSSLBufferDeleter {
void operator()(char* pointer) const { OPENSSL_free(pointer); }
};
using OpenSSLBuffer = std::unique_ptr<char[], OpenSSLBufferDeleter>;

extern int VerifyCallback(int preverify_ok, X509_STORE_CTX* ctx);

extern void UseExtraCaCerts(const std::string& file);
Expand Down
22 changes: 22 additions & 0 deletions src/node_quic.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ void QuicSetCallbacks(const FunctionCallbackInfo<Value>& args) {
SETFUNCTION("onSocketClose", socket_close);
SETFUNCTION("onSocketError", socket_error);
SETFUNCTION("onSessionReady", session_ready);
SETFUNCTION("onSessionCert", session_cert);
SETFUNCTION("onSessionClientHello", session_client_hello);
SETFUNCTION("onSessionClose", session_close);
SETFUNCTION("onSessionError", session_error);
SETFUNCTION("onSessionExtend", session_extend);
Expand All @@ -71,6 +73,23 @@ void QuicALPNVersion(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(OneByteString(env->isolate(), NGTCP2_ALPN_H3));
}

inline int Client_Hello_CB(
SSL* ssl,
int* tls_alert,
void* arg) {
QuicSession* session = static_cast<QuicSession*>(SSL_get_app_data(ssl));
int ret = session->OnClientHello();
switch (ret) {
case 0:
return 1;
case -1:
return -1;
default:
*tls_alert = ret;
return 0;
}
}

int ALPN_Select_Proto_CB(SSL* ssl,
const unsigned char** out,
unsigned char* outlen,
Expand Down Expand Up @@ -270,6 +289,7 @@ void QuicInitSecureContext(const FunctionCallbackInfo<Value>& args) {
SSL_CTX_set_default_verify_paths(**sc);
SSL_CTX_set_max_early_data(**sc, std::numeric_limits<uint32_t>::max());
SSL_CTX_set_alpn_select_cb(**sc, ALPN_Select_Proto_CB, nullptr);
SSL_CTX_set_client_hello_cb(**sc, Client_Hello_CB, nullptr);
CHECK_EQ(
SSL_CTX_add_custom_ext(
**sc,
Expand Down Expand Up @@ -381,6 +401,8 @@ void Initialize(Local<Object> target,
NODE_DEFINE_CONSTANT(constants, ERR_INVALID_REMOTE_TRANSPORT_PARAMS);
NODE_DEFINE_CONSTANT(constants, ERR_INVALID_TLS_SESSION_TICKET);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_CONNECTION_ID_COUNT);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_CERT_ENABLED);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED);
NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED);
NODE_DEFINE_CONSTANT(constants, MAX_RETRYTOKEN_EXPIRATION);
NODE_DEFINE_CONSTANT(constants, MIN_RETRYTOKEN_EXPIRATION);
Expand Down
115 changes: 115 additions & 0 deletions src/node_quic_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,10 @@ inline void LogSecret(
}
}

inline int CertCB(SSL* ssl, void* arg) {
QuicSession* session = static_cast<QuicSession*>(arg);
return session->OnCert();
}

// KeyCB provides a hook into the keying process of the TLS handshake,
// triggering registration of the keys associated with the TLS session.
Expand Down Expand Up @@ -992,6 +996,12 @@ inline int DoTLSHandshake(SSL* ssl) {
switch (err) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
// For the next two, the handshake has been suspended but
// the data was otherwise successfully read, so return 0
// here but the handshake won't continue until we trigger
// things on our side.
case SSL_ERROR_WANT_CLIENT_HELLO_CB:
case SSL_ERROR_WANT_X509_LOOKUP:
return 0;
case SSL_ERROR_SSL:
return NGTCP2_ERR_CRYPTO;
Expand All @@ -1012,6 +1022,12 @@ inline int DoTLSReadEarlyData(SSL* ssl) {
switch (code) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
// For the next two, the handshake has been suspended but
// the data was otherwise successfully read, so return 0
// here but the handshake won't continue until we trigger
// things on our side.
case SSL_ERROR_WANT_CLIENT_HELLO_CB:
case SSL_ERROR_WANT_X509_LOOKUP:
return 0;
case SSL_ERROR_SSL:
return NGTCP2_ERR_CRYPTO;
Expand All @@ -1030,6 +1046,105 @@ inline int DoTLSReadEarlyData(SSL* ssl) {
return 0;
}

inline crypto::OpenSSLBuffer GetClientHelloRandom(SSL* ssl) {
const unsigned char* buf;
SSL_client_hello_get0_random(ssl, &buf);
return crypto::OpenSSLBuffer(
const_cast<char*>(reinterpret_cast<const char*>(buf)));
}

inline crypto::OpenSSLBuffer GetClientHelloSessionID(SSL* ssl) {
const unsigned char* buf;
SSL_client_hello_get0_session_id(ssl, &buf);
return crypto::OpenSSLBuffer(
const_cast<char*>(reinterpret_cast<const char*>(buf)));
}

inline v8::Local<v8::Array> GetClientHelloCiphers(
Environment* env,
SSL* ssl) {
v8::Local<v8::Array> ciphers_array;
const unsigned char* buf;
size_t len = SSL_client_hello_get0_ciphers(ssl, &buf);
if (len == 0)
return ciphers_array;

ciphers_array = v8::Array::New(env->isolate(), len / 2);
size_t pos = 0;
for (size_t n = 0; n < len; n += 2) {
auto cipher = SSL_CIPHER_find(ssl, buf);
buf += 2;
const char* cipher_name = SSL_CIPHER_get_name(cipher);
const char* cipher_version = SSL_CIPHER_get_version(cipher);
v8::Local<v8::Object> obj = v8::Object::New(env->isolate());
USE(obj->Set(
env->context(),
env->name_string(),
OneByteString(env->isolate(), cipher_name)));
USE(obj->Set(
env->context(),
env->version_string(),
OneByteString(env->isolate(), cipher_version)));
USE(ciphers_array->Set(env->context(), pos++, obj));
}

return ciphers_array;
}

inline crypto::OpenSSLBuffer GetClientHelloCompressionMethods(SSL* ssl) {
const unsigned char* buf;
SSL_client_hello_get0_compression_methods(ssl, &buf);
return crypto::OpenSSLBuffer(
const_cast<char*>(reinterpret_cast<const char*>(buf)));
}

inline const char* GetClientHelloServerName(SSL* ssl) {
const unsigned char* buf;
size_t len;
size_t rem;

if (!SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &buf, &rem) ||
rem <= 2)
return nullptr;

len = (*(buf++) << 8);
len += *(buf++);
if (len + 2 != rem)
return nullptr;
rem = len;

if (rem == 0 || *buf++ != TLSEXT_NAMETYPE_host_name)
return nullptr;
rem--;
if (rem <= 2)
return nullptr;
len = (*(buf++) << 8);
len += *(buf++);
if (len + 2 > rem)
return nullptr;
rem = len;
return reinterpret_cast<const char*>(buf);
}

inline const char* GetClientHelloALPN(SSL* ssl) {
const unsigned char* buf;
size_t len;
size_t rem;

if (!SSL_client_hello_get0_ext(
ssl,
TLSEXT_TYPE_application_layer_protocol_negotiation,
&buf, &rem) || rem < 2) {
return nullptr;
}

len = (buf[0] << 8) | buf[1];
if (len + 2 != rem)
return nullptr;
buf += 3;
return reinterpret_cast<const char*>(buf);
}

} // namespace quic
} // namespace node

Expand Down
Loading

0 comments on commit d11fc22

Please sign in to comment.