blob: 8537b8bbb2ea8c70f9892462035d437f61fe3ac6 [file] [log] [blame]
// 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_cert_fetcher.h"
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/format_macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/data_url_loader_factory.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/browser/web_package/signed_exchange_consts.h"
#include "content/browser/web_package/signed_exchange_devtools_proxy.h"
#include "content/browser/web_package/signed_exchange_reporter.h"
#include "content/browser/web_package/signed_exchange_utils.h"
#include "content/common/single_request_url_loader_factory.h"
#include "content/common/throttling_url_loader.h"
#include "content/public/common/resource_type.h"
#include "content/public/common/url_loader_throttle.h"
#include "ipc/ipc_message.h"
#include "mojo/public/cpp/system/simple_watcher.h"
#include "net/base/ip_endpoint.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "services/network/loader_util.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace content {
namespace {
constexpr char kCertChainMimeType[] = "application/cert-chain+cbor";
// Limit certificate messages to 100k, matching BoringSSL's default limit.
const size_t kMaxCertSizeForSignedExchange = 100 * 1024;
static size_t g_max_cert_size_for_signed_exchange =
kMaxCertSizeForSignedExchange;
void ResetMaxCertSizeForTest() {
g_max_cert_size_for_signed_exchange = kMaxCertSizeForSignedExchange;
}
const net::NetworkTrafficAnnotationTag kCertFetcherTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("sigined_exchange_cert_fetcher", R"(
semantics {
sender: "Certificate Fetcher for Signed HTTP Exchanges"
description:
"Retrieves the X.509v3 certificates to verify the signed headers of "
"Signed HTTP Exchanges."
trigger:
"Navigating Chrome (ex: clicking on a link) to an URL and the server "
"returns a Signed HTTP Exchange."
data: "Arbitrary site-controlled data can be included in the URL."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled by settings. This feature is not "
"enabled by default yet."
policy_exception_justification: "Not implemented."
}
comments:
"Chrome would be unable to handle Signed HTTP Exchanges without this "
"type of request."
)");
} // namespace
// static
std::unique_ptr<SignedExchangeCertFetcher>
SignedExchangeCertFetcher::CreateAndStart(
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
std::vector<std::unique_ptr<URLLoaderThrottle>> throttles,
const GURL& cert_url,
bool force_fetch,
CertificateCallback callback,
SignedExchangeDevToolsProxy* devtools_proxy,
SignedExchangeReporter* reporter,
const base::Optional<base::UnguessableToken>& throttling_profile_id) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::CreateAndStart");
std::unique_ptr<SignedExchangeCertFetcher> cert_fetcher(
new SignedExchangeCertFetcher(std::move(shared_url_loader_factory),
std::move(throttles), cert_url, force_fetch,
std::move(callback), devtools_proxy,
reporter, throttling_profile_id));
cert_fetcher->Start();
return cert_fetcher;
}
// https://wicg.github.io/webpackage/loading.html#handling-cert-url
SignedExchangeCertFetcher::SignedExchangeCertFetcher(
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
std::vector<std::unique_ptr<URLLoaderThrottle>> throttles,
const GURL& cert_url,
bool force_fetch,
CertificateCallback callback,
SignedExchangeDevToolsProxy* devtools_proxy,
SignedExchangeReporter* reporter,
const base::Optional<base::UnguessableToken>& throttling_profile_id)
: shared_url_loader_factory_(std::move(shared_url_loader_factory)),
throttles_(std::move(throttles)),
resource_request_(std::make_unique<network::ResourceRequest>()),
callback_(std::move(callback)),
devtools_proxy_(devtools_proxy),
reporter_(reporter) {
// TODO(https://crbug.com/803774): Revisit more ResourceRequest flags.
resource_request_->url = cert_url;
// |request_initiator| is used for cookie checks, but cert requests don't use
// cookies. So just set an opaque Origin.
resource_request_->request_initiator = url::Origin();
resource_request_->resource_type =
static_cast<int>(ResourceType::kSubResource);
// Cert requests should not send credential informartion, because the default
// credentials mode of Fetch is "omit".
resource_request_->allow_credentials = false;
resource_request_->headers.SetHeader(network::kAcceptHeader,
kCertChainMimeType);
if (force_fetch) {
resource_request_->load_flags |=
net::LOAD_DISABLE_CACHE | net::LOAD_BYPASS_CACHE;
}
resource_request_->render_frame_id = MSG_ROUTING_NONE;
if (devtools_proxy_) {
cert_request_id_ = base::UnguessableToken::Create();
resource_request_->enable_load_timing = true;
}
resource_request_->throttling_profile_id = throttling_profile_id;
}
SignedExchangeCertFetcher::~SignedExchangeCertFetcher() = default;
void SignedExchangeCertFetcher::Start() {
if (devtools_proxy_) {
DCHECK(cert_request_id_);
devtools_proxy_->CertificateRequestSent(*cert_request_id_,
*resource_request_);
}
// When NetworkService enabled, data URL is not handled by the passed
// URLRequestContext's SharedURLLoaderFactory.
if (base::FeatureList::IsEnabled(network::features::kNetworkService) &&
resource_request_->url.SchemeIs(url::kDataScheme)) {
shared_url_loader_factory_ =
base::MakeRefCounted<SingleRequestURLLoaderFactory>(
base::BindOnce(&SignedExchangeCertFetcher::OnDataURLRequest,
base::Unretained(this)));
}
url_loader_ = ThrottlingURLLoader::CreateLoaderAndStart(
std::move(shared_url_loader_factory_), std::move(throttles_),
0 /* routing_id */,
ResourceDispatcherHostImpl::Get()->MakeRequestID() /* request_id */,
network::mojom::kURLLoadOptionNone, resource_request_.get(), this,
kCertFetcherTrafficAnnotation, base::ThreadTaskRunnerHandle::Get());
}
void SignedExchangeCertFetcher::Abort() {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::Abort");
MaybeNotifyCompletionToDevtools(
network::URLLoaderCompletionStatus(net::ERR_ABORTED));
DCHECK(callback_);
url_loader_ = nullptr;
body_.reset();
handle_watcher_ = nullptr;
body_string_.clear();
devtools_proxy_ = nullptr;
std::move(callback_).Run(SignedExchangeLoadResult::kCertFetchError, nullptr);
}
void SignedExchangeCertFetcher::OnHandleReady(MojoResult result) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::OnHandleReady");
const void* buffer = nullptr;
uint32_t num_bytes = 0;
MojoResult rv =
body_->BeginReadData(&buffer, &num_bytes, MOJO_READ_DATA_FLAG_NONE);
if (rv == MOJO_RESULT_OK) {
if (body_string_.size() + num_bytes > g_max_cert_size_for_signed_exchange) {
body_->EndReadData(num_bytes);
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy_,
"The response body size of certificate message exceeds the limit.");
Abort();
return;
}
body_string_.append(static_cast<const char*>(buffer), num_bytes);
body_->EndReadData(num_bytes);
} else if (rv == MOJO_RESULT_FAILED_PRECONDITION) {
OnDataComplete();
} else {
DCHECK_EQ(MOJO_RESULT_SHOULD_WAIT, rv);
}
}
void SignedExchangeCertFetcher::OnDataComplete() {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::OnDataComplete");
DCHECK(callback_);
url_loader_ = nullptr;
body_.reset();
handle_watcher_ = nullptr;
// Notify the completion to the devtools here because |this| may be deleted
// before OnComplete() is called.
MaybeNotifyCompletionToDevtools(network::URLLoaderCompletionStatus(net::OK));
std::unique_ptr<SignedExchangeCertificateChain> cert_chain =
SignedExchangeCertificateChain::Parse(
base::as_bytes(base::make_span(body_string_)), devtools_proxy_);
body_string_.clear();
if (!cert_chain) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy_, "Failed to get certificate chain from message.");
std::move(callback_).Run(SignedExchangeLoadResult::kCertParseError,
nullptr);
return;
}
std::move(callback_).Run(SignedExchangeLoadResult::kSuccess,
std::move(cert_chain));
}
// network::mojom::URLLoaderClient
void SignedExchangeCertFetcher::OnReceiveResponse(
const network::ResourceResponseHead& head) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::OnReceiveResponse");
if (devtools_proxy_) {
DCHECK(cert_request_id_);
devtools_proxy_->CertificateResponseReceived(*cert_request_id_,
resource_request_->url, head);
}
if (reporter_)
reporter_->set_cert_server_ip_address(head.remote_endpoint.address());
// |headers| is null when loading data URL.
if (head.headers && head.headers->response_code() != net::HTTP_OK) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy_, base::StringPrintf("Invalid reponse code: %d",
head.headers->response_code()));
Abort();
return;
}
// https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#cert-chain-format
// "The resource at a signature's cert-url MUST have the
// application/cert-chain+cbor content type" [spec text]
if (head.mime_type != kCertChainMimeType) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy_,
base::StringPrintf(
"Content type of cert-url must be application/cert-chain+cbor. "
"Actual content type: %s",
head.mime_type.c_str()));
Abort();
return;
}
if (head.content_length > 0) {
if (base::checked_cast<size_t>(head.content_length) >
g_max_cert_size_for_signed_exchange) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy_,
base::StringPrintf("Invalid content length: %" PRIu64,
head.content_length));
Abort();
return;
}
body_string_.reserve(head.content_length);
}
UMA_HISTOGRAM_BOOLEAN("SignedExchange.CertificateFetch.CacheHit",
head.was_fetched_via_cache);
}
void SignedExchangeCertFetcher::OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& head) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::OnReceiveRedirect");
// Currently the cert fetcher doesn't allow any redirects.
Abort();
}
void SignedExchangeCertFetcher::OnUploadProgress(
int64_t current_position,
int64_t total_size,
OnUploadProgressCallback callback) {
// Cert fetching doesn't have request body.
NOTREACHED();
}
void SignedExchangeCertFetcher::OnReceiveCachedMetadata(
mojo_base::BigBuffer data) {
// Cert fetching doesn't use cached metadata.
NOTREACHED();
}
void SignedExchangeCertFetcher::OnTransferSizeUpdated(
int32_t transfer_size_diff) {
// Do nothing.
}
void SignedExchangeCertFetcher::OnStartLoadingResponseBody(
mojo::ScopedDataPipeConsumerHandle body) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::OnStartLoadingResponseBody");
body_ = std::move(body);
handle_watcher_ = std::make_unique<mojo::SimpleWatcher>(
FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::AUTOMATIC,
base::SequencedTaskRunnerHandle::Get());
handle_watcher_->Watch(
body_.get(), MOJO_HANDLE_SIGNAL_READABLE,
base::BindRepeating(&SignedExchangeCertFetcher::OnHandleReady,
base::Unretained(this)));
}
void SignedExchangeCertFetcher::OnComplete(
const network::URLLoaderCompletionStatus& status) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeCertFetcher::OnComplete");
MaybeNotifyCompletionToDevtools(status);
if (!handle_watcher_)
Abort();
}
void SignedExchangeCertFetcher::OnDataURLRequest(
const network::ResourceRequest& resource_request,
network::mojom::URLLoaderRequest url_loader_request,
network::mojom::URLLoaderClientPtr url_loader_client_ptr) {
data_url_loader_factory_ = std::make_unique<DataURLLoaderFactory>();
data_url_loader_factory_->CreateLoaderAndStart(
std::move(url_loader_request), 0, 0, 0, resource_request,
std::move(url_loader_client_ptr),
net::MutableNetworkTrafficAnnotationTag(kCertFetcherTrafficAnnotation));
}
void SignedExchangeCertFetcher::MaybeNotifyCompletionToDevtools(
const network::URLLoaderCompletionStatus& status) {
if (!devtools_proxy_ || has_notified_completion_to_devtools_)
return;
DCHECK(cert_request_id_);
devtools_proxy_->CertificateRequestCompleted(*cert_request_id_, status);
has_notified_completion_to_devtools_ = true;
}
// static
base::ScopedClosureRunner SignedExchangeCertFetcher::SetMaxCertSizeForTest(
size_t max_cert_size) {
g_max_cert_size_for_signed_exchange = max_cert_size;
return base::ScopedClosureRunner(base::BindOnce(&ResetMaxCertSizeForTest));
}
} // namespace content