mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
72768c7ff6
commit
5b940dd124
14 files changed
+284
-773
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}));
|
||||
Reference in new issue
Block a user