diff options
Diffstat (limited to 'chromium/net/third_party/quiche/src/quic/core/tls_chlo_extractor.cc')
-rw-r--r-- | chromium/net/third_party/quiche/src/quic/core/tls_chlo_extractor.cc | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/chromium/net/third_party/quiche/src/quic/core/tls_chlo_extractor.cc b/chromium/net/third_party/quiche/src/quic/core/tls_chlo_extractor.cc new file mode 100644 index 00000000000..7c10d2c8e0b --- /dev/null +++ b/chromium/net/third_party/quiche/src/quic/core/tls_chlo_extractor.cc @@ -0,0 +1,395 @@ +// Copyright (c) 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/third_party/quiche/src/quic/core/tls_chlo_extractor.h" +#include <cstring> +#include <memory> + +#include "third_party/boringssl/src/include/openssl/ssl.h" +#include "net/third_party/quiche/src/quic/core/frames/quic_crypto_frame.h" +#include "net/third_party/quiche/src/quic/core/quic_data_reader.h" +#include "net/third_party/quiche/src/quic/core/quic_error_codes.h" +#include "net/third_party/quiche/src/quic/core/quic_framer.h" +#include "net/third_party/quiche/src/quic/core/quic_time.h" +#include "net/third_party/quiche/src/quic/core/quic_types.h" +#include "net/third_party/quiche/src/quic/core/quic_versions.h" +#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_str_cat.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_text_utils.h" + +namespace quic { + +TlsChloExtractor::TlsChloExtractor() + : crypto_stream_sequencer_(this), + state_(State::kInitial), + parsed_crypto_frame_in_this_packet_(false) {} + +TlsChloExtractor::TlsChloExtractor(TlsChloExtractor&& other) + : TlsChloExtractor() { + *this = std::move(other); +} + +TlsChloExtractor& TlsChloExtractor::operator=(TlsChloExtractor&& other) { + framer_ = std::move(other.framer_); + if (framer_) { + framer_->set_visitor(this); + } + crypto_stream_sequencer_ = std::move(other.crypto_stream_sequencer_); + crypto_stream_sequencer_.set_stream(this); + ssl_ = std::move(other.ssl_); + if (ssl_) { + std::pair<SSL_CTX*, int> shared_handles = GetSharedSslHandles(); + int ex_data_index = shared_handles.second; + const int rv = SSL_set_ex_data(ssl_.get(), ex_data_index, this); + CHECK_EQ(rv, 1) << "Internal allocation failure in SSL_set_ex_data"; + } + state_ = other.state_; + error_details_ = std::move(other.error_details_); + parsed_crypto_frame_in_this_packet_ = + other.parsed_crypto_frame_in_this_packet_; + alpns_ = std::move(other.alpns_); + server_name_ = std::move(other.server_name_); + return *this; +} + +void TlsChloExtractor::IngestPacket(const ParsedQuicVersion& version, + const QuicReceivedPacket& packet) { + if (state_ == State::kUnrecoverableFailure) { + QUIC_DLOG(ERROR) << "Not ingesting packet after unrecoverable error"; + return; + } + if (version == UnsupportedQuicVersion()) { + QUIC_DLOG(ERROR) << "Not ingesting packet with unsupported version"; + return; + } + if (version.handshake_protocol != PROTOCOL_TLS1_3) { + QUIC_DLOG(ERROR) << "Not ingesting packet with non-TLS version " << version; + return; + } + if (framer_) { + // This is not the first packet we have ingested, check if version matches. + if (!framer_->IsSupportedVersion(version)) { + QUIC_DLOG(ERROR) + << "Not ingesting packet with version mismatch, expected " + << framer_->version() << ", got " << version; + return; + } + } else { + // This is the first packet we have ingested, setup parser. + framer_ = std::make_unique<QuicFramer>( + ParsedQuicVersionVector{version}, QuicTime::Zero(), + Perspective::IS_SERVER, /*expected_server_connection_id_length=*/0); + // Note that expected_server_connection_id_length only matters for short + // headers and we explicitly drop those so we can pass any value here. + framer_->set_visitor(this); + } + + // When the framer parses |packet|, if it sees a CRYPTO frame it will call + // OnCryptoFrame below and that will set parsed_crypto_frame_in_this_packet_ + // to true. + parsed_crypto_frame_in_this_packet_ = false; + const bool parse_success = framer_->ProcessPacket(packet); + if (state_ == State::kInitial && parsed_crypto_frame_in_this_packet_) { + // If we parsed a CRYPTO frame but didn't advance the state from initial, + // then it means that we will need more packets to reassemble the full CHLO, + // so we advance the state here. This can happen when the first packet + // received is not the first one in the crypto stream. This allows us to + // differentiate our state between single-packet CHLO and multi-packet CHLO. + state_ = State::kParsedPartialChloFragment; + } + + if (!parse_success) { + // This could be due to the packet being non-initial for example. + QUIC_DLOG(ERROR) << "Failed to process packet"; + return; + } +} + +// This is called when the framer parsed the unencrypted parts of the header. +bool TlsChloExtractor::OnUnauthenticatedPublicHeader( + const QuicPacketHeader& header) { + if (header.form != IETF_QUIC_LONG_HEADER_PACKET) { + QUIC_DLOG(ERROR) << "Not parsing non-long-header packet " << header; + return false; + } + if (header.long_packet_type != INITIAL) { + QUIC_DLOG(ERROR) << "Not parsing non-initial packet " << header; + return false; + } + // QuicFramer is constructed without knowledge of the server's connection ID + // so it needs to be set up here in order to decrypt the packet. + framer_->SetInitialObfuscators(header.destination_connection_id); + return true; +} + +// This is called by the framer if it detects a change in version during +// parsing. +bool TlsChloExtractor::OnProtocolVersionMismatch(ParsedQuicVersion version) { + // This should never be called because we already check versions in + // IngestPacket. + QUIC_BUG << "Unexpected version mismatch, expected " << framer_->version() + << ", got " << version; + return false; +} + +// This is called by the QuicStreamSequencer if it encounters an unrecoverable +// error that will prevent it from reassembling the crypto stream data. +void TlsChloExtractor::OnUnrecoverableError(QuicErrorCode error, + const std::string& details) { + HandleUnrecoverableError(quiche::QuicheStrCat( + "Crypto stream error ", QuicErrorCodeToString(error), ": ", details)); +} + +// This is called by the framer if it sees a CRYPTO frame during parsing. +bool TlsChloExtractor::OnCryptoFrame(const QuicCryptoFrame& frame) { + if (frame.level != ENCRYPTION_INITIAL) { + // Since we drop non-INITIAL packets in OnUnauthenticatedPublicHeader, + // we should never receive any CRYPTO frames at other encryption levels. + QUIC_BUG << "Parsed bad-level CRYPTO frame " << frame; + return false; + } + // parsed_crypto_frame_in_this_packet_ is checked in IngestPacket to allow + // advancing our state to track the difference between single-packet CHLO + // and multi-packet CHLO. + parsed_crypto_frame_in_this_packet_ = true; + crypto_stream_sequencer_.OnCryptoFrame(frame); + return true; +} + +// Called by the QuicStreamSequencer when it receives a CRYPTO frame that +// advances the amount of contiguous data we now have starting from offset 0. +void TlsChloExtractor::OnDataAvailable() { + // Lazily set up BoringSSL handle. + SetupSslHandle(); + + // Get data from the stream sequencer and pass it to BoringSSL. + struct iovec iov; + while (crypto_stream_sequencer_.GetReadableRegion(&iov)) { + const int rv = SSL_provide_quic_data( + ssl_.get(), ssl_encryption_initial, + reinterpret_cast<const uint8_t*>(iov.iov_base), iov.iov_len); + if (rv != 1) { + HandleUnrecoverableError("SSL_provide_quic_data failed"); + return; + } + crypto_stream_sequencer_.MarkConsumed(iov.iov_len); + } + + // Instruct BoringSSL to attempt parsing a full CHLO from the provided data. + // We ignore the return value since we know the handshake is going to fail + // because we explicitly cancel processing once we've parsed the CHLO. + (void)SSL_do_handshake(ssl_.get()); +} + +// static +TlsChloExtractor* TlsChloExtractor::GetInstanceFromSSL(SSL* ssl) { + std::pair<SSL_CTX*, int> shared_handles = GetSharedSslHandles(); + int ex_data_index = shared_handles.second; + return reinterpret_cast<TlsChloExtractor*>( + SSL_get_ex_data(ssl, ex_data_index)); +} + +// static +int TlsChloExtractor::SetReadSecretCallback( + SSL* ssl, + enum ssl_encryption_level_t /*level*/, + const SSL_CIPHER* /*cipher*/, + const uint8_t* /*secret*/, + size_t /*secret_length*/) { + GetInstanceFromSSL(ssl)->HandleUnexpectedCallback("SetReadSecretCallback"); + return 0; +} + +// static +int TlsChloExtractor::SetWriteSecretCallback( + SSL* ssl, + enum ssl_encryption_level_t /*level*/, + const SSL_CIPHER* /*cipher*/, + const uint8_t* /*secret*/, + size_t /*secret_length*/) { + GetInstanceFromSSL(ssl)->HandleUnexpectedCallback("SetWriteSecretCallback"); + return 0; +} + +// static +int TlsChloExtractor::WriteMessageCallback( + SSL* ssl, + enum ssl_encryption_level_t /*level*/, + const uint8_t* /*data*/, + size_t /*len*/) { + GetInstanceFromSSL(ssl)->HandleUnexpectedCallback("WriteMessageCallback"); + return 0; +} + +// static +int TlsChloExtractor::FlushFlightCallback(SSL* ssl) { + GetInstanceFromSSL(ssl)->HandleUnexpectedCallback("FlushFlightCallback"); + return 0; +} + +void TlsChloExtractor::HandleUnexpectedCallback( + const std::string& callback_name) { + std::string error_details = + quiche::QuicheStrCat("Unexpected callback ", callback_name); + QUIC_BUG << error_details; + HandleUnrecoverableError(error_details); +} + +// static +int TlsChloExtractor::SendAlertCallback(SSL* ssl, + enum ssl_encryption_level_t /*level*/, + uint8_t desc) { + GetInstanceFromSSL(ssl)->SendAlert(desc); + return 0; +} + +void TlsChloExtractor::SendAlert(uint8_t tls_alert_value) { + if (tls_alert_value == SSL3_AD_HANDSHAKE_FAILURE && HasParsedFullChlo()) { + // This is the most common scenario. Since we return an error from + // SelectCertCallback in order to cancel further processing, BoringSSL will + // try to send this alert to tell the client that the handshake failed. + return; + } + HandleUnrecoverableError(quiche::QuicheStrCat( + "BoringSSL attempted to send alert ", static_cast<int>(tls_alert_value), + " ", SSL_alert_desc_string_long(tls_alert_value))); +} + +// static +enum ssl_select_cert_result_t TlsChloExtractor::SelectCertCallback( + const SSL_CLIENT_HELLO* client_hello) { + GetInstanceFromSSL(client_hello->ssl)->HandleParsedChlo(client_hello); + // Always return an error to cancel any further processing in BoringSSL. + return ssl_select_cert_error; +} + +// Extracts the server name and ALPN from the parsed ClientHello. +void TlsChloExtractor::HandleParsedChlo(const SSL_CLIENT_HELLO* client_hello) { + const char* server_name = + SSL_get_servername(client_hello->ssl, TLSEXT_NAMETYPE_host_name); + if (server_name) { + server_name_ = std::string(server_name); + } + const uint8_t* alpn_data; + size_t alpn_len; + int rv = SSL_early_callback_ctx_extension_get( + client_hello, TLSEXT_TYPE_application_layer_protocol_negotiation, + &alpn_data, &alpn_len); + if (rv == 1) { + QuicDataReader alpns_reader(reinterpret_cast<const char*>(alpn_data), + alpn_len); + quiche::QuicheStringPiece alpns_payload; + if (!alpns_reader.ReadStringPiece16(&alpns_payload)) { + HandleUnrecoverableError("Failed to read alpns_payload"); + return; + } + QuicDataReader alpns_payload_reader(alpns_payload); + while (!alpns_payload_reader.IsDoneReading()) { + quiche::QuicheStringPiece alpn_payload; + if (!alpns_payload_reader.ReadStringPiece8(&alpn_payload)) { + HandleUnrecoverableError("Failed to read alpn_payload"); + return; + } + alpns_.emplace_back(std::string(alpn_payload)); + } + } + + // Update our state now that we've parsed a full CHLO. + if (state_ == State::kInitial) { + state_ = State::kParsedFullSinglePacketChlo; + } else if (state_ == State::kParsedPartialChloFragment) { + state_ = State::kParsedFullMultiPacketChlo; + } else { + QUIC_BUG << "Unexpected state on successful parse " + << StateToString(state_); + } +} + +// static +std::pair<SSL_CTX*, int> TlsChloExtractor::GetSharedSslHandles() { + // Use a lambda to benefit from C++11 guarantee that static variables are + // initialized lazily in a thread-safe manner. |shared_handles| is therefore + // guaranteed to be initialized exactly once and never destructed. + static std::pair<SSL_CTX*, int>* shared_handles = []() { + CRYPTO_library_init(); + SSL_CTX* ssl_ctx = SSL_CTX_new(TLS_with_buffers_method()); + SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION); + static const SSL_QUIC_METHOD kQuicCallbacks{ + TlsChloExtractor::SetReadSecretCallback, + TlsChloExtractor::SetWriteSecretCallback, + TlsChloExtractor::WriteMessageCallback, + TlsChloExtractor::FlushFlightCallback, + TlsChloExtractor::SendAlertCallback}; + SSL_CTX_set_quic_method(ssl_ctx, &kQuicCallbacks); + SSL_CTX_set_select_certificate_cb(ssl_ctx, + TlsChloExtractor::SelectCertCallback); + int ex_data_index = + SSL_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr); + return new std::pair<SSL_CTX*, int>(ssl_ctx, ex_data_index); + }(); + return *shared_handles; +} + +// Sets up the per-instance SSL handle needed by BoringSSL. +void TlsChloExtractor::SetupSslHandle() { + if (ssl_) { + // Handles have already been set up. + return; + } + + std::pair<SSL_CTX*, int> shared_handles = GetSharedSslHandles(); + SSL_CTX* ssl_ctx = shared_handles.first; + int ex_data_index = shared_handles.second; + + ssl_ = bssl::UniquePtr<SSL>(SSL_new(ssl_ctx)); + const int rv = SSL_set_ex_data(ssl_.get(), ex_data_index, this); + CHECK_EQ(rv, 1) << "Internal allocation failure in SSL_set_ex_data"; + SSL_set_accept_state(ssl_.get()); +} + +// Called by other methods to record any unrecoverable failures they experience. +void TlsChloExtractor::HandleUnrecoverableError( + const std::string& error_details) { + if (HasParsedFullChlo()) { + // Ignore errors if we've parsed everything successfully. + QUIC_DLOG(ERROR) << "Ignoring error: " << error_details; + return; + } + QUIC_DLOG(ERROR) << "Handling error: " << error_details; + + state_ = State::kUnrecoverableFailure; + + if (error_details_.empty()) { + error_details_ = error_details; + } else { + error_details_ = quiche::QuicheStrCat(error_details_, "; ", error_details); + } +} + +// static +std::string TlsChloExtractor::StateToString(State state) { + switch (state) { + case State::kInitial: + return "Initial"; + case State::kParsedFullSinglePacketChlo: + return "ParsedFullSinglePacketChlo"; + case State::kParsedFullMultiPacketChlo: + return "ParsedFullMultiPacketChlo"; + case State::kParsedPartialChloFragment: + return "ParsedPartialChloFragment"; + case State::kUnrecoverableFailure: + return "UnrecoverableFailure"; + } + return quiche::QuicheStrCat("Unknown(", static_cast<int>(state), ")"); +} + +std::ostream& operator<<(std::ostream& os, + const TlsChloExtractor::State& state) { + os << TlsChloExtractor::StateToString(state); + return os; +} + +} // namespace quic |