Add support for handling client certificate requests in QUIC TLS. This change introduces a mechanism for the QUIC client to respond to a server's request for a client certificate during the TLS handshake. When a CertificateRequest message is received, the handshake is suspended, and a new callback, OnCertificateRequested, is triggered on the ProofHandler. This allows the application to provide a client certificate before the handshake resumes. Protected by quic_restart_flag_quic_client_cert_support. PiperOrigin-RevId: 914968512
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index 069549d..45b8b0e 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -66,6 +66,7 @@ QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_true, true, true, "A testonly reloadable flag that will always default to true.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_update_max_datagram, true, true, "If true, updates the maximum datagram size after flushing pending packets.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_use_received_client_addresses_cache, true, true, "If true, use a LRU cache to record client addresses of packets received on server's original address.") +QUICHE_FLAG(bool, quiche_restart_flag_quic_client_cert_support, false, true, "If true, enables dynamic client certs in QUIC.") QUICHE_FLAG(bool, quiche_restart_flag_quic_dispatcher_close_connection_on_invalid_ack, false, false, "An invalid ack is an ack that the peer sent for a packet that was not sent by the dispatcher. If true, the dispatcher will close the connection if it receives an invalid ack.") QUICHE_FLAG(bool, quiche_restart_flag_quic_shed_tls_handshake_config, false, true, "If true, QUIC connections will call SSL_set_shed_handshake_config to drop BoringSSL handshake state after the handshake finishes in order to save memory.") QUICHE_FLAG(bool, quiche_restart_flag_quic_support_release_time_for_gso, false, false, "If true, QuicGsoBatchWriter will support release time if it is available and the process has the permission to do so.")
diff --git a/quiche/quic/core/crypto/tls_client_connection.cc b/quiche/quic/core/crypto/tls_client_connection.cc index d70737d..5f63f78 100644 --- a/quiche/quic/core/crypto/tls_client_connection.cc +++ b/quiche/quic/core/crypto/tls_client_connection.cc
@@ -7,6 +7,9 @@ #include <utility> #include <vector> +#include "quiche/quic/platform/api/quic_flag_utils.h" +#include "quiche/quic/platform/api/quic_flags.h" + namespace quic { TlsClientConnection::TlsClientConnection(SSL_CTX* ssl_ctx, Delegate* delegate, @@ -21,6 +24,13 @@ bssl::UniquePtr<SSL_CTX> ssl_ctx = TlsConnection::CreateSslCtx(); // Configure certificate verification. SSL_CTX_set_custom_verify(ssl_ctx.get(), SSL_VERIFY_PEER, &VerifyCallback); + + if (GetQuicRestartFlag(quic_client_cert_support)) { + QUIC_RESTART_FLAG_COUNT_N(quic_client_cert_support, 1, 2); + // Register the client certificate callback. + SSL_CTX_set_cert_cb(ssl_ctx.get(), &ClientCertRequestCallback, nullptr); + } + int reverify_on_resume_enabled = 1; SSL_CTX_set_reverify_on_resume(ssl_ctx.get(), reverify_on_resume_enabled); @@ -48,4 +58,10 @@ return 1; } +// static +int TlsClientConnection::ClientCertRequestCallback(SSL* ssl, void*) { + return static_cast<TlsClientConnection*>(ConnectionFromSsl(ssl)) + ->delegate_->OnClientCertRequested(ssl); +} + } // namespace quic
diff --git a/quiche/quic/core/crypto/tls_client_connection.h b/quiche/quic/core/crypto/tls_client_connection.h index 187da35..153a173 100644 --- a/quiche/quic/core/crypto/tls_client_connection.h +++ b/quiche/quic/core/crypto/tls_client_connection.h
@@ -23,6 +23,10 @@ // Called when a NewSessionTicket is received from the server. virtual void InsertSession(bssl::UniquePtr<SSL_SESSION> session) = 0; + // Called when the server requests a client certificate. + // Returns 1 on success, 0 on failure, or -1 to suspend the handshake. + virtual int OnClientCertRequested(SSL* ssl) = 0; + // Provides the delegate for callbacks that are shared between client and // server. virtual TlsConnection::Delegate* ConnectionDelegate() = 0; @@ -46,6 +50,10 @@ // Delegate::InsertSession. static int NewSessionCallback(SSL* ssl, SSL_SESSION* session); + // Registered as the callback for SSL_CTX_set_cert_cb, which calls + // Delegate::OnClientCertRequested. + static int ClientCertRequestCallback(SSL* ssl, void* arg); + Delegate* delegate_; };
diff --git a/quiche/quic/core/http/quic_connection_migration_manager_test.cc b/quiche/quic/core/http/quic_connection_migration_manager_test.cc index 71bfac6..b4fe7d3 100644 --- a/quiche/quic/core/http/quic_connection_migration_manager_test.cc +++ b/quiche/quic/core/http/quic_connection_migration_manager_test.cc
@@ -365,6 +365,11 @@ void OnProofVerifyDetailsAvailable( const ProofVerifyDetails& /*verify_details*/) override {} + bool OnCertificateRequested( + const std::vector<std::string>& /*cert_authorities*/) override { + return false; + } + TestStream* CreateOutgoingBidirectionalStream() override { QuicStreamId id = GetNextOutgoingBidirectionalStreamId(); if (id ==
diff --git a/quiche/quic/core/http/quic_spdy_client_session.cc b/quiche/quic/core/http/quic_spdy_client_session.cc index 740c0e9..053536d 100644 --- a/quiche/quic/core/http/quic_spdy_client_session.cc +++ b/quiche/quic/core/http/quic_spdy_client_session.cc
@@ -76,6 +76,11 @@ void QuicSpdyClientSession::OnProofVerifyDetailsAvailable( const ProofVerifyDetails& /*verify_details*/) {} +bool QuicSpdyClientSession::OnCertificateRequested( + const std::vector<std::string>& /*cert_authorities*/) { + return false; +} + bool QuicSpdyClientSession::ShouldCreateOutgoingBidirectionalStream() { if (!crypto_stream_->encryption_established()) { QUIC_DLOG(INFO) << "Encryption not active so no outgoing stream created.";
diff --git a/quiche/quic/core/http/quic_spdy_client_session.h b/quiche/quic/core/http/quic_spdy_client_session.h index 4d104d1..58701c2 100644 --- a/quiche/quic/core/http/quic_spdy_client_session.h +++ b/quiche/quic/core/http/quic_spdy_client_session.h
@@ -87,6 +87,8 @@ void OnProofValid(const QuicCryptoClientConfig::CachedState& cached) override; void OnProofVerifyDetailsAvailable( const ProofVerifyDetails& verify_details) override; + bool OnCertificateRequested( + const std::vector<std::string>& cert_authorities) override; // QuicSpdyClientSessionWithMigration methods: void OnConnectionToBeClosedDueToMigrationError(
diff --git a/quiche/quic/core/quic_crypto_client_handshaker_test.cc b/quiche/quic/core/quic_crypto_client_handshaker_test.cc index fc42e01..40f40d4 100644 --- a/quiche/quic/core/quic_crypto_client_handshaker_test.cc +++ b/quiche/quic/core/quic_crypto_client_handshaker_test.cc
@@ -24,6 +24,10 @@ const QuicCryptoClientConfig::CachedState& /*cached*/) override {} void OnProofVerifyDetailsAvailable( const ProofVerifyDetails& /*verify_details*/) override {} + bool OnCertificateRequested( + const std::vector<std::string>& /*cert_authorities*/) override { + return false; + } }; class InsecureProofVerifier : public ProofVerifier {
diff --git a/quiche/quic/core/quic_crypto_client_stream.h b/quiche/quic/core/quic_crypto_client_stream.h index 2aaa58c..b30c199 100644 --- a/quiche/quic/core/quic_crypto_client_stream.h +++ b/quiche/quic/core/quic_crypto_client_stream.h
@@ -9,6 +9,7 @@ #include <memory> #include <optional> #include <string> +#include <vector> #include "openssl/ssl.h" #include "quiche/quic/core/crypto/proof_verifier.h" @@ -266,6 +267,12 @@ // will only be called for secure QUIC connections. virtual void OnProofVerifyDetailsAvailable( const ProofVerifyDetails& verify_details) = 0; + + // Called when the server requests a client certificate. |cert_authorities| + // contains the DER-encoded requested CAs. Returns true if the handshake + // should be suspended to provide the certificate asynchronously. + virtual bool OnCertificateRequested( + const std::vector<std::string>& cert_authorities) = 0; }; QuicCryptoClientStream(const QuicServerId& server_id, QuicSession* session,
diff --git a/quiche/quic/core/quic_generic_session.cc b/quiche/quic/core/quic_generic_session.cc index a592f1a..8c2def6 100644 --- a/quiche/quic/core/quic_generic_session.cc +++ b/quiche/quic/core/quic_generic_session.cc
@@ -31,6 +31,10 @@ public: void OnProofValid(const QuicCryptoClientConfig::CachedState&) override {} void OnProofVerifyDetailsAvailable(const ProofVerifyDetails&) override {} + bool OnCertificateRequested( + const std::vector<std::string>& cert_authorities) override { + return false; + } }; class NoOpServerCryptoHelper : public QuicCryptoServerStreamBase::Helper {
diff --git a/quiche/quic/core/tls_client_handshaker.cc b/quiche/quic/core/tls_client_handshaker.cc index fb50241..5f00c09 100644 --- a/quiche/quic/core/tls_client_handshaker.cc +++ b/quiche/quic/core/tls_client_handshaker.cc
@@ -706,6 +706,32 @@ received_application_state_.get()); } +int TlsClientHandshaker::OnClientCertRequested(SSL* ssl) { + QUICHE_DCHECK(GetQuicRestartFlag(quic_client_cert_support)); + if (SSL_get0_chain(ssl) != nullptr) { + QUIC_DVLOG(1) << "Client certificate already set, continuing handshake."; + return 1; + } + const STACK_OF(CRYPTO_BUFFER)* ca_names = SSL_get0_server_requested_CAs(ssl); + std::vector<std::string> cert_authorities; + if (ca_names != nullptr) { + for (size_t i = 0; i < sk_CRYPTO_BUFFER_num(ca_names); ++i) { + CRYPTO_BUFFER* buffer = sk_CRYPTO_BUFFER_value(ca_names, i); + cert_authorities.push_back( + std::string(reinterpret_cast<const char*>(CRYPTO_BUFFER_data(buffer)), + CRYPTO_BUFFER_len(buffer))); + } + } + + // OnCertificateRequested returns true if the implementation intends to + // provide the client certificate asynchronously, in which case we suspend the + // handshake. + if (proof_handler_->OnCertificateRequested(cert_authorities)) { + return -1; + } + return 1; +} + void TlsClientHandshaker::WriteMessage(EncryptionLevel level, absl::string_view data) { if (level == ENCRYPTION_HANDSHAKE && state_ < HANDSHAKE_PROCESSED) {
diff --git a/quiche/quic/core/tls_client_handshaker.h b/quiche/quic/core/tls_client_handshaker.h index 447f7b6..aaa5311 100644 --- a/quiche/quic/core/tls_client_handshaker.h +++ b/quiche/quic/core/tls_client_handshaker.h
@@ -126,6 +126,8 @@ void InsertSession(bssl::UniquePtr<SSL_SESSION> session) override; + int OnClientCertRequested(SSL* ssl) override; + bool PrepareZeroRttConfig(QuicResumptionState* cached_state); QuicSession* session() { return session_; }
diff --git a/quiche/quic/core/tls_client_handshaker_test.cc b/quiche/quic/core/tls_client_handshaker_test.cc index 4587072..a5df77e 100644 --- a/quiche/quic/core/tls_client_handshaker_test.cc +++ b/quiche/quic/core/tls_client_handshaker_test.cc
@@ -31,6 +31,7 @@ #include "quiche/quic/core/quic_types.h" #include "quiche/quic/core/quic_versions.h" #include "quiche/quic/platform/api/quic_expect_bug.h" +#include "quiche/quic/platform/api/quic_flags.h" #include "quiche/quic/platform/api/quic_test.h" #include "quiche/quic/test_tools/crypto_test_utils.h" #include "quiche/quic/test_tools/quic_connection_peer.h" @@ -38,6 +39,7 @@ #include "quiche/quic/test_tools/quic_session_peer.h" #include "quiche/quic/test_tools/quic_test_utils.h" #include "quiche/quic/test_tools/simple_session_cache.h" +#include "quiche/quic/test_tools/test_certificates.h" #include "quiche/quic/tools/fake_proof_verifier.h" using testing::_; @@ -326,8 +328,10 @@ // Send a zero-length ServerHello from server to client. char bogus_handshake_message[] = { // Handshake struct (RFC 8446 appendix B.3) - 2, // HandshakeType server_hello - 0, 0, 0, // uint24 length + 2, // HandshakeType server_hello + 0, + 0, + 0, // uint24 length }; stream()->crypto_message_parser()->ProcessInput( absl::string_view(bogus_handshake_message, @@ -1194,6 +1198,52 @@ } } +TEST_P(TlsClientHandshakerTest, HandshakeSuspendedOnClientCertRequest) { + SetQuicRestartFlag(quic_client_cert_support, true); + crypto_config_ = std::make_unique<QuicCryptoClientConfig>( + std::make_unique<FakeProofVerifier>(), nullptr); + CreateConnection(); + crypto_config_->set_proof_source(nullptr); + InitializeFakeServer(); + server_session_->set_client_cert_mode(ClientCertMode::kRequest); + server_session_->Initialize(); + + std::vector<std::string> requested_cert_authorities; + EXPECT_CALL(*session_, OnCertificateRequested) + .WillOnce(testing::DoAll(testing::SaveArg<0>(&requested_cert_authorities), + testing::Return(true))); + stream()->CryptoConnect(); + + // Advance the handshake until the CertificateRequest message is processed. + crypto_test_utils::AdvanceHandshake(connection_, stream(), 0, + server_connection_, server_stream(), 0); + + // Verify that the handshake is suspended (not complete, but not failed). + EXPECT_FALSE(stream()->one_rtt_keys_available()); + EXPECT_TRUE(connection_->connected()); +} + +TEST_P(TlsClientHandshakerTest, HandshakeCompletesWithPreconfiguredCert) { + auto proof_source = std::make_unique<DefaultClientProofSource>(); + auto chain = quiche::QuicheReferenceCountedPointer<ProofSource::Chain>( + new ProofSource::Chain({std::string(quic::test::kTestCertificate)})); + std::unique_ptr<CertificatePrivateKey> key = + CertificatePrivateKey::LoadFromDer( + quic::test::kTestCertificatePrivateKey); + + proof_source->AddCertAndKey({"*"}, std::move(chain), std::move(*key)); + crypto_config_->set_proof_source(std::move(proof_source)); + + InitializeFakeServer(); + server_session_->set_client_cert_mode(ClientCertMode::kRequire); + + // Handshake should complete normally because the cert is proactive. + CompleteCryptoHandshake(); + + EXPECT_TRUE(stream()->one_rtt_keys_available()); + EXPECT_TRUE(stream()->encryption_established()); +} + } // namespace } // namespace test } // namespace quic
diff --git a/quiche/quic/core/tls_handshaker.cc b/quiche/quic/core/tls_handshaker.cc index 96dcf00..9eca933 100644 --- a/quiche/quic/core/tls_handshaker.cc +++ b/quiche/quic/core/tls_handshaker.cc
@@ -153,6 +153,14 @@ return; } int ssl_error = SSL_get_error(ssl(), rv); + + if (GetQuicRestartFlag(quic_client_cert_support) && + ssl_error == SSL_ERROR_WANT_X509_LOOKUP) { + QUIC_RESTART_FLAG_COUNT_N(quic_client_cert_support, 2, 2); + // Handshake is suspended until the client certificate is set. + return; + } + if (ssl_error == expected_ssl_error_) { return; }
diff --git a/quiche/quic/qbone/qbone_client_session.cc b/quiche/quic/qbone/qbone_client_session.cc index fa88c17..9af3394 100644 --- a/quiche/quic/qbone/qbone_client_session.cc +++ b/quiche/quic/qbone/qbone_client_session.cc
@@ -107,6 +107,11 @@ void QboneClientSession::OnProofVerifyDetailsAvailable( const ProofVerifyDetails& verify_details) {} +bool QboneClientSession::OnCertificateRequested( + const std::vector<std::string>& cert_authorities) { + return false; +} + bool QboneClientSession::HasActiveRequests() const { return GetNumActiveStreams() + num_draining_streams() > 0; }
diff --git a/quiche/quic/qbone/qbone_client_session.h b/quiche/quic/qbone/qbone_client_session.h index b0bd5cf..8a36c7e 100644 --- a/quiche/quic/qbone/qbone_client_session.h +++ b/quiche/quic/qbone/qbone_client_session.h
@@ -72,6 +72,8 @@ void OnProofValid(const QuicCryptoClientConfig::CachedState& cached) override; void OnProofVerifyDetailsAvailable( const ProofVerifyDetails& verify_details) override; + bool OnCertificateRequested( + const std::vector<std::string>& cert_authorities) override; QuicServerId server_id() { return server_id_; } QuicCryptoClientConfig* crypto_client_config() {
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h index 390345b..3ecd487 100644 --- a/quiche/quic/test_tools/quic_test_utils.h +++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -1153,6 +1153,8 @@ MOCK_METHOD(std::vector<std::string>, GetAlpnsToOffer, (), (const, override)); MOCK_METHOD(void, OnAlpnSelected, (absl::string_view), (override)); MOCK_METHOD(void, OnConfigNegotiated, (), (override)); + MOCK_METHOD(bool, OnCertificateRequested, + (const std::vector<std::string>& cert_authorities), (override)); QuicCryptoClientStream* GetMutableCryptoStream() override; const QuicCryptoClientStream* GetCryptoStream() const override;