blob: 356cd5a8b963e5b02c19f13b9e71492129dc8905 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// 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_utils.h"
#include <string_view>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.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/loader/download_utils_impl.h"
#include "content/browser/web_package/signed_exchange_devtools_proxy.h"
#include "content/browser/web_package/signed_exchange_error.h"
#include "content/browser/web_package/signed_exchange_request_handler.h"
#include "content/common/features.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace content {
namespace signed_exchange_utils {
namespace {
constexpr char kLoadResultHistogram[] = "SignedExchange.LoadResult2";
std::optional<base::Time> g_verification_time_for_testing;
} // namespace
void RecordLoadResultHistogram(SignedExchangeLoadResult result) {
base::UmaHistogramEnumeration(kLoadResultHistogram, result);
}
void ReportErrorAndTraceEvent(
SignedExchangeDevToolsProxy* devtools_proxy,
const std::string& error_message,
std::optional<SignedExchangeError::FieldIndexPair> error_field) {
TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeError", TRACE_EVENT_SCOPE_THREAD, "error",
error_message);
if (devtools_proxy)
devtools_proxy->ReportError(error_message, std::move(error_field));
}
bool IsSignedExchangeHandlingEnabled(BrowserContext* context) {
return GetContentClient()->browser()->AllowSignedExchange(context);
}
bool IsSignedExchangeReportingForDistributorsEnabled() {
return base::FeatureList::IsEnabled(network::features::kReporting);
}
bool ShouldHandleAsSignedHTTPExchange(
const GURL& request_url,
const network::mojom::URLResponseHead& head) {
// Currently we don't support the signed exchange which is returned from a
// service worker.
// TODO(crbug.com/40558902): Decide whether we should support it or not.
if (head.was_fetched_via_service_worker)
return false;
if (!SignedExchangeRequestHandler::IsSupportedMimeType(head.mime_type))
return false;
// Do not handle responses without HttpResponseHeaders.
// (Example: data:application/signed-exchange,)
if (!head.headers.get())
return false;
if (download_utils::MustDownload(/*browser_context=*/nullptr, request_url,
head.headers.get(), head.mime_type)) {
return false;
}
return true;
}
std::optional<SignedExchangeVersion> GetSignedExchangeVersion(
std::string_view content_type) {
// https://wicg.github.io/webpackage/loading.html#signed-exchange-version
// Step 1. Let mimeType be the supplied MIME type of response. [spec text]
// |content_type| is the supplied MIME type.
// Step 2. If mimeType is undefined, return undefined. [spec text]
// Step 3. If mimeType's essence is not "application/signed-exchange", return
// undefined. [spec text]
const std::string::size_type semicolon = content_type.find(';');
const std::string essence = base::ToLowerASCII(base::TrimWhitespaceASCII(
content_type.substr(0, semicolon), base::TRIM_ALL));
if (essence != "application/signed-exchange")
return std::nullopt;
// Step 4.Let params be mimeType's parameters. [spec text]
std::map<std::string, std::string> params;
if (semicolon != std::string_view::npos) {
net::HttpUtil::NameValuePairsIterator parser(
content_type.substr(semicolon + 1), ';');
while (parser.GetNext()) {
params[base::ToLowerASCII(parser.name())] = parser.value();
}
if (!parser.valid())
return std::nullopt;
}
// Step 5. If params["v"] exists, return it. Otherwise, return undefined.
// [spec text]
auto iter = params.find("v");
if (iter != params.end()) {
if (iter->second == "b3")
return std::make_optional(SignedExchangeVersion::kB3);
return std::make_optional(SignedExchangeVersion::kUnknown);
}
return std::nullopt;
}
SignedExchangeLoadResult GetLoadResultFromSignatureVerifierResult(
SignedExchangeSignatureVerifier::Result verify_result) {
switch (verify_result) {
case SignedExchangeSignatureVerifier::Result::kSuccess:
return SignedExchangeLoadResult::kSuccess;
case SignedExchangeSignatureVerifier::Result::kErrCertificateSHA256Mismatch:
// "Handling the certificate reference
// ...
// - If the SHA-256 hash of chain’s leaf's certificate is not equal to
// certSha256, return "signature_verification_error"." [spec text]
return SignedExchangeLoadResult::kSignatureVerificationError;
case SignedExchangeSignatureVerifier::Result::
kErrSignatureVerificationFailed:
// "Validating a signature
// ...
// - If parsedSignature’s signature is not a valid signature of message
// by publicKey using the ecdsa_secp256r1_sha256 algorithm, return
// invalid." [spec text]
//
// "Parsing signed exchanges
// - ...
// - If parsedSignature is not valid for headerBytes and
// requestUrlBytes, and signed exchange version version, return
// "signature_verification_error"." [spec text]
return SignedExchangeLoadResult::kSignatureVerificationError;
case SignedExchangeSignatureVerifier::Result::kErrUnsupportedCertType:
// "Validating a signature
// ...
// - If parsedSignature’s signature is not a valid signature of message
// by publicKey using the ecdsa_secp256r1_sha256 algorithm, return
// invalid." [spec text]
//
// "Parsing signed exchanges
// - ...
// - If parsedSignature is not valid for headerBytes and
// requestUrlBytes, and signed exchange version version, return
// "signature_verification_error"." [spec text]
return SignedExchangeLoadResult::kSignatureVerificationError;
case SignedExchangeSignatureVerifier::Result::kErrValidityPeriodTooLong:
// "Cross-origin trust
// ...
// - If signature’s expiration time is more than 604800 seconds (7 days)
// after signature’s date, return "untrusted"." [spec text]
//
// "Parsing signed exchanges
// - ...
// - If parsedSignature does not establish cross-origin trust for
// parsedExchange, return "cert_verification_error"." [spec text]
return SignedExchangeLoadResult::kCertVerificationError;
case SignedExchangeSignatureVerifier::Result::kErrFutureDate:
case SignedExchangeSignatureVerifier::Result::kErrExpired:
// "Validating a signature
// ...
// - If the UA’s estimate of the current time is more than clockSkew
// before signature’s date, return "untrusted".
// - If the UA’s estimate of the current time is after signature’s
// expiration time, return "untrusted"." [spec text]
//
// "Parsing signed exchanges
// - ...
// - If parsedSignature is not valid for headerBytes and
// requestUrlBytes, and signed exchange version version, return
// "signature_verification_error"." [spec text]
return SignedExchangeLoadResult::kSignatureVerificationError;
// Deprecated error results.
case SignedExchangeSignatureVerifier::Result::kErrNoCertificate_deprecated:
case SignedExchangeSignatureVerifier::Result::
kErrNoCertificateSHA256_deprecated:
case SignedExchangeSignatureVerifier::Result::
kErrInvalidSignatureFormat_deprecated:
case SignedExchangeSignatureVerifier::Result::
kErrInvalidSignatureIntegrity_deprecated:
case SignedExchangeSignatureVerifier::Result::
kErrInvalidTimestamp_deprecated:
NOTREACHED();
}
NOTREACHED();
}
net::RedirectInfo CreateRedirectInfo(
const GURL& new_url,
const network::ResourceRequest& outer_request,
const network::mojom::URLResponseHead& outer_response,
bool is_fallback_redirect) {
// https://wicg.github.io/webpackage/loading.html#mp-http-fetch
// Step 3. Set actualResponse's status to 303. [spec text]
return net::RedirectInfo::ComputeRedirectInfo(
"GET", outer_request.url, outer_request.site_for_cookies,
outer_request.update_first_party_url_on_redirect
? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
: net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL,
outer_request.referrer_policy, outer_request.referrer.spec(),
outer_request.request_initiator, 303, new_url,
net::RedirectUtil::GetReferrerPolicyHeader(outer_response.headers.get()),
false /* insecure_scheme_was_upgraded */, true /* copy_fragment */,
is_fallback_redirect);
}
network::mojom::URLResponseHeadPtr CreateRedirectResponseHead(
const network::mojom::URLResponseHead& outer_response,
bool is_fallback_redirect) {
auto response_head = network::mojom::URLResponseHead::New();
response_head->encoded_data_length = 0;
std::string buf;
std::string link_header;
if (!is_fallback_redirect &&
outer_response.headers) {
link_header = outer_response.headers->GetNormalizedHeader("link").value_or(
std::string());
}
if (link_header.empty()) {
buf = base::StringPrintf("HTTP/1.1 %d %s\r\n", 303, "See Other");
} else {
buf = base::StringPrintf(
"HTTP/1.1 %d %s\r\n"
"link: %s\r\n",
303, "See Other", link_header.c_str());
}
response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(buf));
response_head->encoded_data_length = 0;
response_head->request_start = outer_response.request_start;
response_head->response_start = outer_response.response_start;
response_head->request_time = outer_response.request_time;
response_head->response_time = outer_response.response_time;
response_head->load_timing = outer_response.load_timing;
return response_head;
}
int MakeRequestID() {
// Request ID for browser initiated requests. request_ids generated by
// child processes are counted up from 0, while browser created requests
// start at -2 and go down from there. (We need to start at -2 because -1 is
// used as a special value all over the resource_dispatcher_host for
// uninitialized variables.) This way, we no longer have the unlikely (but
// observed in the real world!) event where we have two requests with the same
// request_id_.
static std::atomic_int request_id(-1);
return --request_id;
}
base::Time GetVerificationTime() {
if (g_verification_time_for_testing)
return *g_verification_time_for_testing;
return base::Time::Now();
}
void SetVerificationTimeForTesting(
std::optional<base::Time> verification_time_for_testing) {
g_verification_time_for_testing = verification_time_for_testing;
}
bool IsCookielessOnlyExchange(const net::HttpResponseHeaders& inner_headers) {
std::optional<std::string_view> value;
size_t iter = 0;
while ((value = inner_headers.EnumerateHeader(&iter, "Vary"))) {
if (base::EqualsCaseInsensitiveASCII(*value, "cookie")) {
return true;
}
}
return false;
}
} // namespace signed_exchange_utils
} // namespace content