| // 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/loader/single_request_url_loader_factory.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/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 |