Request a new client certificate if a cached one is stale.
If an SSLPrivateKey is backed by a smartcard or other interesting
module, the handle may eventually stop working. In particular, the
smartcard may be removed at some point.
Ideally, the OS would provide reliable fine-grained signals to clear
relevant the cache entries, but the OS tends not to provide these APIs.
We do drop the cache entry on failure, but the user is required to retry
the operation.
Instead, if an SSLPrivateKey was grabbed from the SSLClientAuthCache,
assume it is potentially stale. Should the signing operation fail, we
can not only drop the cache entry, but retry the request.
This CL does not implement this logic for proxy client certificates,
only server client certificates. Proxy client certificates a missing the
cache clearing logic (https://crbug.com/814911), so we can fill this in
once the plumbing is in place.
Along the way, fill in some URLRequest-level client certificate unit
tests.
Bug: 813022
Change-Id: I9f0450e9f4df1383dd8b73d0297ebea5e3368fec
Reviewed-on: https://chromium-review.googlesource.com/935723
Reviewed-by: Ryan Sleevi <[email protected]>
Commit-Queue: David Benjamin <[email protected]>
Cr-Commit-Position: refs/heads/master@{#539022}
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
index 96ebd3e..aca3e65 100644
--- a/net/http/http_network_transaction.cc
+++ b/net/http/http_network_transaction.cc
@@ -88,6 +88,7 @@
priority_(priority),
headers_valid_(false),
can_send_early_data_(false),
+ server_ssl_client_cert_was_cached_(false),
request_headers_(),
read_buf_len_(0),
total_received_bytes_(0),
@@ -899,9 +900,9 @@
return HandleHttp11Required(result);
}
- // Handle possible handshake errors that may have occurred if the stream
- // used SSL for one or more of the layers.
- result = HandleSSLHandshakeError(result);
+ // Handle possible client certificate errors that may have occurred if the
+ // stream used SSL for one or more of the layers.
+ result = HandleSSLClientAuthError(result);
// At this point we are done with the stream_request_.
stream_request_.reset();
@@ -1486,6 +1487,10 @@
return error;
}
+ if (!response_.cert_request_info->is_proxy) {
+ server_ssl_client_cert_was_cached_ = true;
+ }
+
// TODO(davidben): Add a unit test which covers this path; we need to be
// able to send a legitimate certificate and also bypass/clear the
// SSL session cache.
@@ -1514,21 +1519,31 @@
return OK;
}
-void HttpNetworkTransaction::HandleClientAuthError(int error) {
+int HttpNetworkTransaction::HandleSSLClientAuthError(int error) {
+ // TODO(davidben): This does handle client certificate errors from the
+ // proxy. https://crbug.com/814911.
if (server_ssl_config_.send_client_cert &&
(error == ERR_SSL_PROTOCOL_ERROR || IsClientCertificateError(error))) {
session_->ssl_client_auth_cache()->Remove(
HostPortPair::FromURL(request_->url));
- }
-}
-// TODO(rch): This does not correctly handle errors when an SSL proxy is
-// being used, as all of the errors are handled as if they were generated
-// by the endpoint host, request_->url, rather than considering if they were
-// generated by the SSL proxy. http://crbug.com/69329
-int HttpNetworkTransaction::HandleSSLHandshakeError(int error) {
- DCHECK(request_);
- HandleClientAuthError(error);
+ // The private key handle may have gone stale due to, e.g., the user
+ // unplugging their smartcard. Operating systems do not provide reliable
+ // notifications for this, so if the signature failed and the private key
+ // came from SSLClientAuthCache, retry to ask the user for a new one.
+ if (error == ERR_SSL_CLIENT_AUTH_SIGNATURE_FAILED &&
+ server_ssl_client_cert_was_cached_ && !HasExceededMaxRetries()) {
+ server_ssl_client_cert_was_cached_ = false;
+ server_ssl_config_.send_client_cert = false;
+ server_ssl_config_.client_cert = nullptr;
+ server_ssl_config_.client_private_key = nullptr;
+ retry_attempts_++;
+ net_log_.AddEventWithNetErrorCode(
+ NetLogEventType::HTTP_TRANSACTION_RESTART_AFTER_ERROR, error);
+ ResetConnectionAndRequestForResend();
+ return OK;
+ }
+ }
return error;
}
@@ -1539,7 +1554,7 @@
int HttpNetworkTransaction::HandleIOError(int error) {
// Because the peer may request renegotiation with client authentication at
// any time, check and handle client authentication errors.
- HandleClientAuthError(error);
+ error = HandleSSLClientAuthError(error);
switch (error) {
// If we try to reuse a connection that the server is in the process of
diff --git a/net/http/http_network_transaction.h b/net/http/http_network_transaction.h
index dced0ff7..2fafc4b5 100644
--- a/net/http/http_network_transaction.h
+++ b/net/http/http_network_transaction.h
@@ -232,13 +232,10 @@
// ERR_PROXY_HTTP_1_1_REQUIRED has to be handled.
int HandleHttp11Required(int error);
- // Called to possibly handle a client authentication error.
- void HandleClientAuthError(int error);
-
- // Called to possibly recover from an SSL handshake error. Sets next_state_
+ // Called to possibly handle a client authentication error. Sets next_state_
// and returns OK if recovering from the error. Otherwise, the same error
// code is returned.
- int HandleSSLHandshakeError(int error);
+ int HandleSSLClientAuthError(int error);
// Called to possibly recover from the given error. Sets next_state_ and
// returns OK if recovering from the error. Otherwise, the same error code
@@ -342,6 +339,10 @@
// True if we can send the request over early data.
bool can_send_early_data_;
+ // True if |server_ssl_config_.client_cert| was looked up from the
+ // SSLClientAuthCache, rather than provided externally by the caller.
+ bool server_ssl_client_cert_was_cached_;
+
SSLConfig server_ssl_config_;
SSLConfig proxy_ssl_config_;
diff --git a/net/url_request/url_request_unittest.cc b/net/url_request/url_request_unittest.cc
index 737dda3c..d6ca14e 100644
--- a/net/url_request/url_request_unittest.cc
+++ b/net/url_request/url_request_unittest.cc
@@ -95,8 +95,10 @@
#include "net/socket/socket_test_util.h"
#include "net/socket/ssl_client_socket.h"
#include "net/ssl/channel_id_service.h"
+#include "net/ssl/client_cert_identity_test_util.h"
#include "net/ssl/default_channel_id_store.h"
#include "net/ssl/ssl_connection_status_flags.h"
+#include "net/ssl/ssl_private_key.h"
#include "net/ssl/ssl_server_config.h"
#include "net/ssl/token_binding.h"
#include "net/test/cert_test_util.h"
@@ -10006,19 +10008,51 @@
int on_certificate_requested_count_;
};
+class TestSSLPrivateKey : public SSLPrivateKey {
+ public:
+ explicit TestSSLPrivateKey(scoped_refptr<SSLPrivateKey> key)
+ : key_(std::move(key)) {}
+
+ void set_fail_signing(bool fail_signing) { fail_signing_ = fail_signing; }
+ int sign_count() const { return sign_count_; }
+
+ std::vector<uint16_t> GetAlgorithmPreferences() override {
+ return key_->GetAlgorithmPreferences();
+ }
+ void Sign(uint16_t algorithm,
+ base::span<const uint8_t> input,
+ const SignCallback& callback) override {
+ sign_count_++;
+ if (fail_signing_) {
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::BindOnce(callback, ERR_SSL_CLIENT_AUTH_SIGNATURE_FAILED,
+ std::vector<uint8_t>()));
+ } else {
+ key_->Sign(algorithm, input, callback);
+ }
+ }
+
+ private:
+ ~TestSSLPrivateKey() override = default;
+
+ scoped_refptr<SSLPrivateKey> key_;
+ bool fail_signing_ = false;
+ int sign_count_ = 0;
+};
+
} // namespace
// TODO(davidben): Test the rest of the code. Specifically,
// - Filtering which certificates to select.
-// - Sending a certificate back.
// - Getting a certificate request in an SSL renegotiation sending the
// HTTP request.
-TEST_F(HTTPSRequestTest, ClientAuthTest) {
+TEST_F(HTTPSRequestTest, ClientAuthNoCertificate) {
EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS);
net::SSLServerConfig ssl_config;
ssl_config.client_cert_type =
SSLServerConfig::ClientCertType::OPTIONAL_CLIENT_CERT;
- test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config);
+ test_server.SetSSLConfig(EmbeddedTestServer::CERT_OK, ssl_config);
test_server.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("net/data/ssl")));
ASSERT_TRUE(test_server.Start());
@@ -10045,12 +10079,246 @@
base::RunLoop().Run();
+ EXPECT_EQ(OK, d.request_status());
EXPECT_EQ(1, d.response_started_count());
EXPECT_FALSE(d.received_data_before_response());
EXPECT_NE(0, d.bytes_received());
}
}
+TEST_F(HTTPSRequestTest, ClientAuth) {
+ std::unique_ptr<FakeClientCertIdentity> identity =
+ FakeClientCertIdentity::CreateFromCertAndKeyFiles(
+ GetTestCertsDirectory(), "client_1.pem", "client_1.pk8");
+ ASSERT_TRUE(identity);
+ scoped_refptr<TestSSLPrivateKey> private_key =
+ base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key());
+
+ EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ net::SSLServerConfig ssl_config;
+ ssl_config.client_cert_type =
+ SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT;
+ test_server.SetSSLConfig(EmbeddedTestServer::CERT_OK, ssl_config);
+ test_server.AddDefaultHandlers(
+ base::FilePath(FILE_PATH_LITERAL("net/data/ssl")));
+ ASSERT_TRUE(test_server.Start());
+
+ {
+ SSLClientAuthTestDelegate d;
+ std::unique_ptr<URLRequest> r(default_context_.CreateRequest(
+ test_server.GetURL("/defaultresponse"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ base::RunLoop().Run();
+
+ EXPECT_EQ(1, d.on_certificate_requested_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+
+ // Send a certificate.
+ r->ContinueWithCertificate(identity->certificate(), private_key);
+
+ base::RunLoop().Run();
+
+ EXPECT_EQ(OK, d.request_status());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_NE(0, d.bytes_received());
+
+ // The private key should have been used.
+ EXPECT_EQ(1, private_key->sign_count());
+ }
+
+ // Close all connections and clear the session cache to force a new handshake.
+ default_context_.http_transaction_factory()
+ ->GetSession()
+ ->CloseAllConnections();
+ SSLClientSocket::ClearSessionCache();
+
+ // Connecting again should not call OnCertificateRequested. The identity is
+ // taken from the client auth cache.
+ {
+ SSLClientAuthTestDelegate d;
+ std::unique_ptr<URLRequest> r(default_context_.CreateRequest(
+ test_server.GetURL("/defaultresponse"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ base::RunLoop().Run();
+
+ EXPECT_EQ(OK, d.request_status());
+ EXPECT_EQ(0, d.on_certificate_requested_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_NE(0, d.bytes_received());
+
+ // The private key should have been used.
+ EXPECT_EQ(2, private_key->sign_count());
+ }
+}
+
+// Test that private keys that fail to sign anything get evicted from the cache.
+TEST_F(HTTPSRequestTest, ClientAuthFailSigning) {
+ std::unique_ptr<FakeClientCertIdentity> identity =
+ FakeClientCertIdentity::CreateFromCertAndKeyFiles(
+ GetTestCertsDirectory(), "client_1.pem", "client_1.pk8");
+ ASSERT_TRUE(identity);
+ scoped_refptr<TestSSLPrivateKey> private_key =
+ base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key());
+ private_key->set_fail_signing(true);
+
+ EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ net::SSLServerConfig ssl_config;
+ ssl_config.client_cert_type =
+ SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT;
+ test_server.SetSSLConfig(EmbeddedTestServer::CERT_OK, ssl_config);
+ test_server.AddDefaultHandlers(
+ base::FilePath(FILE_PATH_LITERAL("net/data/ssl")));
+ ASSERT_TRUE(test_server.Start());
+
+ {
+ SSLClientAuthTestDelegate d;
+ std::unique_ptr<URLRequest> r(default_context_.CreateRequest(
+ test_server.GetURL("/defaultresponse"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+ base::RunLoop().Run();
+
+ EXPECT_EQ(1, d.on_certificate_requested_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+
+ // Send a certificate.
+ r->ContinueWithCertificate(identity->certificate(), private_key);
+ base::RunLoop().Run();
+
+ // The private key cannot sign anything, so we report an error.
+ EXPECT_EQ(ERR_SSL_CLIENT_AUTH_SIGNATURE_FAILED, d.request_status());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+
+ // The private key should have been used.
+ EXPECT_EQ(1, private_key->sign_count());
+ }
+
+ // Close all connections and clear the session cache to force a new handshake.
+ default_context_.http_transaction_factory()
+ ->GetSession()
+ ->CloseAllConnections();
+ SSLClientSocket::ClearSessionCache();
+
+ // The bad identity should have been evicted from the cache, so connecting
+ // again should call OnCertificateRequested again.
+ {
+ SSLClientAuthTestDelegate d;
+ std::unique_ptr<URLRequest> r(default_context_.CreateRequest(
+ test_server.GetURL("/defaultresponse"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ base::RunLoop().Run();
+
+ EXPECT_EQ(1, d.on_certificate_requested_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+
+ // There should have been no additional uses of the private key.
+ EXPECT_EQ(1, private_key->sign_count());
+ }
+}
+
+// Test that cached private keys that fail to sign anything trigger a
+// retry. This is so we handle unplugged smartcards
+// gracefully. https://crbug.com/813022.
+TEST_F(HTTPSRequestTest, ClientAuthFailSigningRetry) {
+ std::unique_ptr<FakeClientCertIdentity> identity =
+ FakeClientCertIdentity::CreateFromCertAndKeyFiles(
+ GetTestCertsDirectory(), "client_1.pem", "client_1.pk8");
+ ASSERT_TRUE(identity);
+ scoped_refptr<TestSSLPrivateKey> private_key =
+ base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key());
+
+ EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS);
+ net::SSLServerConfig ssl_config;
+ ssl_config.client_cert_type =
+ SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT;
+ test_server.SetSSLConfig(EmbeddedTestServer::CERT_OK, ssl_config);
+ test_server.AddDefaultHandlers(
+ base::FilePath(FILE_PATH_LITERAL("net/data/ssl")));
+ ASSERT_TRUE(test_server.Start());
+
+ // Connect with a client certificate to put it in the client auth cache.
+ {
+ SSLClientAuthTestDelegate d;
+ std::unique_ptr<URLRequest> r(default_context_.CreateRequest(
+ test_server.GetURL("/defaultresponse"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ base::RunLoop().Run();
+
+ EXPECT_EQ(1, d.on_certificate_requested_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+
+ r->ContinueWithCertificate(identity->certificate(), private_key);
+ base::RunLoop().Run();
+
+ EXPECT_EQ(OK, d.request_status());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_NE(0, d.bytes_received());
+
+ // The private key should have been used.
+ EXPECT_EQ(1, private_key->sign_count());
+ }
+
+ // Close all connections and clear the session cache to force a new handshake.
+ default_context_.http_transaction_factory()
+ ->GetSession()
+ ->CloseAllConnections();
+ SSLClientSocket::ClearSessionCache();
+
+ // Cause the private key to fail. Connecting again should attempt to use it,
+ // notice the failure, and then request a new identity via
+ // OnCertificateRequested.
+ private_key->set_fail_signing(true);
+
+ {
+ SSLClientAuthTestDelegate d;
+ std::unique_ptr<URLRequest> r(default_context_.CreateRequest(
+ test_server.GetURL("/defaultresponse"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ base::RunLoop().Run();
+
+ // There was an additional signing call on the private key (the one which
+ // failed).
+ EXPECT_EQ(2, private_key->sign_count());
+
+ // That caused another OnCertificateRequested call.
+ EXPECT_EQ(1, d.on_certificate_requested_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+ }
+}
+
TEST_F(HTTPSRequestTest, ResumeTest) {
// Test that we attempt a session resume when making two connections to the
// same host.