| // Copyright 2016 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/cert/caching_cert_verifier.h" |
| |
| #include <memory> |
| |
| #include "base/files/file_path.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/test/task_environment.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/test_completion_callback.h" |
| #include "net/cert/cert_database.h" |
| #include "net/cert/cert_verifier.h" |
| #include "net/cert/cert_verify_result.h" |
| #include "net/cert/mock_cert_verifier.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/cert/x509_util.h" |
| #include "net/log/net_log_with_source.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/ct_test_util.h" |
| #include "net/test/gtest_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "net/test/test_with_task_environment.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using net::test::IsError; |
| using net::test::IsOk; |
| |
| using testing::_; |
| using testing::Mock; |
| using testing::Return; |
| using testing::ReturnRef; |
| |
| namespace net { |
| |
| class CachingCertVerifierTest : public TestWithTaskEnvironment { |
| public: |
| CachingCertVerifierTest() : verifier_(std::make_unique<MockCertVerifier>()) {} |
| ~CachingCertVerifierTest() override = default; |
| |
| protected: |
| CachingCertVerifier verifier_; |
| }; |
| |
| TEST_F(CachingCertVerifierTest, CacheHit) { |
| base::FilePath certs_dir = GetTestCertsDirectory(); |
| scoped_refptr<X509Certificate> test_cert( |
| ImportCertFromFile(certs_dir, "ok_cert.pem")); |
| ASSERT_TRUE(test_cert.get()); |
| |
| int error; |
| CertVerifyResult verify_result; |
| TestCompletionCallback callback; |
| std::unique_ptr<CertVerifier::Request> request; |
| |
| error = callback.GetResult(verifier_.Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource())); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_EQ(1u, verifier_.requests()); |
| ASSERT_EQ(0u, verifier_.cache_hits()); |
| ASSERT_EQ(1u, verifier_.GetCacheSize()); |
| |
| error = verifier_.Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| // Synchronous completion. |
| ASSERT_NE(ERR_IO_PENDING, error); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_FALSE(request); |
| ASSERT_EQ(2u, verifier_.requests()); |
| ASSERT_EQ(1u, verifier_.cache_hits()); |
| ASSERT_EQ(1u, verifier_.GetCacheSize()); |
| } |
| |
| TEST_F(CachingCertVerifierTest, CacheHitCTResultsCached) { |
| base::FilePath certs_dir = GetTestCertsDirectory(); |
| scoped_refptr<X509Certificate> test_cert( |
| ImportCertFromFile(certs_dir, "ok_cert.pem")); |
| ASSERT_TRUE(test_cert.get()); |
| |
| auto cert_verifier = std::make_unique<MockCertVerifier>(); |
| // Mock the cert verification and CT verification results. |
| CertVerifyResult mock_result; |
| mock_result.cert_status = OK; |
| mock_result.verified_cert = test_cert; |
| |
| scoped_refptr<ct::SignedCertificateTimestamp> sct; |
| ct::GetX509CertSCT(&sct); |
| SignedCertificateTimestampAndStatus sct_and_status(sct, ct::SCT_STATUS_OK); |
| SignedCertificateTimestampAndStatusList sct_list{sct_and_status}; |
| mock_result.scts = sct_list; |
| cert_verifier->AddResultForCert(test_cert, mock_result, OK); |
| |
| // We don't use verifier_ here because we needed to call AddResultForCert from |
| // the mock verifier. |
| CachingCertVerifier cache_verifier(std::move(cert_verifier)); |
| |
| int result; |
| CertVerifyResult verify_result; |
| TestCompletionCallback callback; |
| std::unique_ptr<CertVerifier::Request> request; |
| |
| result = callback.GetResult(cache_verifier.Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource())); |
| ASSERT_EQ(OK, result); |
| ASSERT_EQ(1u, verify_result.scts.size()); |
| ASSERT_EQ(ct::SCT_STATUS_OK, verify_result.scts[0].status); |
| ASSERT_EQ(1u, cache_verifier.requests()); |
| ASSERT_EQ(0u, cache_verifier.cache_hits()); |
| ASSERT_EQ(1u, cache_verifier.GetCacheSize()); |
| |
| result = cache_verifier.Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| // Synchronous completion. |
| ASSERT_EQ(OK, result); |
| ASSERT_FALSE(request); |
| ASSERT_EQ(1u, verify_result.scts.size()); |
| ASSERT_EQ(ct::SCT_STATUS_OK, verify_result.scts[0].status); |
| ASSERT_EQ(2u, cache_verifier.requests()); |
| ASSERT_EQ(1u, cache_verifier.cache_hits()); |
| ASSERT_EQ(1u, cache_verifier.GetCacheSize()); |
| } |
| |
| // Tests the same server certificate with different intermediate CA |
| // certificates. These should be treated as different certificate chains even |
| // though the two X509Certificate objects contain the same server certificate. |
| TEST_F(CachingCertVerifierTest, DifferentCACerts) { |
| base::FilePath certs_dir = GetTestCertsDirectory(); |
| |
| scoped_refptr<X509Certificate> server_cert = |
| ImportCertFromFile(certs_dir, "salesforce_com_test.pem"); |
| ASSERT_TRUE(server_cert); |
| |
| scoped_refptr<X509Certificate> intermediate_cert1 = |
| ImportCertFromFile(certs_dir, "verisign_intermediate_ca_2011.pem"); |
| ASSERT_TRUE(intermediate_cert1); |
| |
| scoped_refptr<X509Certificate> intermediate_cert2 = |
| ImportCertFromFile(certs_dir, "verisign_intermediate_ca_2016.pem"); |
| ASSERT_TRUE(intermediate_cert2); |
| |
| std::vector<bssl::UniquePtr<CRYPTO_BUFFER>> intermediates; |
| intermediates.push_back(bssl::UpRef(intermediate_cert1->cert_buffer())); |
| scoped_refptr<X509Certificate> cert_chain1 = |
| X509Certificate::CreateFromBuffer(bssl::UpRef(server_cert->cert_buffer()), |
| std::move(intermediates)); |
| ASSERT_TRUE(cert_chain1); |
| |
| intermediates.clear(); |
| intermediates.push_back(bssl::UpRef(intermediate_cert2->cert_buffer())); |
| scoped_refptr<X509Certificate> cert_chain2 = |
| X509Certificate::CreateFromBuffer(bssl::UpRef(server_cert->cert_buffer()), |
| std::move(intermediates)); |
| ASSERT_TRUE(cert_chain2); |
| |
| int error; |
| CertVerifyResult verify_result; |
| TestCompletionCallback callback; |
| std::unique_ptr<CertVerifier::Request> request; |
| |
| error = callback.GetResult(verifier_.Verify( |
| CertVerifier::RequestParams(cert_chain1, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource())); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_EQ(1u, verifier_.requests()); |
| ASSERT_EQ(0u, verifier_.cache_hits()); |
| ASSERT_EQ(1u, verifier_.GetCacheSize()); |
| |
| error = callback.GetResult(verifier_.Verify( |
| CertVerifier::RequestParams(cert_chain2, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource())); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_EQ(2u, verifier_.requests()); |
| ASSERT_EQ(0u, verifier_.cache_hits()); |
| ASSERT_EQ(2u, verifier_.GetCacheSize()); |
| } |
| |
| TEST_F(CachingCertVerifierTest, ObserverIsForwarded) { |
| auto mock_cert_verifier = std::make_unique<MockCertVerifier>(); |
| MockCertVerifier* mock_cert_verifier_ptr = mock_cert_verifier.get(); |
| CachingCertVerifier cache_verifier(std::move(mock_cert_verifier)); |
| |
| CertVerifierObserverCounter observer_(&cache_verifier); |
| EXPECT_EQ(observer_.change_count(), 0u); |
| // A CertVerifierChanged event on the wrapped verifier should be forwarded to |
| // observers registered on CachingCertVerifier. |
| mock_cert_verifier_ptr->SimulateOnCertVerifierChanged(); |
| EXPECT_EQ(observer_.change_count(), 1u); |
| } |
| |
| namespace { |
| enum class ChangeType { |
| kSetConfig, |
| kCertVerifierChanged, |
| kCertDBChanged, |
| }; |
| } // namespace |
| |
| class CachingCertVerifierCacheClearingTest |
| : public testing::TestWithParam<ChangeType> { |
| public: |
| CachingCertVerifierCacheClearingTest() { |
| auto mock_cert_verifier = std::make_unique<MockCertVerifier>(); |
| mock_verifier_ = mock_cert_verifier.get(); |
| verifier_ = |
| std::make_unique<CachingCertVerifier>(std::move(mock_cert_verifier)); |
| } |
| |
| ChangeType change_type() const { return GetParam(); } |
| |
| void DoCacheClearingAction() { |
| switch (change_type()) { |
| case ChangeType::kSetConfig: |
| verifier_->SetConfig({}); |
| break; |
| case ChangeType::kCertVerifierChanged: |
| mock_verifier_->SimulateOnCertVerifierChanged(); |
| break; |
| case ChangeType::kCertDBChanged: |
| CertDatabase::GetInstance()->NotifyObserversCertDBChanged(); |
| base::RunLoop().RunUntilIdle(); |
| break; |
| } |
| } |
| |
| protected: |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| std::unique_ptr<CachingCertVerifier> verifier_; |
| raw_ptr<MockCertVerifier> mock_verifier_; |
| }; |
| |
| TEST_P(CachingCertVerifierCacheClearingTest, CacheClearedSyncVerification) { |
| base::FilePath certs_dir = GetTestCertsDirectory(); |
| scoped_refptr<X509Certificate> test_cert( |
| ImportCertFromFile(certs_dir, "ok_cert.pem")); |
| ASSERT_TRUE(test_cert.get()); |
| |
| mock_verifier_->set_async(false); |
| |
| int error; |
| CertVerifyResult verify_result; |
| TestCompletionCallback callback; |
| std::unique_ptr<CertVerifier::Request> request; |
| |
| error = verifier_->Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_EQ(1u, verifier_->requests()); |
| ASSERT_EQ(0u, verifier_->cache_hits()); |
| ASSERT_EQ(1u, verifier_->GetCacheSize()); |
| |
| DoCacheClearingAction(); |
| ASSERT_EQ(0u, verifier_->GetCacheSize()); |
| |
| error = verifier_->Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_FALSE(request); |
| ASSERT_EQ(2u, verifier_->requests()); |
| ASSERT_EQ(0u, verifier_->cache_hits()); |
| ASSERT_EQ(1u, verifier_->GetCacheSize()); |
| } |
| |
| TEST_P(CachingCertVerifierCacheClearingTest, CacheClearedAsyncVerification) { |
| base::FilePath certs_dir = GetTestCertsDirectory(); |
| scoped_refptr<X509Certificate> test_cert( |
| ImportCertFromFile(certs_dir, "ok_cert.pem")); |
| ASSERT_TRUE(test_cert.get()); |
| |
| mock_verifier_->set_async(true); |
| |
| int error; |
| CertVerifyResult verify_result; |
| TestCompletionCallback callback; |
| std::unique_ptr<CertVerifier::Request> request; |
| |
| error = verifier_->Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| ASSERT_EQ(ERR_IO_PENDING, error); |
| ASSERT_TRUE(request); |
| ASSERT_EQ(1u, verifier_->requests()); |
| ASSERT_EQ(0u, verifier_->cache_hits()); |
| ASSERT_EQ(0u, verifier_->GetCacheSize()); |
| |
| DoCacheClearingAction(); |
| ASSERT_EQ(0u, verifier_->GetCacheSize()); |
| |
| error = callback.WaitForResult(); |
| ASSERT_TRUE(IsCertificateError(error)); |
| // Async result should not have been cached since it was from a verification |
| // started before the config changed. |
| ASSERT_EQ(0u, verifier_->GetCacheSize()); |
| |
| error = verifier_->Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| ASSERT_EQ(ERR_IO_PENDING, error); |
| ASSERT_TRUE(request); |
| ASSERT_EQ(2u, verifier_->requests()); |
| ASSERT_EQ(0u, verifier_->cache_hits()); |
| ASSERT_EQ(0u, verifier_->GetCacheSize()); |
| |
| error = callback.WaitForResult(); |
| ASSERT_TRUE(IsCertificateError(error)); |
| // New async result should be cached since it was from a verification started |
| // after the config changed. |
| ASSERT_EQ(1u, verifier_->GetCacheSize()); |
| |
| // Verify again. Result should be synchronous this time since it will get the |
| // cached result. |
| error = verifier_->Verify( |
| CertVerifier::RequestParams(test_cert, "www.example.com", 0, |
| /*ocsp_response=*/std::string(), |
| /*sct_list=*/std::string()), |
| &verify_result, callback.callback(), &request, NetLogWithSource()); |
| ASSERT_TRUE(IsCertificateError(error)); |
| ASSERT_FALSE(request); |
| ASSERT_EQ(3u, verifier_->requests()); |
| ASSERT_EQ(1u, verifier_->cache_hits()); |
| ASSERT_EQ(1u, verifier_->GetCacheSize()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| CachingCertVerifierCacheClearingTest, |
| testing::Values(ChangeType::kSetConfig, |
| ChangeType::kCertVerifierChanged, |
| ChangeType::kCertDBChanged)); |
| |
| } // namespace net |