Add FingerprintProofVerifier, a proof verifier that checks against a predefined set of certificate fingerprints. This will be used to implement <https://wicg.github.io/web-transport/#dom-quictransportconfiguration-server_certificate_fingerprints>. PiperOrigin-RevId: 314717008 Change-Id: I0a745f442618936c501e6324382c95806af369bb
diff --git a/quic/core/quic_time.h b/quic/core/quic_time.h index 93ea308..079b59a 100644 --- a/quic/core/quic_time.h +++ b/quic/core/quic_time.h
@@ -188,6 +188,10 @@ return microseconds_ == other.microseconds_; } + QuicTime::Delta operator-(const QuicWallTime& rhs) const { + return QuicTime::Delta::FromMicroseconds(microseconds_ - rhs.microseconds_); + } + private: explicit constexpr QuicWallTime(uint64_t microseconds) : microseconds_(microseconds) {}
diff --git a/quic/quic_transport/web_transport_fingerprint_proof_verifier.cc b/quic/quic_transport/web_transport_fingerprint_proof_verifier.cc new file mode 100644 index 0000000..15cd706 --- /dev/null +++ b/quic/quic_transport/web_transport_fingerprint_proof_verifier.cc
@@ -0,0 +1,220 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/third_party/quiche/src/quic/quic_transport/web_transport_fingerprint_proof_verifier.h" + +#include <cstdint> +#include <memory> + +#include "third_party/boringssl/src/include/openssl/sha.h" +#include "net/third_party/quiche/src/quic/core/crypto/certificate_view.h" +#include "net/third_party/quiche/src/quic/core/quic_time.h" +#include "net/third_party/quiche/src/quic/core/quic_types.h" +#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_str_cat.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_text_utils.h" + +namespace quic { +namespace { + +constexpr size_t kFingerprintLength = SHA256_DIGEST_LENGTH * 3 - 1; + +constexpr std::array<char, 16> kHexDigits = {'0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', + 'c', 'd', 'e', 'f'}; + +// Assumes that the character is normalized to lowercase beforehand. +bool IsNormalizedHexDigit(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); +} + +void NormalizeFingerprint(CertificateFingerprint& fingerprint) { + fingerprint.fingerprint = + quiche::QuicheTextUtils::ToLower(fingerprint.fingerprint); +} + +} // namespace + +constexpr char CertificateFingerprint::kSha256[]; + +std::string ComputeSha256Fingerprint(quiche::QuicheStringPiece input) { + std::vector<uint8_t> raw_hash; + raw_hash.resize(SHA256_DIGEST_LENGTH); + SHA256(reinterpret_cast<const uint8_t*>(input.data()), input.size(), + raw_hash.data()); + + std::string output; + output.resize(kFingerprintLength); + for (size_t i = 0; i < output.size(); i++) { + uint8_t hash_byte = raw_hash[i / 3]; + switch (i % 3) { + case 0: + output[i] = kHexDigits[hash_byte >> 4]; + break; + case 1: + output[i] = kHexDigits[hash_byte & 0xf]; + break; + case 2: + output[i] = ':'; + break; + } + } + return output; +} + +ProofVerifyDetails* WebTransportFingerprintProofVerifier::Details::Clone() + const { + return new Details(*this); +} + +WebTransportFingerprintProofVerifier::WebTransportFingerprintProofVerifier( + const QuicClock* clock, + int max_validity_days) + : clock_(clock), + max_validity_days_(max_validity_days), + // Add an extra second to max validity to accomodate various edge cases. + max_validity_( + QuicTime::Delta::FromSeconds(max_validity_days * 86400 + 1)) {} + +bool WebTransportFingerprintProofVerifier::AddFingerprint( + CertificateFingerprint fingerprint) { + NormalizeFingerprint(fingerprint); + if (fingerprint.algorithm != CertificateFingerprint::kSha256) { + QUIC_DLOG(WARNING) << "Algorithms other than SHA-256 are not supported"; + return false; + } + if (fingerprint.fingerprint.size() != kFingerprintLength) { + QUIC_DLOG(WARNING) << "Invalid fingerprint length"; + return false; + } + for (size_t i = 0; i < fingerprint.fingerprint.size(); i++) { + char current = fingerprint.fingerprint[i]; + if (i % 3 == 2) { + if (current != ':') { + QUIC_DLOG(WARNING) + << "Missing colon separator between the bytes of the hash"; + return false; + } + } else { + if (!IsNormalizedHexDigit(current)) { + QUIC_DLOG(WARNING) << "Fingerprint must be in hexadecimal"; + return false; + } + } + } + + fingerprints_.push_back(fingerprint); + return true; +} + +QuicAsyncStatus WebTransportFingerprintProofVerifier::VerifyProof( + const std::string& /*hostname*/, + const uint16_t /*port*/, + const std::string& /*server_config*/, + QuicTransportVersion /*transport_version*/, + quiche::QuicheStringPiece /*chlo_hash*/, + const std::vector<std::string>& /*certs*/, + const std::string& /*cert_sct*/, + const std::string& /*signature*/, + const ProofVerifyContext* /*context*/, + std::string* error_details, + std::unique_ptr<ProofVerifyDetails>* details, + std::unique_ptr<ProofVerifierCallback> /*callback*/) { + *error_details = + "QUIC crypto certificate verification is not supported in " + "WebTransportFingerprintProofVerifier"; + QUIC_BUG << *error_details; + *details = std::make_unique<Details>(Status::kInternalError); + return QUIC_FAILURE; +} + +QuicAsyncStatus WebTransportFingerprintProofVerifier::VerifyCertChain( + const std::string& /*hostname*/, + const std::vector<std::string>& certs, + const std::string& /*ocsp_response*/, + const std::string& /*cert_sct*/, + const ProofVerifyContext* /*context*/, + std::string* error_details, + std::unique_ptr<ProofVerifyDetails>* details, + std::unique_ptr<ProofVerifierCallback> /*callback*/) { + if (certs.empty()) { + *details = std::make_unique<Details>(Status::kInternalError); + *error_details = "No certificates provided"; + return QUIC_FAILURE; + } + + if (!HasKnownFingerprint(certs[0])) { + *details = std::make_unique<Details>(Status::kUnknownFingerprint); + *error_details = "Certificate does not match any fingerprint"; + return QUIC_FAILURE; + } + + std::unique_ptr<CertificateView> view = + CertificateView::ParseSingleCertificate(certs[0]); + if (view == nullptr) { + *details = std::make_unique<Details>(Status::kCertificateParseFailure); + *error_details = "Failed to parse the certificate"; + return QUIC_FAILURE; + } + + if (!HasValidExpiry(*view)) { + *details = std::make_unique<Details>(Status::kExpiryTooLong); + *error_details = quiche::QuicheStrCat( + "Certificate expiry exceeds the configured limit of ", + max_validity_days_, " days"); + return QUIC_FAILURE; + } + + if (!IsWithinValidityPeriod(*view)) { + *details = std::make_unique<Details>(Status::kExpired); + *error_details = + "Certificate has expired or has validity listed in the future"; + return QUIC_FAILURE; + } + + *details = std::make_unique<Details>(Status::kValidCertificate); + return QUIC_SUCCESS; +} + +std::unique_ptr<ProofVerifyContext> +WebTransportFingerprintProofVerifier::CreateDefaultContext() { + return nullptr; +} + +bool WebTransportFingerprintProofVerifier::HasKnownFingerprint( + quiche::QuicheStringPiece der_certificate) { + // https://wicg.github.io/web-transport/#verify-a-certificate-fingerprint + const std::string fingerprint = ComputeSha256Fingerprint(der_certificate); + for (const CertificateFingerprint& reference : fingerprints_) { + if (reference.algorithm != CertificateFingerprint::kSha256) { + QUIC_BUG << "Unexpected non-SHA-256 hash"; + continue; + } + if (fingerprint == reference.fingerprint) { + return true; + } + } + return false; +} + +bool WebTransportFingerprintProofVerifier::HasValidExpiry( + const CertificateView& certificate) { + if (!certificate.validity_start().IsBefore(certificate.validity_end())) { + return false; + } + + const QuicTime::Delta duration_seconds = + certificate.validity_end() - certificate.validity_start(); + return duration_seconds <= max_validity_; +} + +bool WebTransportFingerprintProofVerifier::IsWithinValidityPeriod( + const CertificateView& certificate) { + QuicWallTime now = clock_->WallNow(); + return now.IsAfter(certificate.validity_start()) && + now.IsBefore(certificate.validity_end()); +} + +} // namespace quic
diff --git a/quic/quic_transport/web_transport_fingerprint_proof_verifier.h b/quic/quic_transport/web_transport_fingerprint_proof_verifier.h new file mode 100644 index 0000000..b17ee87 --- /dev/null +++ b/quic/quic_transport/web_transport_fingerprint_proof_verifier.h
@@ -0,0 +1,118 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_QUIC_QUIC_TRANSPORT_FINGERPRINT_PROOF_VERIFIER_H_ +#define QUICHE_QUIC_QUIC_TRANSPORT_FINGERPRINT_PROOF_VERIFIER_H_ + +#include <vector> + +#include "net/third_party/quiche/src/quic/core/crypto/certificate_view.h" +#include "net/third_party/quiche/src/quic/core/crypto/proof_verifier.h" +#include "net/third_party/quiche/src/quic/core/quic_clock.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h" + +namespace quic { + +// Represents a fingerprint of an X.509 certificate in a format based on +// https://w3c.github.io/webrtc-pc/#dom-rtcdtlsfingerprint. +struct QUIC_EXPORT_PRIVATE CertificateFingerprint { + static constexpr char kSha256[] = "sha-256"; + + // An algorithm described by one of the names in + // https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml + std::string algorithm; + // Hex-encoded, colon-separated fingerprint of the certificate. For example, + // "12:3d:5b:71:8c:54:df:85:7e:bd:e3:7c:66:da:f9:db:6a:94:8f:85:cb:6e:44:7f:09:3e:05:f2:dd:d4:f7:86" + std::string fingerprint; +}; + +// Computes a SHA-256 fingerprint of the specified input formatted in the same +// format as CertificateFingerprint::fingerprint would contain. +QUIC_EXPORT_PRIVATE std::string ComputeSha256Fingerprint( + quiche::QuicheStringPiece input); + +// WebTransportFingerprintProofVerifier verifies the server leaf certificate +// against a supplied list of certificate fingerprints following the procedure +// described in the WebTransport specification. The certificate is deemed +// trusted if it matches a fingerprint in the list, has expiry dates that are +// not too long and has not expired. Only the leaf is checked, the rest of the +// chain is ignored. Reference specification: +// https://wicg.github.io/web-transport/#dom-quictransportconfiguration-server_certificate_fingerprints +class QUIC_EXPORT_PRIVATE WebTransportFingerprintProofVerifier + : public ProofVerifier { + public: + // Note: the entries in this list may be logged into a UMA histogram, and thus + // should not be renumbered. + enum class Status { + kValidCertificate = 0, + kUnknownFingerprint = 1, + kCertificateParseFailure = 2, + kExpiryTooLong = 3, + kExpired = 4, + kInternalError = 5, + + kMaxValue = kInternalError, + }; + + class QUIC_EXPORT_PRIVATE Details : public ProofVerifyDetails { + public: + explicit Details(Status status) : status_(status) {} + Status status() const { return status_; } + + ProofVerifyDetails* Clone() const override; + + private: + const Status status_; + }; + + // |clock| is used to check if the certificate has expired. It is not owned + // and must outlive the object. |max_validity_days| is the maximum time for + // which the certificate is allowed to be valid. + WebTransportFingerprintProofVerifier(const QuicClock* clock, + int max_validity_days); + + // Adds a certificate fingerprint to be trusted. The fingerprints are + // case-insensitive and are validated internally; the function returns true if + // the validation passes. + bool AddFingerprint(CertificateFingerprint fingerprint); + + // ProofVerifier implementation. + QuicAsyncStatus VerifyProof( + const std::string& hostname, + const uint16_t port, + const std::string& server_config, + QuicTransportVersion transport_version, + quiche::QuicheStringPiece chlo_hash, + const std::vector<std::string>& certs, + const std::string& cert_sct, + const std::string& signature, + const ProofVerifyContext* context, + std::string* error_details, + std::unique_ptr<ProofVerifyDetails>* details, + std::unique_ptr<ProofVerifierCallback> callback) override; + QuicAsyncStatus VerifyCertChain( + const std::string& hostname, + const std::vector<std::string>& certs, + const std::string& ocsp_response, + const std::string& cert_sct, + const ProofVerifyContext* context, + std::string* error_details, + std::unique_ptr<ProofVerifyDetails>* details, + std::unique_ptr<ProofVerifierCallback> callback) override; + std::unique_ptr<ProofVerifyContext> CreateDefaultContext() override; + + private: + bool HasKnownFingerprint(quiche::QuicheStringPiece der_certificate); + bool HasValidExpiry(const CertificateView& certificate); + bool IsWithinValidityPeriod(const CertificateView& certificate); + + const QuicClock* clock_; // Unowned. + const int max_validity_days_; + const QuicTime::Delta max_validity_; + std::vector<CertificateFingerprint> fingerprints_; +}; + +} // namespace quic + +#endif // QUICHE_QUIC_QUIC_TRANSPORT_FINGERPRINT_PROOF_VERIFIER_H_
diff --git a/quic/quic_transport/web_transport_fingerprint_proof_verifier_test.cc b/quic/quic_transport/web_transport_fingerprint_proof_verifier_test.cc new file mode 100644 index 0000000..b79e873 --- /dev/null +++ b/quic/quic_transport/web_transport_fingerprint_proof_verifier_test.cc
@@ -0,0 +1,184 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/third_party/quiche/src/quic/quic_transport/web_transport_fingerprint_proof_verifier.h" + +#include <memory> + +#include "net/third_party/quiche/src/quic/core/quic_types.h" +#include "net/third_party/quiche/src/quic/platform/api/quic_test.h" +#include "net/third_party/quiche/src/quic/test_tools/mock_clock.h" +#include "net/third_party/quiche/src/quic/test_tools/test_certificates.h" +#include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h" + +namespace quic { +namespace test { +namespace { + +using ::testing::HasSubstr; + +// 2020-02-01 12:35:56 UTC +constexpr QuicTime::Delta kValidTime = QuicTime::Delta::FromSeconds(1580560556); + +struct VerifyResult { + QuicAsyncStatus status; + WebTransportFingerprintProofVerifier::Status detailed_status; + std::string error; +}; + +class WebTransportFingerprintProofVerifierTest : public QuicTest { + public: + WebTransportFingerprintProofVerifierTest() { + clock_.AdvanceTime(kValidTime); + verifier_ = std::make_unique<WebTransportFingerprintProofVerifier>( + &clock_, /*max_validity_days=*/365); + AddTestCertificate(); + } + + protected: + VerifyResult Verify(quiche::QuicheStringPiece certificate) { + VerifyResult result; + std::unique_ptr<ProofVerifyDetails> details; + result.status = verifier_->VerifyCertChain( + /*hostname=*/"", std::vector<std::string>{std::string(certificate)}, + /*ocsp_response=*/"", + /*cert_sct=*/"", + /*context=*/nullptr, &result.error, &details, + /*callback=*/nullptr); + result.detailed_status = + static_cast<WebTransportFingerprintProofVerifier::Details*>( + details.get()) + ->status(); + return result; + } + + void AddTestCertificate() { + EXPECT_TRUE(verifier_->AddFingerprint(CertificateFingerprint{ + .algorithm = CertificateFingerprint::kSha256, + .fingerprint = ComputeSha256Fingerprint(kTestCertificate)})); + } + + MockClock clock_; + std::unique_ptr<WebTransportFingerprintProofVerifier> verifier_; +}; + +TEST_F(WebTransportFingerprintProofVerifierTest, Sha256Fingerprint) { + // Computed using `openssl x509 -fingerprint -sha256`. + EXPECT_EQ(ComputeSha256Fingerprint(kTestCertificate), + "f2:e5:46:5e:2b:f7:ec:d6:f6:30:66:a5:a3:75:11:73:4a:a0:eb:7c:47:01:" + "0e:86:d6:75:8e:d4:f4:fa:1b:0f"); +} + +TEST_F(WebTransportFingerprintProofVerifierTest, SimpleFingerprint) { + VerifyResult result = Verify(kTestCertificate); + EXPECT_EQ(result.status, QUIC_SUCCESS); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kValidCertificate); + + result = Verify(kWildcardCertificate); + EXPECT_EQ(result.status, QUIC_FAILURE); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kUnknownFingerprint); + + result = Verify("Some random text"); + EXPECT_EQ(result.status, QUIC_FAILURE); +} + +TEST_F(WebTransportFingerprintProofVerifierTest, Validity) { + // Validity periods of kTestCertificate, according to `openssl x509 -text`: + // Not Before: Jan 30 18:13:59 2020 GMT + // Not After : Feb 2 18:13:59 2020 GMT + + // 2020-01-29 19:00:00 UTC + constexpr QuicTime::Delta kStartTime = + QuicTime::Delta::FromSeconds(1580324400); + clock_.Reset(); + clock_.AdvanceTime(kStartTime); + + VerifyResult result = Verify(kTestCertificate); + EXPECT_EQ(result.status, QUIC_FAILURE); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kExpired); + + clock_.AdvanceTime(QuicTime::Delta::FromSeconds(86400)); + result = Verify(kTestCertificate); + EXPECT_EQ(result.status, QUIC_SUCCESS); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kValidCertificate); + + clock_.AdvanceTime(QuicTime::Delta::FromSeconds(4 * 86400)); + result = Verify(kTestCertificate); + EXPECT_EQ(result.status, QUIC_FAILURE); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kExpired); +} + +TEST_F(WebTransportFingerprintProofVerifierTest, MaxValidity) { + verifier_ = std::make_unique<WebTransportFingerprintProofVerifier>( + &clock_, /*max_validity_days=*/2); + AddTestCertificate(); + VerifyResult result = Verify(kTestCertificate); + EXPECT_EQ(result.status, QUIC_FAILURE); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kExpiryTooLong); + EXPECT_THAT(result.error, HasSubstr("limit of 2 days")); + + // kTestCertificate is valid for exactly four days. + verifier_ = std::make_unique<WebTransportFingerprintProofVerifier>( + &clock_, /*max_validity_days=*/4); + AddTestCertificate(); + result = Verify(kTestCertificate); + EXPECT_EQ(result.status, QUIC_SUCCESS); + EXPECT_EQ(result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kValidCertificate); +} + +TEST_F(WebTransportFingerprintProofVerifierTest, InvalidCertificate) { + constexpr quiche::QuicheStringPiece kInvalidCertificate = "Hello, world!"; + ASSERT_TRUE(verifier_->AddFingerprint(CertificateFingerprint{ + .algorithm = CertificateFingerprint::kSha256, + .fingerprint = ComputeSha256Fingerprint(kInvalidCertificate)})); + + VerifyResult result = Verify(kInvalidCertificate); + EXPECT_EQ(result.status, QUIC_FAILURE); + EXPECT_EQ( + result.detailed_status, + WebTransportFingerprintProofVerifier::Status::kCertificateParseFailure); +} + +TEST_F(WebTransportFingerprintProofVerifierTest, AddCertificate) { + // Accept all-uppercase fingerprints. + verifier_ = std::make_unique<WebTransportFingerprintProofVerifier>( + &clock_, /*max_validity_days=*/365); + EXPECT_TRUE(verifier_->AddFingerprint(CertificateFingerprint{ + .algorithm = CertificateFingerprint::kSha256, + .fingerprint = "F2:E5:46:5E:2B:F7:EC:D6:F6:30:66:A5:A3:75:11:73:4A:A0:EB:" + "7C:47:01:0E:86:D6:75:8E:D4:F4:FA:1B:0F"})); + EXPECT_EQ(Verify(kTestCertificate).detailed_status, + WebTransportFingerprintProofVerifier::Status::kValidCertificate); + + // Reject unknown hash algorithms. + EXPECT_FALSE(verifier_->AddFingerprint(CertificateFingerprint{ + .algorithm = "sha-1", + .fingerprint = + "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"})); + // Reject invalid length. + EXPECT_FALSE(verifier_->AddFingerprint( + CertificateFingerprint{.algorithm = CertificateFingerprint::kSha256, + .fingerprint = "00:00:00:00"})); + // Reject missing colons. + EXPECT_FALSE(verifier_->AddFingerprint(CertificateFingerprint{ + .algorithm = CertificateFingerprint::kSha256, + .fingerprint = "00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00." + "00.00.00.00.00.00.00.00.00.00.00.00.00"})); + // Reject non-hex symbols. + EXPECT_FALSE(verifier_->AddFingerprint(CertificateFingerprint{ + .algorithm = CertificateFingerprint::kSha256, + .fingerprint = "zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:" + "zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz"})); +} + +} // namespace +} // namespace test +} // namespace quic
diff --git a/quic/test_tools/mock_clock.cc b/quic/test_tools/mock_clock.cc index 1761dd9..21c080a 100644 --- a/quic/test_tools/mock_clock.cc +++ b/quic/test_tools/mock_clock.cc
@@ -14,6 +14,10 @@ now_ = now_ + delta; } +void MockClock::Reset() { + now_ = QuicTime::Zero(); +} + QuicTime MockClock::Now() const { return now_; }
diff --git a/quic/test_tools/mock_clock.h b/quic/test_tools/mock_clock.h index 4bd51e9..2ce2e96 100644 --- a/quic/test_tools/mock_clock.h +++ b/quic/test_tools/mock_clock.h
@@ -24,6 +24,8 @@ // Advances the current time by |delta|, which may be negative. void AdvanceTime(QuicTime::Delta delta); + // Resets time back to zero. + void Reset(); private: QuicTime now_;