| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/socket/tls_stream_attempt.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string_view> |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/values.h" |
| #include "net/base/completion_once_callback.h" |
| #include "net/base/features.h" |
| #include "net/base/host_port_pair.h" |
| #include "net/base/net_errors.h" |
| #include "net/socket/client_socket_factory.h" |
| #include "net/socket/tcp_stream_attempt.h" |
| #include "net/ssl/ssl_cert_request_info.h" |
| |
| namespace net { |
| |
| // static |
| std::string_view TlsStreamAttempt::StateToString(State state) { |
| switch (state) { |
| case State::kNone: |
| return "None"; |
| case State::kTcpAttempt: |
| return "TcpAttempt"; |
| case State::kTcpAttemptComplete: |
| return "TcpAttemptComplete"; |
| case State::kTlsAttempt: |
| return "TlsAttempt"; |
| case State::kTlsAttemptComplete: |
| return "TlsAttemptComplete"; |
| } |
| } |
| |
| TlsStreamAttempt::TlsStreamAttempt(const StreamAttemptParams* params, |
| IPEndPoint ip_endpoint, |
| perfetto::Track track, |
| HostPortPair host_port_pair, |
| Delegate* delegate) |
| : StreamAttempt(params, |
| ip_endpoint, |
| track, |
| NetLogSourceType::TLS_STREAM_ATTEMPT, |
| NetLogEventType::TLS_STREAM_ATTEMPT_ALIVE), |
| host_port_pair_(std::move(host_port_pair)), |
| delegate_(delegate) {} |
| |
| TlsStreamAttempt::~TlsStreamAttempt() { |
| MaybeRecordTlsHandshakeEnd(ERR_ABORTED); |
| } |
| |
| LoadState TlsStreamAttempt::GetLoadState() const { |
| switch (next_state_) { |
| case State::kNone: |
| return LOAD_STATE_IDLE; |
| case State::kTcpAttempt: |
| case State::kTcpAttemptComplete: |
| CHECK(nested_attempt_); |
| return nested_attempt_->GetLoadState(); |
| case State::kTlsAttempt: |
| case State::kTlsAttemptComplete: |
| return LOAD_STATE_SSL_HANDSHAKE; |
| } |
| } |
| |
| base::Value::Dict TlsStreamAttempt::GetInfoAsValue() const { |
| base::Value::Dict dict; |
| dict.Set("next_state", StateToString(next_state_)); |
| dict.Set("tcp_handshake_completed", tcp_handshake_completed_); |
| dict.Set("tls_handshake_started", tls_handshake_started_); |
| dict.Set("has_ssl_config", ssl_config_.has_value()); |
| if (nested_attempt_) { |
| dict.Set("nested_attempt", nested_attempt_->GetInfoAsValue()); |
| } |
| return dict; |
| } |
| |
| scoped_refptr<SSLCertRequestInfo> TlsStreamAttempt::GetCertRequestInfo() { |
| return ssl_cert_request_info_; |
| } |
| |
| int TlsStreamAttempt::StartInternal() { |
| CHECK_EQ(next_state_, State::kNone); |
| next_state_ = State::kTcpAttempt; |
| return DoLoop(OK); |
| } |
| |
| base::Value::Dict TlsStreamAttempt::GetNetLogStartParams() { |
| base::Value::Dict dict; |
| dict.Set("host_port", host_port_pair_.ToString()); |
| return dict; |
| } |
| |
| void TlsStreamAttempt::OnIOComplete(int rv) { |
| CHECK_NE(rv, ERR_IO_PENDING); |
| rv = DoLoop(rv); |
| if (rv != ERR_IO_PENDING) { |
| NotifyOfCompletion(rv); |
| } |
| } |
| |
| int TlsStreamAttempt::DoLoop(int rv) { |
| CHECK_NE(next_state_, State::kNone); |
| |
| do { |
| State state = next_state_; |
| next_state_ = State::kNone; |
| switch (state) { |
| case State::kNone: |
| NOTREACHED() << "Invalid state"; |
| case State::kTcpAttempt: |
| rv = DoTcpAttempt(); |
| break; |
| case State::kTcpAttemptComplete: |
| rv = DoTcpAttemptComplete(rv); |
| break; |
| case State::kTlsAttempt: |
| rv = DoTlsAttempt(rv); |
| break; |
| case State::kTlsAttemptComplete: |
| rv = DoTlsAttemptComplete(rv); |
| break; |
| } |
| } while (next_state_ != State::kNone && rv != ERR_IO_PENDING); |
| |
| return rv; |
| } |
| |
| int TlsStreamAttempt::DoTcpAttempt() { |
| next_state_ = State::kTcpAttemptComplete; |
| nested_attempt_ = std::make_unique<TcpStreamAttempt>(¶ms(), ip_endpoint(), |
| track(), &net_log()); |
| return nested_attempt_->Start( |
| base::BindOnce(&TlsStreamAttempt::OnIOComplete, base::Unretained(this))); |
| } |
| |
| int TlsStreamAttempt::DoTcpAttemptComplete(int rv) { |
| const LoadTimingInfo::ConnectTiming& nested_timing = |
| nested_attempt_->connect_timing(); |
| mutable_connect_timing().connect_start = nested_timing.connect_start; |
| |
| tcp_handshake_completed_ = true; |
| delegate_->OnTcpHandshakeComplete(); |
| |
| if (rv != OK) { |
| return rv; |
| } |
| |
| net_log().BeginEvent(NetLogEventType::TLS_STREAM_ATTEMPT_WAIT_FOR_SSL_CONFIG); |
| |
| next_state_ = State::kTlsAttempt; |
| |
| if (ssl_config_.has_value()) { |
| // We restarted for ECH retry and already have a SSLConfig with retry |
| // configs. |
| return OK; |
| } |
| |
| int ssl_config_ready_result = delegate_->WaitForSSLConfigReady(base::BindOnce( |
| &TlsStreamAttempt::OnIOComplete, weak_ptr_factory_.GetWeakPtr())); |
| if (ssl_config_ready_result == ERR_IO_PENDING) { |
| TRACE_EVENT_INSTANT("net.stream", "WaitForSSLConfig", track()); |
| } |
| return ssl_config_ready_result; |
| } |
| |
| int TlsStreamAttempt::DoTlsAttempt(int rv) { |
| CHECK_EQ(rv, OK); |
| |
| net_log().EndEvent(NetLogEventType::TLS_STREAM_ATTEMPT_WAIT_FOR_SSL_CONFIG); |
| |
| next_state_ = State::kTlsAttemptComplete; |
| |
| std::unique_ptr<StreamSocket> nested_socket = |
| nested_attempt_->ReleaseStreamSocket(); |
| if (!ssl_config_) { |
| auto get_config_result = delegate_->GetSSLConfig(); |
| |
| if (get_config_result.has_value()) { |
| ssl_config_ = *get_config_result; |
| // For metrics, we want to know whether the server advertised Trust Anchor |
| // IDs in DNS (i.e., whether the client could use Trust Anchor IDs with |
| // this server, regardless of whether the feature was enabled). But we |
| // don't want to actually configure the Trust Anchor IDs on the connection |
| // if the feature flag isn't enabled. |
| trust_anchor_ids_from_dns_ = !ssl_config_->trust_anchor_ids.empty(); |
| if (!base::FeatureList::IsEnabled(features::kTLSTrustAnchorIDs)) { |
| ssl_config_->trust_anchor_ids.clear(); |
| } |
| } else { |
| CHECK_EQ(get_config_result.error(), GetSSLConfigError::kAbort); |
| return ERR_ABORTED; |
| } |
| } |
| |
| nested_attempt_.reset(); |
| |
| tls_handshake_started_ = true; |
| mutable_connect_timing().ssl_start = base::TimeTicks::Now(); |
| tls_handshake_timeout_timer_.Start( |
| FROM_HERE, kTlsHandshakeTimeout, |
| base::BindOnce(&TlsStreamAttempt::OnTlsHandshakeTimeout, |
| base::Unretained(this))); |
| |
| ssl_socket_ = params().client_socket_factory->CreateSSLClientSocket( |
| params().ssl_client_context, std::move(nested_socket), host_port_pair_, |
| *ssl_config_); |
| |
| TRACE_EVENT_BEGIN("net.stream", "TlsConnect", track()); |
| net_log().BeginEvent(NetLogEventType::TLS_STREAM_ATTEMPT_CONNECT); |
| |
| return ssl_socket_->Connect( |
| base::BindOnce(&TlsStreamAttempt::OnIOComplete, base::Unretained(this))); |
| } |
| |
| int TlsStreamAttempt::DoTlsAttemptComplete(int rv) { |
| MaybeRecordTlsHandshakeEnd(rv); |
| net_log().EndEventWithNetErrorCode( |
| NetLogEventType::TLS_STREAM_ATTEMPT_CONNECT, rv); |
| |
| mutable_connect_timing().ssl_end = base::TimeTicks::Now(); |
| tls_handshake_timeout_timer_.Stop(); |
| |
| const bool ech_enabled = params().ssl_client_context->config().ech_enabled; |
| |
| if (!ech_retry_configs_ && rv == ERR_ECH_NOT_NEGOTIATED && ech_enabled) { |
| CHECK(ssl_socket_); |
| // We used ECH, and the server could not decrypt the ClientHello. However, |
| // it was able to handshake with the public name and send authenticated |
| // retry configs. If this is not the first time around, retry the connection |
| // with the new ECHConfigList, or with ECH disabled (empty retry configs), |
| // as directed. |
| // |
| // See |
| // https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html#section-6.1.6 |
| ech_retry_configs_ = ssl_socket_->GetECHRetryConfigs(); |
| ssl_config_->ech_config_list = *ech_retry_configs_; |
| |
| // TODO(crbug.com/346835898): Add a NetLog to record ECH retry configs. |
| |
| ResetStateForRestart(); |
| next_state_ = State::kTcpAttempt; |
| return OK; |
| } |
| |
| // If we got a certificate error and the server advertised some Trust Anchor |
| // IDs in the handshake that we trust, then retry the connection, using the |
| // fresh Trust Anchor IDs from the server. We only want to retry once; if we |
| // have we already retried, so we skip all of this and treat the connection |
| // error as usual. |
| // |
| // TODO(https://crbug.com/399937371): clarify and test the interactions of ECH |
| // retry and TAI retry. |
| if (IsCertificateError(rv) && !retried_for_trust_anchor_ids_ && |
| base::FeatureList::IsEnabled(features::kTLSTrustAnchorIDs)) { |
| CHECK(ssl_socket_); |
| |
| std::vector<std::vector<uint8_t>> server_trust_anchor_ids = |
| ssl_socket_->GetServerTrustAnchorIDsForRetry(); |
| // https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html#name-retry-mechanism: |
| // If the EncryptedExtensions had no trust_anchor extension, or no match was |
| // found, the client returns the error to the application. |
| if (!server_trust_anchor_ids.empty()) { |
| std::vector<uint8_t> trust_anchor_ids_for_retry = |
| SSLConfig::SelectTrustAnchorIDs( |
| server_trust_anchor_ids, |
| params().ssl_client_context->config().trust_anchor_ids); |
| if (!trust_anchor_ids_for_retry.empty()) { |
| retried_for_trust_anchor_ids_ = true; |
| ssl_config_->trust_anchor_ids = trust_anchor_ids_for_retry; |
| |
| ResetStateForRestart(); |
| next_state_ = State::kTcpAttempt; |
| return OK; |
| } |
| } |
| } |
| |
| const bool is_ech_capable = |
| ssl_config_ && !ssl_config_->ech_config_list.empty(); |
| SSLClientSocket::RecordSSLConnectResult( |
| ssl_socket_.get(), rv, is_ech_capable, ech_enabled, ech_retry_configs_, |
| trust_anchor_ids_from_dns_, retried_for_trust_anchor_ids_, |
| connect_timing()); |
| |
| if (rv == OK || IsCertificateError(rv)) { |
| CHECK(ssl_socket_); |
| SetStreamSocket(std::move(ssl_socket_)); |
| } else if (rv == ERR_SSL_CLIENT_AUTH_CERT_NEEDED) { |
| CHECK(ssl_socket_); |
| ssl_cert_request_info_ = base::MakeRefCounted<SSLCertRequestInfo>(); |
| ssl_socket_->GetSSLCertRequestInfo(ssl_cert_request_info_.get()); |
| } |
| |
| return rv; |
| } |
| |
| void TlsStreamAttempt::OnTlsHandshakeTimeout() { |
| // TODO(bashi): The error code should be ERR_CONNECTION_TIMED_OUT but use |
| // ERR_TIMED_OUT for consistency with ConnectJobs. |
| OnIOComplete(ERR_TIMED_OUT); |
| } |
| |
| void TlsStreamAttempt::MaybeRecordTlsHandshakeEnd(int rv) { |
| if (!tls_handshake_started_ || !tls_handshake_timeout_timer_.IsRunning()) { |
| return; |
| } |
| TRACE_EVENT_END("net.stream", track(), "result", rv); |
| } |
| |
| void TlsStreamAttempt::ResetStateForRestart() { |
| tcp_handshake_completed_ = false; |
| tls_handshake_started_ = false; |
| ssl_socket_.reset(); |
| ssl_cert_request_info_.reset(); |
| } |
| |
| } // namespace net |