tls: drop hand-rolled TLS client hello parser

This existed for 'resumeSession', which needed to do an async lookup
though SSL_CTX_sess_set_get_cb is sync-only. Nowadays both OpenSSL &
BoringSSL have an early ClientHello callback for suspend/resume to
handle this properly, so it was redundant, in addition to being
complicated and generally a bit fragile & scary.

This PR switches to use the modern OpenSSL/BoringSSL mechanisms for this
and drops the client hello parser & related infrastructure completely.

In addition, there's a new test here, covering a fixed bug: the hello
parser silently dropped fragmented hellos, which we now do handle
correctly.

Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64827
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
This commit is contained in:
Tim Perry authored and Node.js GitHub Bot committed 2026-08-04 12:33:00 +00:00
1 parent 72768c7ff6
commit 5b940dd124
14 files changed
+284 -773

No files matched your search

+4 -5
View File
@@ -262,8 +262,8 @@ function loadSession(hello) {
return owner.destroy(new ERR_SOCKET_CLOSED());
owner._handle.loadSession(session);
// Session is loaded. End the parser to allow handshaking to continue.
owner._handle.endParser();
// Session is loaded. Let the handshake continue.
owner._handle.clientHelloDone();
}
if (hello.sessionId.length <= 0 ||
@@ -281,8 +281,8 @@ function loadSession(hello) {
// Sessions with tickets can be resumed directly from the ticket, no server
// session storage is necessary.
// Without a call to a resumeSession listener, a session will never be
// loaded, so end the parser to allow handshaking to continue.
owner._handle.endParser();
// loaded, so let the handshake continue.
owner._handle.clientHelloDone();
}
}
@@ -970,7 +970,6 @@ TLSSocket.prototype._init = function(socket, wrap) {
if (this.server) {
if (this.server.listenerCount('resumeSession') > 0 ||
this.server.listenerCount('newSession') > 0) {
// Also starts the client hello parser as a side effect.
ssl.enableSessionCallbacks();
}
if (this.server.listenerCount('OCSPRequest') > 0)
-52
View File
@@ -417,7 +417,6 @@
'src/crypto/crypto_rsa.cc',
'src/crypto/crypto_spkac.cc',
'src/crypto/crypto_util.cc',
'src/crypto/crypto_clienthello.cc',
'src/crypto/crypto_dh.cc',
'src/crypto/crypto_hash.cc',
'src/crypto/crypto_keys.cc',
@@ -427,7 +426,6 @@
'src/crypto/crypto_x509.cc',
'src/crypto/crypto_argon2.h',
'src/crypto/crypto_bio.h',
'src/crypto/crypto_clienthello-inl.h',
'src/crypto/crypto_dh.h',
'src/crypto/crypto_hmac.h',
'src/crypto/crypto_kmac.h',
@@ -443,7 +441,6 @@
'src/crypto/crypto_keygen.h',
'src/crypto/crypto_scrypt.h',
'src/crypto/crypto_tls.h',
'src/crypto/crypto_clienthello.h',
'src/crypto/crypto_context.h',
'src/crypto/crypto_ec.h',
'src/crypto/crypto_pqc.h',
@@ -473,7 +470,6 @@
'src/tracing/trace_event_legacy.h',
],
'node_cctest_openssl_sources': [
'test/cctest/test_crypto_clienthello.cc',
'test/cctest/test_node_crypto.cc',
'test/cctest/test_node_crypto_env.cc',
],
@@ -1316,54 +1312,6 @@
}],
],
}, # fuzz_env
{ # fuzz_ClientHelloParser.cc
'target_name': 'fuzz_ClientHelloParser',
'type': 'executable',
'dependencies': [
'<(node_lib_target_name)',
],
'includes': [
'node.gypi'
],
'include_dirs': [
'src',
'tools/msvs/genfiles',
'deps/v8/include',
'deps/cares/include',
'deps/uv/include',
'test/cctest',
],
'defines': [
'NODE_ARCH="<(target_arch)"',
'NODE_PLATFORM="<(OS)"',
'NODE_WANT_INTERNALS=1',
],
'sources': [
'test/fuzzers/fuzz_ClientHelloParser.cc',
],
'conditions': [
[ 'node_shared_hdr_histogram=="false"', {
'dependencies': [
'deps/histogram/histogram.gyp:histogram',
],
}],
[ 'node_shared_uvwasi=="false"', {
'dependencies': [ 'deps/uvwasi/uvwasi.gyp:uvwasi' ],
'include_dirs': [ 'deps/uvwasi/include' ],
}],
['OS=="linux" or OS=="openharmony"', {
'ldflags': [ '-fsanitize=fuzzer' ]
}],
# Ensure that ossfuzz flag has been set and that we are on Linux
[ 'OS not in "linux openharmony" or ossfuzz!="true"', {
'type': 'none',
}],
# Avoid excessive LTO
['enable_lto=="true"', {
'ldflags': [ '-fno-lto' ],
}],
],
}, # fuzz_ClientHelloParser.cc
{ # fuzz_strings
'target_name': 'fuzz_strings',
'type': 'executable',
+20 -21
View File
@@ -30,27 +30,26 @@ throughout the rest of the code.
The rest of the files are structured by their function, as detailed in the
following table:
| File (\*.h/\*.cc) | Description |
| -------------------- | -------------------------------------------------------------------------- |
| `crypto_aes` | AES Cipher support. |
| `crypto_argon2` | Argon2 key / bit generation implementation. |
| `crypto_cipher` | General Encryption/Decryption utilities. |
| `crypto_clienthello` | TLS/SSL client hello parser implementation. Used during SSL/TLS handshake. |
| `crypto_context` | Implementation of the `SecureContext` object. |
| `crypto_dh` | Diffie-Hellman Key Agreement implementation. |
| `crypto_dsa` | DSA (Digital Signature) Key Generation functions. |
| `crypto_ec` | Elliptic-curve cryptography implementation. |
| `crypto_hash` | Basic hash (e.g. SHA-256) functions. |
| `crypto_hkdf` | HKDF (Key derivation) implementation. |
| `crypto_hmac` | HMAC implementations. |
| `crypto_keys` | Utilities for using and generating secret, private, and public keys. |
| `crypto_pbkdf2` | PBKDF2 key / bit generation implementation. |
| `crypto_rsa` | RSA Key Generation functions. |
| `crypto_scrypt` | Scrypt key / bit generation implementation. |
| `crypto_sig` | General digital signature and verification utilities. |
| `crypto_spkac` | Netscape SPKAC certificate utilities. |
| `crypto_ssl` | Implementation of the `SSLWrap` object. |
| `crypto_timing` | Implementation of the TimingSafeEqual. |
| File (\*.h/\*.cc) | Description |
| ----------------- | -------------------------------------------------------------------- |
| `crypto_aes` | AES Cipher support. |
| `crypto_argon2` | Argon2 key / bit generation implementation. |
| `crypto_cipher` | General Encryption/Decryption utilities. |
| `crypto_context` | Implementation of the `SecureContext` object. |
| `crypto_dh` | Diffie-Hellman Key Agreement implementation. |
| `crypto_dsa` | DSA (Digital Signature) Key Generation functions. |
| `crypto_ec` | Elliptic-curve cryptography implementation. |
| `crypto_hash` | Basic hash (e.g. SHA-256) functions. |
| `crypto_hkdf` | HKDF (Key derivation) implementation. |
| `crypto_hmac` | HMAC implementations. |
| `crypto_keys` | Utilities for using and generating secret, private, and public keys. |
| `crypto_pbkdf2` | PBKDF2 key / bit generation implementation. |
| `crypto_rsa` | RSA Key Generation functions. |
| `crypto_scrypt` | Scrypt key / bit generation implementation. |
| `crypto_sig` | General digital signature and verification utilities. |
| `crypto_spkac` | Netscape SPKAC certificate utilities. |
| `crypto_ssl` | Implementation of the `SSLWrap` object. |
| `crypto_timing` | Implementation of the TimingSafeEqual. |
When new crypto protocols are added, they will be added into their own
`crypto_` `*.h` and `*.cc` files.
-90
View File
@@ -1,90 +0,0 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
#ifndef SRC_CRYPTO_CRYPTO_CLIENTHELLO_INL_H_
#define SRC_CRYPTO_CRYPTO_CLIENTHELLO_INL_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "crypto/crypto_clienthello.h"
#include "util.h"
namespace node {
namespace crypto {
inline ClientHelloParser::ClientHelloParser()
: state_(kEnded),
onhello_cb_(nullptr),
onend_cb_(nullptr),
cb_arg_(nullptr) {
Reset();
}
inline void ClientHelloParser::Reset() {
frame_len_ = 0;
body_offset_ = 0;
extension_offset_ = 0;
session_size_ = 0;
session_id_ = nullptr;
tls_ticket_size_ = -1;
tls_ticket_ = nullptr;
servername_size_ = 0;
servername_ = nullptr;
}
inline void ClientHelloParser::Start(ClientHelloParser::OnHelloCb onhello_cb,
ClientHelloParser::OnEndCb onend_cb,
void* cb_arg) {
if (!IsEnded())
return;
Reset();
CHECK_NOT_NULL(onhello_cb);
state_ = kWaiting;
onhello_cb_ = onhello_cb;
onend_cb_ = onend_cb;
cb_arg_ = cb_arg;
}
inline void ClientHelloParser::End() {
if (state_ == kEnded)
return;
state_ = kEnded;
if (onend_cb_ != nullptr) {
onend_cb_(cb_arg_);
onend_cb_ = nullptr;
}
}
inline bool ClientHelloParser::IsEnded() const {
return state_ == kEnded;
}
inline bool ClientHelloParser::IsPaused() const {
return state_ == kPaused;
}
} // namespace crypto
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CRYPTO_CRYPTO_CLIENTHELLO_INL_H_
-238
View File
@@ -1,238 +0,0 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "crypto/crypto_clienthello.h" // NOLINT(build/include_inline)
#include "crypto/crypto_clienthello-inl.h"
namespace node {
namespace crypto {
void ClientHelloParser::Parse(const uint8_t* data, size_t avail) {
switch (state_) {
case kWaiting:
if (!ParseRecordHeader(data, avail))
break;
[[fallthrough]];
case kTLSHeader:
ParseHeader(data, avail);
break;
case kPaused:
// Just nop
case kEnded:
// Already ended, just ignore it
break;
default:
break;
}
}
bool ClientHelloParser::ParseRecordHeader(const uint8_t* data, size_t avail) {
// >= 5 bytes for header parsing
if (avail < 5)
return false;
if (data[0] == kChangeCipherSpec ||
data[0] == kAlert ||
data[0] == kHandshake ||
data[0] == kApplicationData) {
frame_len_ = (data[3] << 8) + data[4];
state_ = kTLSHeader;
body_offset_ = 5;
} else {
End();
return false;
}
// Sanity check (too big frame, or too small)
// Let OpenSSL handle it
if (frame_len_ >= kMaxTLSFrameLen) {
End();
return false;
}
return true;
}
void ClientHelloParser::ParseHeader(const uint8_t* data, size_t avail) {
ClientHello hello;
// We need at least six bytes (one byte for kClientHello, three bytes for the
// length of the handshake message, and two bytes for the protocol version).
// If the client sent a frame that suggests a smaller ClientHello, give up.
if (frame_len_ < 6) return End();
// >= 5 + frame size bytes for frame parsing
if (body_offset_ + frame_len_ > avail)
return;
// Check hello protocol version. Protocol tuples that we know about:
//
// (3,1) TLS v1.0
// (3,2) TLS v1.1
// (3,3) TLS v1.2
//
// Note that TLS v1.3 uses a TLS v1.2 handshake so requires no specific
// support here.
if (data[body_offset_ + 4] != 0x03 ||
data[body_offset_ + 5] < 0x01 ||
data[body_offset_ + 5] > 0x03) {
return End();
}
if (data[body_offset_] == kClientHello) {
if (state_ == kTLSHeader) {
if (!ParseTLSClientHello(data, avail))
return End();
} else {
// We couldn't get here, but whatever
return End();
}
// Check if we overflowed (do not reply with any private data)
if (session_id_ == nullptr ||
session_size_ > 32 ||
session_id_ + session_size_ > data + avail) {
return End();
}
}
state_ = kPaused;
hello.session_id_ = session_id_;
hello.session_size_ = session_size_;
hello.has_ticket_ = tls_ticket_ != nullptr && tls_ticket_size_ != 0;
hello.servername_ = servername_;
hello.servername_size_ = static_cast<uint8_t>(servername_size_);
onhello_cb_(cb_arg_, hello);
}
void ClientHelloParser::ParseExtension(const uint16_t type,
const uint8_t* data,
size_t len) {
// NOTE: In case of anything we're just returning back, ignoring the problem.
// That's because we're heavily relying on OpenSSL to solve any problem with
// incoming data.
switch (type) {
case kServerName:
{
if (len < 2)
return;
uint32_t server_names_len = (data[0] << 8) + data[1];
if (server_names_len + 2 > len)
return;
for (size_t offset = 2; offset < 2 + server_names_len; ) {
if (offset + 3 > len)
return;
uint8_t name_type = data[offset];
if (name_type != kServernameHostname)
return;
uint16_t name_len = (data[offset + 1] << 8) + data[offset + 2];
offset += 3;
if (offset + name_len > len)
return;
servername_ = data + offset;
servername_size_ = name_len;
offset += name_len;
}
}
break;
case kTLSSessionTicket:
tls_ticket_size_ = len;
tls_ticket_ = data + len;
break;
default:
// Ignore
break;
}
}
bool ClientHelloParser::ParseTLSClientHello(const uint8_t* data, size_t avail) {
const uint8_t* body;
// Skip frame header, hello header, protocol version and random data
size_t session_offset = body_offset_ + 4 + 2 + 32;
if (session_offset + 1 >= avail)
return false;
body = data + session_offset;
session_size_ = *body;
session_id_ = body + 1;
size_t cipher_offset = session_offset + 1 + session_size_;
// Session OOB failure
if (cipher_offset + 1 >= avail)
return false;
uint16_t cipher_len =
(data[cipher_offset] << 8) + data[cipher_offset + 1];
size_t comp_offset = cipher_offset + 2 + cipher_len;
// Cipher OOB failure
if (comp_offset >= avail)
return false;
uint8_t comp_len = data[comp_offset];
size_t extension_offset = comp_offset + 1 + comp_len;
// Compression OOB failure
if (extension_offset > avail)
return false;
// No extensions present
if (extension_offset == avail)
return true;
size_t ext_off = extension_offset + 2;
// Parse known extensions
while (ext_off < avail) {
// Extension OOB
if (ext_off + 4 > avail)
return false;
uint16_t ext_type = (data[ext_off] << 8) + data[ext_off + 1];
uint16_t ext_len = (data[ext_off + 2] << 8) + data[ext_off + 3];
ext_off += 4;
// Extension OOB
if (ext_off + ext_len > avail)
return false;
ParseExtension(ext_type,
data + ext_off,
ext_len);
ext_off += ext_len;
}
// Extensions OOB failure
if (ext_off > avail)
return false;
return true;
}
} // namespace crypto
} // namespace node
-131
View File
@@ -1,131 +0,0 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
#ifndef SRC_CRYPTO_CRYPTO_CLIENTHELLO_H_
#define SRC_CRYPTO_CRYPTO_CLIENTHELLO_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <cstddef> // size_t
#include <cstdint>
namespace node {
namespace crypto {
// Parse the client hello so we can do async session resumption. OpenSSL's
// session resumption uses synchronous callbacks, see SSL_CTX_sess_set_get_cb
// and get_session_cb.
//
// TLS1.3 handshakes masquerade as TLS1.2 session resumption, and to do this,
// they always include a session_id in the ClientHello, making up a bogus value
// if necessary. The parser can't know if its a bogus id, and will cause a
// 'newSession' event to be emitted. This should do no harm, the id won't be
// found, and the handshake will continue.
class ClientHelloParser {
public:
inline ClientHelloParser();
class ClientHello {
public:
inline uint8_t session_size() const { return session_size_; }
inline const uint8_t* session_id() const { return session_id_; }
inline bool has_ticket() const { return has_ticket_; }
inline uint8_t servername_size() const { return servername_size_; }
inline const uint8_t* servername() const { return servername_; }
private:
uint8_t session_size_;
const uint8_t* session_id_;
bool has_ticket_;
uint8_t servername_size_;
const uint8_t* servername_;
friend class ClientHelloParser;
};
typedef void (*OnHelloCb)(void* arg, const ClientHello& hello);
typedef void (*OnEndCb)(void* arg);
void Parse(const uint8_t* data, size_t avail);
inline void Reset();
inline void Start(OnHelloCb onhello_cb, OnEndCb onend_cb, void* cb_arg);
inline void End();
inline bool IsPaused() const;
inline bool IsEnded() const;
private:
static const size_t kMaxTLSFrameLen = 16 * 1024 + 5;
static const size_t kMaxSSLExFrameLen = 32 * 1024;
static const uint8_t kServernameHostname = 0;
static const size_t kMinStatusRequestSize = 5;
enum ParseState {
kWaiting,
kTLSHeader,
kPaused,
kEnded
};
enum FrameType {
kChangeCipherSpec = 20,
kAlert = 21,
kHandshake = 22,
kApplicationData = 23,
kOther = 255
};
enum HandshakeType {
kClientHello = 1
};
enum ExtensionType {
kServerName = 0,
kTLSSessionTicket = 35
};
bool ParseRecordHeader(const uint8_t* data, size_t avail);
void ParseHeader(const uint8_t* data, size_t avail);
void ParseExtension(const uint16_t type,
const uint8_t* data,
size_t len);
bool ParseTLSClientHello(const uint8_t* data, size_t avail);
ParseState state_;
OnHelloCb onhello_cb_;
OnEndCb onend_cb_;
void* cb_arg_;
size_t frame_len_ = 0;
size_t body_offset_ = 0;
size_t extension_offset_ = 0;
uint8_t session_size_ = 0;
const uint8_t* session_id_ = nullptr;
uint16_t servername_size_ = 0;
const uint8_t* servername_ = nullptr;
uint16_t tls_ticket_size_ = -1;
const uint8_t* tls_ticket_ = nullptr;
};
} // namespace crypto
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CRYPTO_CRYPTO_CLIENTHELLO_H_
+8
View File
@@ -1689,6 +1689,14 @@ 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);
}
+6
View File
@@ -35,6 +35,11 @@ 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;
@@ -74,6 +79,7 @@ class SecureContext final : public BaseObject {
ncrypto::SSLPointer CreateSSL();
void SetClientHelloCallback(ClientHelloCb cb);
void SetGetSessionCallback(GetSessionCb cb);
void SetKeylogCallback(KeylogCb cb);
void SetNewSessionCallback(NewSessionCb cb);
+99 -80
View File
@@ -23,7 +23,6 @@
#include <cstdio>
#include "async_wrap-inl.h"
#include "crypto/crypto_bio.h"
#include "crypto/crypto_clienthello-inl.h"
#include "crypto/crypto_common.h"
#include "crypto/crypto_context.h"
#include "crypto/crypto_util.h"
@@ -103,42 +102,43 @@ SSL_SESSION* GetSessionCallback(
return w->ReleaseSession();
}
void OnClientHello(
void* arg,
const ClientHelloParser::ClientHello& hello) {
TLSWrap* w = static_cast<TLSWrap*>(arg);
Environment* env = w->env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
// The TLS library invokes this before version negotiation and before session
// or ticket resumption, which makes async session lookup possible. If required
// the handshake is suspended here and resumed once JS has answered.
#ifdef OPENSSL_IS_BORINGSSL
ssl_select_cert_result_t EarlyClientHelloCallback(const SSL_CLIENT_HELLO* ch) {
TLSWrap* w = static_cast<TLSWrap*>(SSL_get_app_data(ch->ssl));
if (!w->should_suspend_for_client_hello()) return ssl_select_cert_success;
Local<Object> hello_obj = Object::New(env->isolate());
Local<String> servername = (hello.servername() == nullptr)
? String::Empty(env->isolate())
: OneByteString(env->isolate(),
hello.servername(),
hello.servername_size());
Local<Object> buf =
Buffer::Copy(
env,
reinterpret_cast<const char*>(hello.session_id()),
hello.session_size()).FromMaybe(Local<Object>());
const uint8_t* ext;
size_t ext_len;
bool has_ticket = SSL_early_callback_ctx_extension_get(
ch, TLSEXT_TYPE_session_ticket, &ext, &ext_len) &&
ext_len > 0;
if ((buf.IsEmpty() ||
hello_obj->Set(env->context(), env->session_id_string(), buf)
.IsNothing()) ||
hello_obj->Set(env->context(), env->servername_string(), servername)
.IsNothing() ||
hello_obj
->Set(env->context(),
env->tls_ticket_string(),
Boolean::New(env->isolate(), hello.has_ticket()))
.IsNothing()) {
return;
}
Local<Value> argv[] = { hello_obj };
w->MakeCallback(env->onclienthello_string(), arraysize(argv), argv);
return w->OnEarlyClientHello(ch->session_id, ch->session_id_len, has_ticket)
? ssl_select_cert_success
: ssl_select_cert_retry;
}
#else
int EarlyClientHelloCallback(SSL* s, int* al, void* arg) {
TLSWrap* w = static_cast<TLSWrap*>(SSL_get_app_data(s));
if (!w->should_suspend_for_client_hello()) return SSL_CLIENT_HELLO_SUCCESS;
const unsigned char* session_id;
size_t session_id_len = SSL_client_hello_get0_session_id(s, &session_id);
const unsigned char* ext;
size_t ext_len;
bool has_ticket = SSL_client_hello_get0_ext(
s, TLSEXT_TYPE_session_ticket, &ext, &ext_len) == 1 &&
ext_len > 0;
return w->OnEarlyClientHello(session_id, session_id_len, has_ticket)
? SSL_CLIENT_HELLO_SUCCESS
: SSL_CLIENT_HELLO_RETRY;
}
#endif
void KeylogCallback(const SSL* s, const char* line) {
TLSWrap* w = static_cast<TLSWrap*>(SSL_get_app_data(s));
@@ -425,6 +425,7 @@ TLSWrap::TLSWrap(Environment* env,
ssl_ = sc_->CreateSSL();
CHECK(ssl_);
sc_->SetClientHelloCallback(EarlyClientHelloCallback);
sc_->SetGetSessionCallback(GetSessionCallback);
sc_->SetNewSessionCallback(NewSessionCallback);
@@ -473,6 +474,61 @@ void TLSWrap::NewSessionDoneCb() {
Cycle();
}
// N.b. TLS1.3 ClientHellos carry a fake legacy_session_id (middlebox compat),
// and so emit spurious 'resumeSession'/'newSession' events here.
bool TLSWrap::OnEarlyClientHello(const unsigned char* session_id,
size_t session_id_len,
bool has_ticket) {
if (!hello_emitted_) {
hello_emitted_ = true;
Debug(this, "Scheduling onclienthello");
// The hello data is only valid inside the library callback, and JS must
// not run while we are on its stack: a handler that synchronously wrote
// to the socket would re-enter SSL mid-handshake. Copy what we need and
// emit from a fresh stack instead.
std::vector<unsigned char> id(session_id, session_id + session_id_len);
BaseObjectPtr<TLSWrap> strong_ref{this};
env()->SetImmediate(
[this, strong_ref, id = std::move(id), has_ticket](Environment* env) {
if (ssl_) EmitClientHello(id, has_ticket);
});
}
return hello_answered_;
}
void TLSWrap::EmitClientHello(const std::vector<unsigned char>& session_id,
bool has_ticket) {
Debug(this, "Emitting onclienthello");
Environment* env = this->env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Local<Object> hello_obj = Object::New(env->isolate());
Local<Object> buf =
Buffer::Copy(env,
reinterpret_cast<const char*>(session_id.data()),
session_id.size())
.FromMaybe(Local<Object>());
if ((buf.IsEmpty() ||
hello_obj->Set(env->context(), env->session_id_string(), buf)
.IsNothing()) ||
hello_obj
->Set(env->context(),
env->tls_ticket_string(),
Boolean::New(env->isolate(), has_ticket))
.IsNothing()) {
// Continue the handshake unresumed rather than leaving it suspended.
hello_answered_ = true;
Cycle();
return;
}
Local<Value> argv[] = {hello_obj};
MakeCallback(env->onclienthello_string(), arraysize(argv), argv);
}
void TLSWrap::InitSSL() {
// Initialize SSL – OpenSSL takes ownership of these.
enc_in_ = NodeBIO::New(env()).release();
@@ -641,12 +697,6 @@ void TLSWrap::SSLInfoCallback(const SSL* ssl_, int where, int ret) {
void TLSWrap::EncOut() {
Debug(this, "Trying to write encrypted output");
// Ignore cycling data if ClientHello wasn't yet parsed
if (!hello_parser_.IsEnded()) {
Debug(this, "Returning from EncOut(), hello_parser_ active");
return;
}
// Write in progress
if (write_size_ != 0) {
Debug(this, "Returning from EncOut(), write currently in progress");
@@ -785,11 +835,6 @@ void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) {
void TLSWrap::ClearOut() {
Debug(this, "Trying to read cleartext output");
// Ignore cycling data if ClientHello wasn't yet parsed
if (!hello_parser_.IsEnded()) {
Debug(this, "Returning from ClearOut(), hello_parser_ active");
return;
}
// No reads after EOF
if (eof_) {
@@ -912,11 +957,6 @@ void TLSWrap::ClearOut() {
void TLSWrap::ClearIn() {
Debug(this, "Trying to write cleartext input");
// Ignore cycling data if ClientHello wasn't yet parsed
if (!hello_parser_.IsEnded()) {
Debug(this, "Returning from ClearIn(), hello_parser_ active");
return;
}
if (ssl_ == nullptr) {
Debug(this, "Returning from ClearIn(), ssl_ == nullptr");
@@ -1181,20 +1221,7 @@ void TLSWrap::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
// Commit the amount of data actually read into the peeked/allocated buffer
// from the underlying stream.
NodeBIO* enc_in = NodeBIO::FromBIO(enc_in_);
enc_in->Commit(nread);
// Parse ClientHello first, if we need to. It's only parsed if session event
// listeners are used on the server side. "ended" is the initial state, so
// can mean parsing was never started, or that parsing is finished. Either
// way, ended means we can give the buffered data to SSL.
if (!hello_parser_.IsEnded()) {
size_t avail = 0;
uint8_t* data = reinterpret_cast<uint8_t*>(enc_in->Peek(&avail));
CHECK_IMPLIES(data == nullptr, avail == 0);
Debug(this, "Passing %zu bytes to the hello parser", avail);
return hello_parser_.Parse(data, avail);
}
NodeBIO::FromBIO(enc_in_)->Commit(nread);
// Cycle OpenSSL's state
Cycle();
@@ -1253,15 +1280,6 @@ void TLSWrap::EnableSessionCallbacks(const FunctionCallbackInfo<Value>& args) {
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
CHECK_NOT_NULL(wrap->ssl_);
wrap->enable_session_callbacks();
// Clients don't use the HelloParser.
if (wrap->is_client())
return;
NodeBIO::FromBIO(wrap->enc_in_)->set_initial(kMaxHelloLength);
wrap->hello_parser_.Start(OnClientHello,
OnClientHelloParseEnd,
wrap);
}
void TLSWrap::EnableKeylogCallback(const FunctionCallbackInfo<Value>& args) {
@@ -1336,7 +1354,7 @@ void TLSWrap::Destroy() {
void TLSWrap::EnableCertCb(const FunctionCallbackInfo<Value>& args) {
TLSWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
wrap->WaitForCertCb(OnClientHelloParseEnd, wrap);
wrap->WaitForCertCb(ResumeAfterCertCb, wrap);
}
void TLSWrap::WaitForCertCb(CertCb cb, void* arg) {
@@ -1344,9 +1362,9 @@ void TLSWrap::WaitForCertCb(CertCb cb, void* arg) {
cert_cb_arg_ = arg;
}
void TLSWrap::OnClientHelloParseEnd(void* arg) {
void TLSWrap::ResumeAfterCertCb(void* arg) {
TLSWrap* c = static_cast<TLSWrap*>(arg);
Debug(c, "OnClientHelloParseEnd()");
Debug(c, "ResumeAfterCertCb()");
c->Cycle();
}
@@ -2033,10 +2051,11 @@ void TLSWrap::ExportKeyingMaterial(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(buffer);
}
void TLSWrap::EndParser(const FunctionCallbackInfo<Value>& args) {
void TLSWrap::ClientHelloDone(const FunctionCallbackInfo<Value>& args) {
TLSWrap* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
w->hello_parser_.End();
w->hello_answered_ = true;
w->Cycle();
}
void TLSWrap::Renegotiate(const FunctionCallbackInfo<Value>& args) {
@@ -2214,10 +2233,10 @@ void TLSWrap::Initialize(
t->Inherit(AsyncWrap::GetConstructorTemplate(env));
SetProtoMethod(isolate, t, "certCbDone", CertCbDone);
SetProtoMethod(isolate, t, "clientHelloDone", ClientHelloDone);
SetProtoMethod(isolate, t, "destroySSL", DestroySSL);
SetProtoMethod(isolate, t, "enableCertCb", EnableCertCb);
SetProtoMethod(isolate, t, "enableALPNCb", EnableALPNCb);
SetProtoMethod(isolate, t, "endParser", EndParser);
SetProtoMethod(isolate, t, "enableKeylogCallback", EnableKeylogCallback);
SetProtoMethod(isolate, t, "enableSessionCallbacks", EnableSessionCallbacks);
SetProtoMethod(isolate, t, "enableTrace", EnableTrace);
@@ -2285,10 +2304,10 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetWriteQueueSize);
registry->Register(CertCbDone);
registry->Register(ClientHelloDone);
registry->Register(DestroySSL);
registry->Register(EnableCertCb);
registry->Register(EnableALPNCb);
registry->Register(EndParser);
registry->Register(EnableKeylogCallback);
registry->Register(EnableSessionCallbacks);
registry->Register(EnableTrace);
+19 -7
View File
@@ -25,7 +25,6 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "crypto/crypto_context.h"
#include "crypto/crypto_clienthello.h"
#include "async_wrap.h"
#include "stream_wrap.h"
@@ -66,6 +65,11 @@ class TLSWrap : public AsyncWrap,
inline bool is_cert_cb_running() const { return cert_cb_running_; }
inline bool is_waiting_cert_cb() const { return cert_cb_ != nullptr; }
inline bool has_session_callbacks() const { return session_callbacks_; }
// We need to suspend the ClientHello only for server session id
// callbacks, and only on the first pass.
inline bool should_suspend_for_client_hello() const {
return is_server() && session_callbacks_ && !hello_answered_;
}
inline void set_cert_cb_running(bool on = true) { cert_cb_running_ = on; }
inline void set_awaiting_new_session(bool on = true) {
awaiting_new_session_ = on;
@@ -105,6 +109,12 @@ class TLSWrap : public AsyncWrap,
// Called by the done() callback of the 'newSession' event.
void NewSessionDoneCb();
// Schedules 'onclienthello'; returns false to suspend the handshake until
// clientHelloDone(). The emit itself must not run on the library's stack.
bool OnEarlyClientHello(const unsigned char* session_id,
size_t session_id_len,
bool has_ticket);
// Implement MemoryRetainer:
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(TLSWrap)
@@ -122,9 +132,6 @@ class TLSWrap : public AsyncWrap,
static constexpr int kClearOutChunkSize = 16384;
// Maximum number of bytes for hello parser
static constexpr int kMaxHelloLength = 16384;
// Usual ServerHello + Certificate size
static constexpr int kInitialClientBufferLength = 4096;
@@ -140,6 +147,8 @@ class TLSWrap : public AsyncWrap,
}
void WaitForCertCb(CertCb cb, void* arg);
void EmitClientHello(const std::vector<unsigned char>& session_id,
bool has_ticket);
TLSWrap(Environment* env,
v8::Local<v8::Object> obj,
@@ -180,6 +189,7 @@ class TLSWrap : public AsyncWrap,
static int SelectSNIContextCallback(SSL* s, int* ad, void* arg);
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ClientHelloDone(const v8::FunctionCallbackInfo<v8::Value>& args);
static void DestroySSL(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableCertCb(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableALPNCb(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -188,7 +198,6 @@ class TLSWrap : public AsyncWrap,
static void EnableSessionCallbacks(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableTrace(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ExportKeyingMaterial(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetALPNNegotiatedProto(
@@ -215,7 +224,7 @@ class TLSWrap : public AsyncWrap,
static void IsSessionReused(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadSession(const v8::FunctionCallbackInfo<v8::Value>& args);
static void NewSessionDone(const v8::FunctionCallbackInfo<v8::Value>& args);
static void OnClientHelloParseEnd(void* arg);
static void ResumeAfterCertCb(void* arg);
static void Receive(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
static void RequestOCSP(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -257,7 +266,6 @@ class TLSWrap : public AsyncWrap,
Kind kind_;
ncrypto::SSLSessionPointer next_sess_;
ncrypto::SSLPointer ssl_;
ClientHelloParser hello_parser_;
v8::Global<v8::ArrayBufferView> ocsp_response_;
BaseObjectPtr<SecureContext> sni_context_;
BaseObjectPtr<SecureContext> sc_;
@@ -274,6 +282,10 @@ class TLSWrap : public AsyncWrap,
bool session_callbacks_ = false;
bool awaiting_new_session_ = false;
// 'onclienthello' has been emitted for this connection.
bool hello_emitted_ = false;
// JS has answered it by calling clientHelloDone().
bool hello_answered_ = false;
bool in_dowrite_ = false;
bool started_ = false;
bool shutdown_ = false;
-133
View File
@@ -1,133 +0,0 @@
#include "crypto/crypto_clienthello-inl.h"
#include "gtest/gtest.h"
// If the test is being compiled with an address sanitizer enabled, it should
// catch the memory violation, so do not use a guard page.
#ifdef __SANITIZE_ADDRESS__
#define NO_GUARD_PAGE
#elif defined(__has_feature)
#if __has_feature(address_sanitizer)
#define NO_GUARD_PAGE
#endif
#endif
// If the test is running without an address sanitizer, see if we can use
// mprotect() or VirtualProtect() to cause a segmentation fault when spatial
// safety is violated.
#if !defined(NO_GUARD_PAGE)
#ifdef __linux__
#include <sys/mman.h>
#include <unistd.h>
#if defined(_SC_PAGE_SIZE) && defined(PROT_NONE) && defined(PROT_READ) && \
defined(PROT_WRITE)
#define USE_MPROTECT
#endif
#elif defined(_WIN32) && defined(_MSC_VER)
#include <Windows.h>
#include <memoryapi.h>
#define USE_VIRTUALPROTECT
#endif
#endif
#if defined(USE_MPROTECT)
size_t GetPageSize() {
int page_size = sysconf(_SC_PAGE_SIZE);
CHECK_GE(page_size, 1);
return page_size;
}
#elif defined(USE_VIRTUALPROTECT)
size_t GetPageSize() {
SYSTEM_INFO system_info;
GetSystemInfo(&system_info);
return system_info.dwPageSize;
}
#endif
template <size_t N>
class OverrunGuardedBuffer {
public:
OverrunGuardedBuffer() {
#if defined(USE_MPROTECT) || defined(USE_VIRTUALPROTECT)
size_t page = GetPageSize();
CHECK_GE(page, N);
#endif
#ifdef USE_MPROTECT
// Place the packet right before a guard page, which, when accessed, causes
// a segmentation fault.
alloc_base = static_cast<uint8_t*>(aligned_alloc(page, 2 * page));
CHECK_NOT_NULL(alloc_base);
uint8_t* second_page = alloc_base + page;
CHECK_EQ(mprotect(second_page, page, PROT_NONE), 0);
data_base = second_page - N;
#elif defined(USE_VIRTUALPROTECT)
// On Windows, it works almost the same way.
alloc_base = static_cast<uint8_t*>(
VirtualAlloc(nullptr, 2 * page, MEM_COMMIT, PAGE_READWRITE));
CHECK_NOT_NULL(alloc_base);
uint8_t* second_page = alloc_base + page;
DWORD old_prot;
CHECK_NE(VirtualProtect(second_page, page, PAGE_NOACCESS, &old_prot), 0);
CHECK_EQ(old_prot, PAGE_READWRITE);
data_base = second_page - N;
#else
// Place the packet in a regular allocated buffer. The bug causes undefined
// behavior, which might crash the process, and when it does not, address
// sanitizers and valgrind will catch it.
alloc_base = static_cast<uint8_t*>(malloc(N));
CHECK_NOT_NULL(alloc_base);
data_base = alloc_base;
#endif
}
OverrunGuardedBuffer(const OverrunGuardedBuffer& other) = delete;
OverrunGuardedBuffer& operator=(const OverrunGuardedBuffer& other) = delete;
~OverrunGuardedBuffer() {
#if defined(USE_MPROTECT) || defined(USE_VIRTUALPROTECT)
size_t page = GetPageSize();
#endif
#ifdef USE_VIRTUALPROTECT
VirtualFree(alloc_base, 2 * page, MEM_RELEASE);
#else
#ifdef USE_MPROTECT
// Revert page protection such that the memory can be free()'d.
uint8_t* second_page = alloc_base + page;
CHECK_EQ(mprotect(second_page, page, PROT_READ | PROT_WRITE), 0);
#endif
free(alloc_base);
#endif
}
uint8_t* data() {
return data_base;
}
private:
uint8_t* alloc_base;
uint8_t* data_base;
};
// Test that ClientHelloParser::ParseHeader() does not blindly trust the client
// to send a valid frame length and subsequently does not read out-of-bounds.
TEST(NodeCrypto, ClientHelloParserParseHeaderOutOfBoundsRead) {
using node::crypto::ClientHelloParser;
// This is the simplest packet triggering the bug.
const uint8_t packet[] = {0x16, 0x03, 0x01, 0x00, 0x00};
OverrunGuardedBuffer<sizeof(packet)> buffer;
memcpy(buffer.data(), packet, sizeof(packet));
// Let the ClientHelloParser parse the packet. This should not lead to a
// segmentation fault or to undefined behavior.
node::crypto::ClientHelloParser parser;
bool end_cb_called = false;
parser.Start([](void* arg, auto hello) { GTEST_FAIL(); },
[](void* arg) {
bool* end_cb_called = static_cast<bool*>(arg);
EXPECT_FALSE(*end_cb_called);
*end_cb_called = true;
},
&end_cb_called);
parser.Parse(buffer.data(), sizeof(packet));
EXPECT_TRUE(end_cb_called);
}
-16
View File
@@ -1,16 +0,0 @@
/*
* A fuzzer focused on node::crypto::ClientHelloParser.
*/
#include <stdlib.h>
#include "crypto/crypto_clienthello-inl.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
node::crypto::ClientHelloParser parser;
bool end_cb_called = false;
parser.Start([](void* arg, auto hello) { },
[](void* arg) { },
&end_cb_called);
parser.Parse(data, size);
return 0;
}
@@ -0,0 +1,75 @@
'use strict';
// A ClientHello split across several TLS records must still emit
// 'resumeSession'.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const fixtures = require('../common/fixtures');
const net = require('net');
const tls = require('tls');
const options = {
key: fixtures.readKey('rsa_private.pem'),
cert: fixtures.readKey('rsa_cert.crt'),
};
// Capture a real ClientHello record so the replayed bytes are well-formed.
function captureClientHello(callback) {
let hello = Buffer.alloc(0);
const collector = net.createServer((socket) => {
socket.on('data', (chunk) => {
hello = Buffer.concat([hello, chunk]);
if (hello.length < 5 || hello.length < 5 + hello.readUInt16BE(3)) return;
socket.destroy();
collector.close(() => callback(hello));
});
});
collector.listen(0, common.mustCall(() => {
tls.connect({ port: collector.address().port, rejectUnauthorized: false })
.on('error', () => {});
}));
}
captureClientHello(common.mustCall((hello) => {
assert.strictEqual(hello[0], 22);
// Split partway through the fixed-size header, before the session ID.
const body = hello.subarray(5);
const split = 20;
assert.ok(body.length > split);
const record = (payload) => Buffer.concat([
Buffer.from([22, hello[1], hello[2],
payload.length >> 8, payload.length & 0xff]),
payload,
]);
const server = tls.createServer(options);
server.on('tlsClientError', () => {}); // The replay never completes.
server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port, common.mustCall(() => {
client.write(record(body.subarray(0, split)));
setTimeout(() => client.write(record(body.subarray(split))), 10);
}));
client.on('error', () => {});
// Fail fast: a missing event stalls the handshake rather than erroring.
const guard = setTimeout(() => {
throw new Error('resumeSession was not emitted');
}, common.platformTimeout(10000));
server.on('resumeSession', common.mustCall((id, callback) => {
clearTimeout(guard);
assert.ok(id.length > 0);
callback(null, null);
client.destroy();
server.close();
}));
}));
}));
@@ -0,0 +1,53 @@
'use strict';
// Writing to a server TLSSocket synchronously from inside a 'resumeSession'
// handler, while the handshake is still waiting on the ClientHello, must not
// break the connection; the data must be delivered once the handshake ends.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { EventEmitter } = require('events');
const fixtures = require('../common/fixtures');
const net = require('net');
const tls = require('tls');
const secureContext = tls.createSecureContext({
key: fixtures.readKey('rsa_private.pem'),
cert: fixtures.readKey('rsa_cert.crt'),
});
const fakeServer = new EventEmitter();
fakeServer.getTicketKeys = () => null;
let serverSocket;
fakeServer.on('resumeSession', common.mustCall((id, callback) => {
serverSocket.write('from-mid-handshake');
callback(null, null);
}));
const server = net.createServer(common.mustCall((raw) => {
serverSocket = new tls.TLSSocket(raw, {
isServer: true,
secureContext,
server: fakeServer,
});
serverSocket.on('error', common.mustNotCall());
}));
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false,
}, common.mustCall(() => {
client.on('data', common.mustCall((data) => {
assert.strictEqual(data.toString(), 'from-mid-handshake');
client.end();
server.close();
}));
}));
client.on('error', common.mustNotCall());
}));