| // Copyright 2018 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 "content/browser/web_package/signed_exchange_signature_verifier.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/big_endian.h" |
| #include "base/command_line.h" |
| #include "base/containers/span.h" |
| #include "base/format_macros.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "content/browser/web_package/signed_exchange_consts.h" |
| #include "content/browser/web_package/signed_exchange_envelope.h" |
| #include "content/browser/web_package/signed_exchange_signature_header_field.h" |
| #include "content/browser/web_package/signed_exchange_utils.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "crypto/sha2.h" |
| #include "crypto/signature_verifier.h" |
| #include "net/cert/asn1_util.h" |
| #include "net/cert/x509_util.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "third_party/boringssl/src/include/openssl/bytestring.h" |
| #include "third_party/boringssl/src/include/openssl/ec.h" |
| #include "third_party/boringssl/src/include/openssl/ec_key.h" |
| #include "third_party/boringssl/src/include/openssl/evp.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#signature-validity |
| // Step 5. "Let message be the concatenation of the following byte strings." |
| constexpr uint8_t kMessageHeader[] = |
| // 5.1. "A string that consists of octet 32 (0x20) repeated 64 times." |
| // [spec text] |
| "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20" |
| "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20" |
| "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20" |
| "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20" |
| // 5.2. "A context string: the ASCII encoding of "HTTP Exchange 1"." ... |
| // "but implementations of drafts MUST NOT use it and MUST use another |
| // draft-specific string beginning with "HTTP Exchange 1 " instead." |
| // [spec text] |
| // 5.3. "A single 0 byte which serves as a separator." [spec text] |
| "HTTP Exchange 1 b3"; |
| |
| constexpr base::TimeDelta kOneWeek = base::TimeDelta::FromDays(7); |
| constexpr base::TimeDelta kFourWeeks = base::TimeDelta::FromDays(4 * 7); |
| |
| base::Optional<crypto::SignatureVerifier::SignatureAlgorithm> |
| GetSignatureAlgorithm(scoped_refptr<net::X509Certificate> cert, |
| SignedExchangeDevToolsProxy* devtools_proxy) { |
| TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), "GetSignatureAlgorithm"); |
| base::StringPiece spki; |
| if (!net::asn1::ExtractSPKIFromDERCert( |
| net::x509_util::CryptoBufferAsStringPiece(cert->cert_buffer()), |
| &spki)) { |
| signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy, |
| "Failed to extract SPKI."); |
| return base::nullopt; |
| } |
| |
| CBS cbs; |
| CBS_init(&cbs, reinterpret_cast<const uint8_t*>(spki.data()), spki.size()); |
| bssl::UniquePtr<EVP_PKEY> pkey(EVP_parse_public_key(&cbs)); |
| if (!pkey || CBS_len(&cbs) != 0) { |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, "Failed to parse public key."); |
| return base::nullopt; |
| } |
| |
| int pkey_id = EVP_PKEY_id(pkey.get()); |
| if (pkey_id != EVP_PKEY_EC) { |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, |
| base::StringPrintf("Unsupported public key type: %d. Only ECDSA keys " |
| "on the secp256r1 curve are supported.", |
| pkey_id)); |
| return base::nullopt; |
| } |
| |
| const EC_GROUP* group = EC_KEY_get0_group(EVP_PKEY_get0_EC_KEY(pkey.get())); |
| int curve_name = EC_GROUP_get_curve_name(group); |
| if (curve_name == NID_X9_62_prime256v1) |
| return crypto::SignatureVerifier::ECDSA_SHA256; |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, |
| base::StringPrintf("Unsupported EC group: %d. Only ECDSA keys on the " |
| "secp256r1 curve are supported.", |
| curve_name)); |
| return base::nullopt; |
| } |
| |
| bool VerifySignature(base::span<const uint8_t> sig, |
| base::span<const uint8_t> msg, |
| scoped_refptr<net::X509Certificate> cert, |
| crypto::SignatureVerifier::SignatureAlgorithm algorithm, |
| SignedExchangeDevToolsProxy* devtools_proxy) { |
| TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), "VerifySignature"); |
| crypto::SignatureVerifier verifier; |
| if (!net::x509_util::SignatureVerifierInitWithCertificate( |
| &verifier, algorithm, sig, cert->cert_buffer())) { |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, "SignatureVerifierInitWithCertificate failed."); |
| return false; |
| } |
| verifier.VerifyUpdate(msg); |
| if (!verifier.VerifyFinal()) { |
| signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy, |
| "VerifyFinal failed."); |
| return false; |
| } |
| return true; |
| } |
| |
| std::string HexDump(const std::vector<uint8_t>& msg) { |
| std::string output; |
| for (const auto& byte : msg) { |
| base::StringAppendF(&output, "%02x", byte); |
| } |
| return output; |
| } |
| |
| void AppendToBuf8BytesBigEndian(std::vector<uint8_t>* buf, uint64_t n) { |
| char encoded[8]; |
| base::WriteBigEndian(encoded, n); |
| buf->insert(buf->end(), std::begin(encoded), std::end(encoded)); |
| } |
| |
| std::vector<uint8_t> GenerateSignedMessage( |
| SignedExchangeVersion version, |
| const SignedExchangeEnvelope& envelope) { |
| TRACE_EVENT_BEGIN0(TRACE_DISABLED_BY_DEFAULT("loading"), |
| "GenerateSignedMessage"); |
| |
| const auto signature = envelope.signature(); |
| |
| // https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#signature-validity |
| // Step 5. "Let message be the concatenation of the following byte strings." |
| std::vector<uint8_t> message; |
| // see kMessageHeader for Steps 5.1 to 5.3. |
| message.insert(message.end(), std::begin(kMessageHeader), |
| std::end(kMessageHeader)); |
| |
| // Step 5.4. "If cert-sha256 is set, a byte holding the value 32 followed by |
| // the 32 bytes of the value of cert-sha256. Otherwise a 0 byte." [spec text] |
| // Note: cert-sha256 must be set for application/signed-exchange envelope |
| // format. |
| message.push_back(32); |
| const auto& cert_sha256 = envelope.signature().cert_sha256.value(); |
| message.insert(message.end(), std::begin(cert_sha256.data), |
| std::end(cert_sha256.data)); |
| |
| // Step 5.5. "The 8-byte big-endian encoding of the length in bytes of |
| // validity-url, followed by the bytes of validity-url." [spec text] |
| const auto& validity_url_bytes = signature.validity_url.raw_string; |
| AppendToBuf8BytesBigEndian(&message, validity_url_bytes.size()); |
| message.insert(message.end(), std::begin(validity_url_bytes), |
| std::end(validity_url_bytes)); |
| |
| // Step 5.6. "The 8-byte big-endian encoding of date." [spec text] |
| AppendToBuf8BytesBigEndian(&message, signature.date); |
| |
| // Step 5.7. "The 8-byte big-endian encoding of expires." [spec text] |
| AppendToBuf8BytesBigEndian(&message, signature.expires); |
| |
| // Step 5.8. "The 8-byte big-endian encoding of the length in bytes of |
| // requestUrl, followed by the bytes of requestUrl." [spec text] |
| const auto& request_url_bytes = envelope.request_url().raw_string; |
| |
| AppendToBuf8BytesBigEndian(&message, request_url_bytes.size()); |
| message.insert(message.end(), std::begin(request_url_bytes), |
| std::end(request_url_bytes)); |
| |
| // Step 5.9. "The 8-byte big-endian encoding of the length in bytes of |
| // headers, followed by the bytes of headers." [spec text] |
| AppendToBuf8BytesBigEndian(&message, envelope.cbor_header().size()); |
| message.insert(message.end(), envelope.cbor_header().begin(), |
| envelope.cbor_header().end()); |
| |
| TRACE_EVENT_END1(TRACE_DISABLED_BY_DEFAULT("loading"), |
| "GenerateSignedMessage", "dump", HexDump(message)); |
| return message; |
| } |
| |
| base::Time TimeFromSignedExchangeUnixTime(uint64_t t) { |
| return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(t); |
| } |
| |
| SignedExchangeSignatureVerifier::Result VerifyValidityPeriod( |
| const SignedExchangeEnvelope& envelope) { |
| base::Time expires_time = |
| TimeFromSignedExchangeUnixTime(envelope.signature().expires); |
| base::Time creation_time = |
| TimeFromSignedExchangeUnixTime(envelope.signature().date); |
| |
| // 3. "If expires is more than 7 days (604800 seconds) after date, return |
| // "invalid"." [spec text] |
| if ((expires_time - creation_time).InSeconds() > kOneWeek.InSeconds()) { |
| return SignedExchangeSignatureVerifier::Result::kErrValidityPeriodTooLong; |
| } |
| return SignedExchangeSignatureVerifier::Result::kSuccess; |
| } |
| |
| // Implements "Signature validity" of |
| // https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#signature-validity |
| SignedExchangeSignatureVerifier::Result VerifyTimestamps( |
| const SignedExchangeEnvelope& envelope, |
| const base::Time& verification_time) { |
| base::Time expires_time = |
| TimeFromSignedExchangeUnixTime(envelope.signature().expires); |
| base::Time creation_time = |
| TimeFromSignedExchangeUnixTime(envelope.signature().date); |
| |
| // 4. "If the current time is before date or after expires, return |
| // "invalid"." |
| if (verification_time < creation_time) { |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "SignedExchange.SignatureVerificationError.NotYetValid", |
| (creation_time - verification_time).InSeconds(), 1, |
| kFourWeeks.InSeconds(), 50); |
| return SignedExchangeSignatureVerifier::Result::kErrFutureDate; |
| } |
| if (expires_time < verification_time) { |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "SignedExchange.SignatureVerificationError.Expired", |
| (verification_time - expires_time).InSeconds(), 1, |
| kFourWeeks.InSeconds(), 50); |
| return SignedExchangeSignatureVerifier::Result::kErrExpired; |
| } |
| |
| UMA_HISTOGRAM_CUSTOM_COUNTS("SignedExchange.TimeUntilExpiration", |
| (expires_time - verification_time).InSeconds(), 1, |
| kOneWeek.InSeconds(), 50); |
| return SignedExchangeSignatureVerifier::Result::kSuccess; |
| } |
| |
| // Returns true if SPKI hash of |certificate| is included in the |
| // --ignore-certificate-errors-spki-list command line flag, and |
| // ContentBrowserClient::CanIgnoreCertificateErrorIfNeeded() returns true. |
| bool ShouldIgnoreTimestampError( |
| scoped_refptr<net::X509Certificate> certificate) { |
| static base::NoDestructor< |
| SignedExchangeSignatureVerifier::IgnoreErrorsSPKIList> |
| instance(*base::CommandLine::ForCurrentProcess()); |
| return instance->ShouldIgnoreError(certificate); |
| } |
| |
| } // namespace |
| |
| SignedExchangeSignatureVerifier::Result SignedExchangeSignatureVerifier::Verify( |
| SignedExchangeVersion version, |
| const SignedExchangeEnvelope& envelope, |
| scoped_refptr<net::X509Certificate> certificate, |
| const base::Time& verification_time, |
| SignedExchangeDevToolsProxy* devtools_proxy) { |
| SCOPED_UMA_HISTOGRAM_TIMER("SignedExchange.Time.SignatureVerify"); |
| TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), |
| "SignedExchangeSignatureVerifier::Verify"); |
| DCHECK(certificate); |
| const auto validity_period_result = VerifyValidityPeriod(envelope); |
| if (validity_period_result != Result::kSuccess) { |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, |
| base::StringPrintf( |
| "Specified validity period too long. creation_time: %" PRIu64 |
| ", expires_time: %" PRIu64 ", verification_time: %" PRIu64, |
| envelope.signature().date, envelope.signature().expires, |
| (verification_time - base::Time::UnixEpoch()).InSeconds())); |
| return validity_period_result; |
| } |
| const auto timestamp_result = VerifyTimestamps(envelope, verification_time); |
| if (timestamp_result != Result::kSuccess && |
| !ShouldIgnoreTimestampError(certificate)) { |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, |
| base::StringPrintf( |
| "Invalid timestamp. creation_time: %" PRIu64 |
| ", expires_time: %" PRIu64 ", verification_time: %" PRIu64, |
| envelope.signature().date, envelope.signature().expires, |
| (verification_time - base::Time::UnixEpoch()).InSeconds())); |
| return timestamp_result; |
| } |
| // Currently we don't support ed25519key. So |cert_sha256| must be set. |
| DCHECK(envelope.signature().cert_sha256.has_value()); |
| |
| // The main-certificate is the first certificate in certificate-chain. |
| if (*envelope.signature().cert_sha256 != |
| net::X509Certificate::CalculateFingerprint256( |
| certificate->cert_buffer())) { |
| signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy, |
| "cert-sha256 mismatch."); |
| return Result::kErrCertificateSHA256Mismatch; |
| } |
| |
| auto message = GenerateSignedMessage(version, envelope); |
| |
| base::Optional<crypto::SignatureVerifier::SignatureAlgorithm> algorithm = |
| GetSignatureAlgorithm(certificate, devtools_proxy); |
| if (!algorithm) |
| return Result::kErrUnsupportedCertType; |
| |
| const std::string& sig = envelope.signature().sig; |
| if (!VerifySignature( |
| base::make_span(reinterpret_cast<const uint8_t*>(sig.data()), |
| sig.size()), |
| message, certificate, *algorithm, devtools_proxy)) { |
| signed_exchange_utils::ReportErrorAndTraceEvent( |
| devtools_proxy, "Failed to verify signature \"sig\"."); |
| return Result::kErrSignatureVerificationFailed; |
| } |
| return Result::kSuccess; |
| } |
| |
| SignedExchangeSignatureVerifier::IgnoreErrorsSPKIList::IgnoreErrorsSPKIList( |
| const std::string& spki_list) { |
| Parse(spki_list); |
| } |
| |
| SignedExchangeSignatureVerifier::IgnoreErrorsSPKIList::IgnoreErrorsSPKIList( |
| const base::CommandLine& command_line) { |
| if (!GetContentClient()->browser()->CanIgnoreCertificateErrorIfNeeded()) |
| return; |
| Parse(command_line.GetSwitchValueASCII( |
| network::switches::kIgnoreCertificateErrorsSPKIList)); |
| } |
| |
| void SignedExchangeSignatureVerifier::IgnoreErrorsSPKIList::Parse( |
| const std::string& spki_list) { |
| hash_set_ = |
| network::IgnoreErrorsCertVerifier::MakeWhitelist(base::SplitString( |
| spki_list, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)); |
| } |
| |
| SignedExchangeSignatureVerifier::IgnoreErrorsSPKIList::~IgnoreErrorsSPKIList() = |
| default; |
| |
| bool SignedExchangeSignatureVerifier::IgnoreErrorsSPKIList::ShouldIgnoreError( |
| scoped_refptr<net::X509Certificate> certificate) { |
| if (hash_set_.empty()) |
| return false; |
| |
| base::StringPiece spki; |
| if (!net::asn1::ExtractSPKIFromDERCert( |
| net::x509_util::CryptoBufferAsStringPiece(certificate->cert_buffer()), |
| &spki)) { |
| return false; |
| } |
| net::SHA256HashValue hash; |
| crypto::SHA256HashString(spki, &hash, sizeof(net::SHA256HashValue)); |
| return hash_set_.find(hash) != hash_set_.end(); |
| } |
| |
| } // namespace content |