diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js index c1e7fbcae338cd..51fea0e996149c 100644 --- a/lib/internal/tls/wrap.js +++ b/lib/internal/tls/wrap.js @@ -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) diff --git a/node.gyp b/node.gyp index e88bac122b93d4..e07d9b6bf5e842 100644 --- a/node.gyp +++ b/node.gyp @@ -415,7 +415,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', @@ -425,7 +424,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', @@ -441,7 +439,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', @@ -471,7 +468,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', ], @@ -1309,54 +1305,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', diff --git a/src/crypto/README.md b/src/crypto/README.md index cc5093a385cad3..ad06cf9892765b 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -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. diff --git a/src/crypto/crypto_clienthello-inl.h b/src/crypto/crypto_clienthello-inl.h deleted file mode 100644 index 1b8a0c00c3074d..00000000000000 --- a/src/crypto/crypto_clienthello-inl.h +++ /dev/null @@ -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_ diff --git a/src/crypto/crypto_clienthello.cc b/src/crypto/crypto_clienthello.cc deleted file mode 100644 index 203289ae29113a..00000000000000 --- a/src/crypto/crypto_clienthello.cc +++ /dev/null @@ -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(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 diff --git a/src/crypto/crypto_clienthello.h b/src/crypto/crypto_clienthello.h deleted file mode 100644 index 3af08bc6475ecf..00000000000000 --- a/src/crypto/crypto_clienthello.h +++ /dev/null @@ -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 // size_t -#include - -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_ diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 5447de596f98ec..d6387a55bd7839 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -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); } diff --git a/src/crypto/crypto_context.h b/src/crypto/crypto_context.h index 95ddea4c262dd5..c65f553e40fd88 100644 --- a/src/crypto/crypto_context.h +++ b/src/crypto/crypto_context.h @@ -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); diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc index ce44ad15fa79d3..9981704fb34a22 100644 --- a/src/crypto/crypto_tls.cc +++ b/src/crypto/crypto_tls.cc @@ -23,7 +23,6 @@ #include #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(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(SSL_get_app_data(ch->ssl)); + if (!w->should_suspend_for_client_hello()) return ssl_select_cert_success; - Local hello_obj = Object::New(env->isolate()); - Local servername = (hello.servername() == nullptr) - ? String::Empty(env->isolate()) - : OneByteString(env->isolate(), - hello.servername(), - hello.servername_size()); - Local buf = - Buffer::Copy( - env, - reinterpret_cast(hello.session_id()), - hello.session_size()).FromMaybe(Local()); + 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; - } + 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(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; - Local argv[] = { hello_obj }; - w->MakeCallback(env->onclienthello_string(), arraysize(argv), argv); + 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(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 id(session_id, session_id + session_id_len); + BaseObjectPtr 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& 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 hello_obj = Object::New(env->isolate()); + Local buf = + Buffer::Copy(env, + reinterpret_cast(session_id.data()), + session_id.size()) + .FromMaybe(Local()); + + 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 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(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& 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& args) { @@ -1336,7 +1354,7 @@ void TLSWrap::Destroy() { void TLSWrap::EnableCertCb(const FunctionCallbackInfo& 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(arg); - Debug(c, "OnClientHelloParseEnd()"); + Debug(c, "ResumeAfterCertCb()"); c->Cycle(); } @@ -2033,10 +2051,11 @@ void TLSWrap::ExportKeyingMaterial(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(buffer); } -void TLSWrap::EndParser(const FunctionCallbackInfo& args) { +void TLSWrap::ClientHelloDone(const FunctionCallbackInfo& 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& 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); diff --git a/src/crypto/crypto_tls.h b/src/crypto/crypto_tls.h index 87063b50bb7455..61f773b9c609d2 100644 --- a/src/crypto/crypto_tls.h +++ b/src/crypto/crypto_tls.h @@ -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& session_id, + bool has_ticket); TLSWrap(Environment* env, v8::Local obj, @@ -180,6 +189,7 @@ class TLSWrap : public AsyncWrap, static int SelectSNIContextCallback(SSL* s, int* ad, void* arg); static void CertCbDone(const v8::FunctionCallbackInfo& args); + static void ClientHelloDone(const v8::FunctionCallbackInfo& args); static void DestroySSL(const v8::FunctionCallbackInfo& args); static void EnableCertCb(const v8::FunctionCallbackInfo& args); static void EnableALPNCb(const v8::FunctionCallbackInfo& args); @@ -188,7 +198,6 @@ class TLSWrap : public AsyncWrap, static void EnableSessionCallbacks( const v8::FunctionCallbackInfo& args); static void EnableTrace(const v8::FunctionCallbackInfo& args); - static void EndParser(const v8::FunctionCallbackInfo& args); static void ExportKeyingMaterial( const v8::FunctionCallbackInfo& args); static void GetALPNNegotiatedProto( @@ -215,7 +224,7 @@ class TLSWrap : public AsyncWrap, static void IsSessionReused(const v8::FunctionCallbackInfo& args); static void LoadSession(const v8::FunctionCallbackInfo& args); static void NewSessionDone(const v8::FunctionCallbackInfo& args); - static void OnClientHelloParseEnd(void* arg); + static void ResumeAfterCertCb(void* arg); static void Receive(const v8::FunctionCallbackInfo& args); static void Renegotiate(const v8::FunctionCallbackInfo& args); static void RequestOCSP(const v8::FunctionCallbackInfo& args); @@ -257,7 +266,6 @@ class TLSWrap : public AsyncWrap, Kind kind_; ncrypto::SSLSessionPointer next_sess_; ncrypto::SSLPointer ssl_; - ClientHelloParser hello_parser_; v8::Global ocsp_response_; BaseObjectPtr sni_context_; BaseObjectPtr 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; diff --git a/test/cctest/test_crypto_clienthello.cc b/test/cctest/test_crypto_clienthello.cc deleted file mode 100644 index 870857cf906109..00000000000000 --- a/test/cctest/test_crypto_clienthello.cc +++ /dev/null @@ -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 -#include -#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 -#include -#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 -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(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( - 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(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 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(arg); - EXPECT_FALSE(*end_cb_called); - *end_cb_called = true; - }, - &end_cb_called); - parser.Parse(buffer.data(), sizeof(packet)); - EXPECT_TRUE(end_cb_called); -} diff --git a/test/fuzzers/fuzz_ClientHelloParser.cc b/test/fuzzers/fuzz_ClientHelloParser.cc deleted file mode 100644 index 87d7ae5e303e17..00000000000000 --- a/test/fuzzers/fuzz_ClientHelloParser.cc +++ /dev/null @@ -1,16 +0,0 @@ -/* - * A fuzzer focused on node::crypto::ClientHelloParser. - */ - -#include -#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; -} diff --git a/test/parallel/test-tls-client-hello-fragmented.js b/test/parallel/test-tls-client-hello-fragmented.js new file mode 100644 index 00000000000000..690482e73dc5ff --- /dev/null +++ b/test/parallel/test-tls-client-hello-fragmented.js @@ -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(); + })); + })); +})); diff --git a/test/parallel/test-tls-clienthello-sync-write.js b/test/parallel/test-tls-clienthello-sync-write.js new file mode 100644 index 00000000000000..4b7fcc541f0cba --- /dev/null +++ b/test/parallel/test-tls-clienthello-sync-write.js @@ -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()); +}));