| // Copyright 2013 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/multi_log_ct_verifier.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_samples.h" |
| #include "base/metrics/statistics_recorder.h" |
| #include "base/values.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/ct_log_verifier.h" |
| #include "net/cert/ct_serialization.h" |
| #include "net/cert/pem.h" |
| #include "net/cert/sct_status_flags.h" |
| #include "net/cert/signed_certificate_timestamp.h" |
| #include "net/cert/signed_certificate_timestamp_and_status.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/log/net_log_source_type.h" |
| #include "net/log/net_log_with_source.h" |
| #include "net/log/test_net_log.h" |
| #include "net/log/test_net_log_util.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/ct_test_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::_; |
| using testing::Mock; |
| |
| namespace net { |
| |
| namespace { |
| |
| const char kHostname[] = "example.com"; |
| const char kLogDescription[] = "somelog"; |
| |
| class DoNothingLogProvider : public MultiLogCTVerifier::CTLogProvider { |
| public: |
| DoNothingLogProvider() = default; |
| ~DoNothingLogProvider() = default; |
| }; |
| |
| class MultiLogCTVerifierTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| scoped_refptr<const CTLogVerifier> log( |
| CTLogVerifier::Create(ct::GetTestPublicKey(), kLogDescription)); |
| ASSERT_TRUE(log); |
| log_verifiers_.push_back(log); |
| |
| DoNothingLogProvider notifier; |
| verifier_ = std::make_unique<MultiLogCTVerifier>(¬ifier); |
| verifier_->SetLogs(log_verifiers_); |
| std::string der_test_cert(ct::GetDerEncodedX509Cert()); |
| chain_ = X509Certificate::CreateFromBytes( |
| base::as_bytes(base::make_span(der_test_cert))); |
| ASSERT_TRUE(chain_.get()); |
| |
| embedded_sct_chain_ = |
| CreateCertificateChainFromFile(GetTestCertsDirectory(), |
| "ct-test-embedded-cert.pem", |
| X509Certificate::FORMAT_AUTO); |
| ASSERT_TRUE(embedded_sct_chain_.get()); |
| } |
| |
| bool CheckForEmbeddedSCTInNetLog( |
| const RecordingNetLogObserver& net_log_observer) { |
| auto entries = net_log_observer.GetEntries(); |
| if (entries.size() != 2) |
| return false; |
| |
| auto embedded_scts = |
| GetOptionalStringValueFromParams(entries[0], "embedded_scts"); |
| if (!embedded_scts || embedded_scts->empty()) |
| return false; |
| |
| const NetLogEntry& parsed = entries[1]; |
| if (parsed.params.empty()) { |
| return false; |
| } |
| |
| const base::Value::List* scts = parsed.params.FindList("scts"); |
| if (!scts || scts->size() != 1) |
| return false; |
| |
| const base::Value& the_sct = (*scts)[0]; |
| if (!the_sct.is_dict()) |
| return false; |
| |
| const std::string* origin = the_sct.GetDict().FindString("origin"); |
| if (!origin || *origin != "Embedded in certificate") |
| return false; |
| |
| const std::string* verification_status = |
| the_sct.GetDict().FindString("verification_status"); |
| if (!verification_status || *verification_status != "Verified") |
| return false; |
| |
| return true; |
| } |
| |
| // Returns true if |chain| is a certificate with embedded SCTs that can be |
| // successfully extracted. |
| bool VerifySinglePrecertificateChain(scoped_refptr<X509Certificate> chain) { |
| SignedCertificateTimestampAndStatusList scts; |
| verifier_->Verify(kHostname, chain.get(), base::StringPiece(), |
| base::StringPiece(), &scts, NetLogWithSource()); |
| return !scts.empty(); |
| } |
| |
| // Returns true if |chain| is a certificate with a single embedded SCT that |
| // can be successfully extracted and matched to the test log indicated by |
| // |kLogDescription|. |
| bool CheckPrecertificateVerification(scoped_refptr<X509Certificate> chain) { |
| SignedCertificateTimestampAndStatusList scts; |
| RecordingNetLogObserver net_log_observer(NetLogCaptureMode::kDefault); |
| NetLogWithSource net_log = NetLogWithSource::Make( |
| NetLog::Get(), NetLogSourceType::SSL_CONNECT_JOB); |
| verifier_->Verify(kHostname, chain.get(), base::StringPiece(), |
| base::StringPiece(), &scts, net_log); |
| return ct::CheckForSingleVerifiedSCTInResult(scts, kLogDescription) && |
| ct::CheckForSCTOrigin( |
| scts, ct::SignedCertificateTimestamp::SCT_EMBEDDED) && |
| CheckForEmbeddedSCTInNetLog(net_log_observer); |
| } |
| |
| // Histogram-related helper methods |
| int GetValueFromHistogram(const std::string& histogram_name, |
| int sample_index) { |
| base::Histogram* histogram = static_cast<base::Histogram*>( |
| base::StatisticsRecorder::FindHistogram(histogram_name)); |
| |
| if (histogram == nullptr) |
| return 0; |
| |
| std::unique_ptr<base::HistogramSamples> samples = |
| histogram->SnapshotSamples(); |
| return samples->GetCount(sample_index); |
| } |
| |
| int NumEmbeddedSCTsInHistogram() { |
| return GetValueFromHistogram("Net.CertificateTransparency.SCTOrigin", |
| ct::SignedCertificateTimestamp::SCT_EMBEDDED); |
| } |
| |
| int NumValidSCTsInStatusHistogram() { |
| return GetValueFromHistogram("Net.CertificateTransparency.SCTStatus", |
| ct::SCT_STATUS_OK); |
| } |
| |
| protected: |
| std::unique_ptr<MultiLogCTVerifier> verifier_; |
| scoped_refptr<X509Certificate> chain_; |
| scoped_refptr<X509Certificate> embedded_sct_chain_; |
| std::vector<scoped_refptr<const CTLogVerifier>> log_verifiers_; |
| }; |
| |
| TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCT) { |
| ASSERT_TRUE(CheckPrecertificateVerification(embedded_sct_chain_)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithPreCA) { |
| scoped_refptr<X509Certificate> chain( |
| CreateCertificateChainFromFile(GetTestCertsDirectory(), |
| "ct-test-embedded-with-preca-chain.pem", |
| X509Certificate::FORMAT_AUTO)); |
| ASSERT_TRUE(chain.get()); |
| ASSERT_TRUE(CheckPrecertificateVerification(chain)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithIntermediate) { |
| scoped_refptr<X509Certificate> chain(CreateCertificateChainFromFile( |
| GetTestCertsDirectory(), |
| "ct-test-embedded-with-intermediate-chain.pem", |
| X509Certificate::FORMAT_AUTO)); |
| ASSERT_TRUE(chain.get()); |
| ASSERT_TRUE(CheckPrecertificateVerification(chain)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, |
| VerifiesEmbeddedSCTWithIntermediateAndPreCA) { |
| scoped_refptr<X509Certificate> chain(CreateCertificateChainFromFile( |
| GetTestCertsDirectory(), |
| "ct-test-embedded-with-intermediate-preca-chain.pem", |
| X509Certificate::FORMAT_AUTO)); |
| ASSERT_TRUE(chain.get()); |
| ASSERT_TRUE(CheckPrecertificateVerification(chain)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, VerifiesSCTOverX509Cert) { |
| std::string sct_list = ct::GetSCTListForTesting(); |
| |
| SignedCertificateTimestampAndStatusList scts; |
| verifier_->Verify(kHostname, chain_.get(), base::StringPiece(), sct_list, |
| &scts, NetLogWithSource()); |
| ASSERT_TRUE(ct::CheckForSingleVerifiedSCTInResult(scts, kLogDescription)); |
| ASSERT_TRUE(ct::CheckForSCTOrigin( |
| scts, ct::SignedCertificateTimestamp::SCT_FROM_TLS_EXTENSION)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromUnknownLog) { |
| std::string sct_list = ct::GetSCTListWithInvalidSCT(); |
| SignedCertificateTimestampAndStatusList scts; |
| |
| verifier_->Verify(kHostname, chain_.get(), base::StringPiece(), sct_list, |
| &scts, NetLogWithSource()); |
| EXPECT_EQ(1U, scts.size()); |
| EXPECT_EQ("", scts[0].sct->log_description); |
| EXPECT_EQ(ct::SCT_STATUS_LOG_UNKNOWN, scts[0].status); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, CountsValidSCTsInStatusHistogram) { |
| int num_valid_scts = NumValidSCTsInStatusHistogram(); |
| |
| ASSERT_TRUE(VerifySinglePrecertificateChain(embedded_sct_chain_)); |
| |
| EXPECT_EQ(num_valid_scts + 1, NumValidSCTsInStatusHistogram()); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, CountsInvalidSCTsInStatusHistogram) { |
| std::string sct_list = ct::GetSCTListWithInvalidSCT(); |
| SignedCertificateTimestampAndStatusList scts; |
| |
| int num_valid_scts = NumValidSCTsInStatusHistogram(); |
| int num_invalid_scts = GetValueFromHistogram( |
| "Net.CertificateTransparency.SCTStatus", ct::SCT_STATUS_LOG_UNKNOWN); |
| |
| verifier_->Verify(kHostname, chain_.get(), base::StringPiece(), sct_list, |
| &scts, NetLogWithSource()); |
| |
| ASSERT_EQ(num_valid_scts, NumValidSCTsInStatusHistogram()); |
| ASSERT_EQ(num_invalid_scts + 1, |
| GetValueFromHistogram("Net.CertificateTransparency.SCTStatus", |
| ct::SCT_STATUS_LOG_UNKNOWN)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, CountsSingleEmbeddedSCTInOriginsHistogram) { |
| int old_embedded_count = NumEmbeddedSCTsInHistogram(); |
| ASSERT_TRUE(CheckPrecertificateVerification(embedded_sct_chain_)); |
| EXPECT_EQ(old_embedded_count + 1, NumEmbeddedSCTsInHistogram()); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, SetLogsRemovesOldLogs) { |
| log_verifiers_.clear(); |
| verifier_->SetLogs(log_verifiers_); |
| // Log list is now empty so verification should fail. |
| ASSERT_FALSE(CheckPrecertificateVerification(embedded_sct_chain_)); |
| } |
| |
| TEST_F(MultiLogCTVerifierTest, SetLogsAddsNewLogs) { |
| // Clear the log list. |
| log_verifiers_.clear(); |
| verifier_->SetLogs(log_verifiers_); |
| |
| // Add valid log again via SetLogs |
| scoped_refptr<const CTLogVerifier> log( |
| CTLogVerifier::Create(ct::GetTestPublicKey(), kLogDescription)); |
| ASSERT_TRUE(log); |
| log_verifiers_.push_back(log); |
| verifier_->SetLogs(log_verifiers_); |
| |
| // Verification should now succeed. |
| ASSERT_TRUE(CheckPrecertificateVerification(embedded_sct_chain_)); |
| } |
| |
| } // namespace |
| |
| } // namespace net |