1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/browser/web_package/signed_exchange_cert_fetcher.h"
6
7 #include "base/bind.h"
8 #include "base/feature_list.h"
9 #include "base/format_macros.h"
10 #include "base/metrics/histogram_macros.h"
11 #include "base/numerics/safe_conversions.h"
12 #include "base/strings/string_piece.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/trace_event/trace_event.h"
15 #include "content/browser/data_url_loader_factory.h"
16 #include "content/browser/loader/single_request_url_loader_factory.h"
17 #include "content/browser/web_package/signed_exchange_consts.h"
18 #include "content/browser/web_package/signed_exchange_devtools_proxy.h"
19 #include "content/browser/web_package/signed_exchange_utils.h"
20 #include "ipc/ipc_message.h"
21 #include "mojo/public/cpp/system/simple_watcher.h"
22 #include "net/base/ip_endpoint.h"
23 #include "net/base/load_flags.h"
24 #include "net/http/http_request_headers.h"
25 #include "net/http/http_status_code.h"
26 #include "services/network/public/cpp/shared_url_loader_factory.h"
27 #include "third_party/blink/public/common/loader/throttling_url_loader.h"
28 #include "third_party/blink/public/common/loader/url_loader_throttle.h"
29 #include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
30
31 namespace content {
32
33 namespace {
34
35 constexpr char kCertChainMimeType[] = "application/cert-chain+cbor";
36
37 // Limit certificate messages to 100k, matching BoringSSL's default limit.
38 const size_t kMaxCertSizeForSignedExchange = 100 * 1024;
39 static size_t g_max_cert_size_for_signed_exchange =
40 kMaxCertSizeForSignedExchange;
41
ResetMaxCertSizeForTest()42 void ResetMaxCertSizeForTest() {
43 g_max_cert_size_for_signed_exchange = kMaxCertSizeForSignedExchange;
44 }
45
46 const net::NetworkTrafficAnnotationTag kCertFetcherTrafficAnnotation =
47 net::DefineNetworkTrafficAnnotation("sigined_exchange_cert_fetcher", R"(
48 semantics {
49 sender: "Certificate Fetcher for Signed HTTP Exchanges"
50 description:
51 "Retrieves the X.509v3 certificates to verify the signed headers of "
52 "Signed HTTP Exchanges."
53 trigger:
54 "Navigating Chrome (ex: clicking on a link) to an URL and the server "
55 "returns a Signed HTTP Exchange."
56 data: "Arbitrary site-controlled data can be included in the URL."
57 destination: WEBSITE
58 }
59 policy {
60 cookies_allowed: NO
61 setting:
62 "This feature cannot be disabled by settings. This feature is not "
63 "enabled by default yet."
64 policy_exception_justification: "Not implemented."
65 }
66 comments:
67 "Chrome would be unable to handle Signed HTTP Exchanges without this "
68 "type of request."
69 )");
70
71 } // namespace
72
73 // static
74 std::unique_ptr<SignedExchangeCertFetcher>
CreateAndStart(scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles,const GURL & cert_url,bool force_fetch,CertificateCallback callback,SignedExchangeDevToolsProxy * devtools_proxy,const base::Optional<base::UnguessableToken> & throttling_profile_id,net::IsolationInfo isolation_info)75 SignedExchangeCertFetcher::CreateAndStart(
76 scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
77 std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles,
78 const GURL& cert_url,
79 bool force_fetch,
80 CertificateCallback callback,
81 SignedExchangeDevToolsProxy* devtools_proxy,
82 const base::Optional<base::UnguessableToken>& throttling_profile_id,
83 net::IsolationInfo isolation_info) {
84 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
85 "SignedExchangeCertFetcher::CreateAndStart");
86 std::unique_ptr<SignedExchangeCertFetcher> cert_fetcher(
87 new SignedExchangeCertFetcher(
88 std::move(shared_url_loader_factory), std::move(throttles), cert_url,
89 force_fetch, std::move(callback), devtools_proxy,
90 throttling_profile_id, std::move(isolation_info)));
91 cert_fetcher->Start();
92 return cert_fetcher;
93 }
94
95 // https://wicg.github.io/webpackage/loading.html#handling-cert-url
SignedExchangeCertFetcher(scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles,const GURL & cert_url,bool force_fetch,CertificateCallback callback,SignedExchangeDevToolsProxy * devtools_proxy,const base::Optional<base::UnguessableToken> & throttling_profile_id,net::IsolationInfo isolation_info)96 SignedExchangeCertFetcher::SignedExchangeCertFetcher(
97 scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
98 std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles,
99 const GURL& cert_url,
100 bool force_fetch,
101 CertificateCallback callback,
102 SignedExchangeDevToolsProxy* devtools_proxy,
103 const base::Optional<base::UnguessableToken>& throttling_profile_id,
104 net::IsolationInfo isolation_info)
105 : shared_url_loader_factory_(std::move(shared_url_loader_factory)),
106 throttles_(std::move(throttles)),
107 resource_request_(std::make_unique<network::ResourceRequest>()),
108 callback_(std::move(callback)),
109 devtools_proxy_(devtools_proxy) {
110 // TODO(https://crbug.com/803774): Revisit more ResourceRequest flags.
111 resource_request_->url = cert_url;
112 // |request_initiator| is used for cookie checks, but cert requests don't use
113 // cookies. So just set an opaque Origin.
114 resource_request_->request_initiator = url::Origin();
115 resource_request_->resource_type =
116 static_cast<int>(blink::mojom::ResourceType::kSubResource);
117 // Cert requests should not send credential informartion, because the default
118 // credentials mode of Fetch is "omit".
119 resource_request_->credentials_mode = network::mojom::CredentialsMode::kOmit;
120 resource_request_->headers.SetHeader(net::HttpRequestHeaders::kAccept,
121 kCertChainMimeType);
122 if (force_fetch) {
123 resource_request_->load_flags |=
124 net::LOAD_DISABLE_CACHE | net::LOAD_BYPASS_CACHE;
125 }
126 resource_request_->render_frame_id = MSG_ROUTING_NONE;
127 if (devtools_proxy_) {
128 cert_request_id_ = base::UnguessableToken::Create();
129 resource_request_->enable_load_timing = true;
130 }
131 resource_request_->throttling_profile_id = throttling_profile_id;
132 if (!isolation_info.IsEmpty()) {
133 resource_request_->trusted_params =
134 network::ResourceRequest::TrustedParams();
135 resource_request_->trusted_params->isolation_info = isolation_info;
136 }
137 }
138
139 SignedExchangeCertFetcher::~SignedExchangeCertFetcher() = default;
140
Start()141 void SignedExchangeCertFetcher::Start() {
142 if (devtools_proxy_) {
143 DCHECK(cert_request_id_);
144 devtools_proxy_->CertificateRequestSent(*cert_request_id_,
145 *resource_request_);
146 }
147 // When NetworkService enabled, data URL is not handled by the passed
148 // URLRequestContext's SharedURLLoaderFactory.
149 if (resource_request_->url.SchemeIs(url::kDataScheme)) {
150 shared_url_loader_factory_ =
151 base::MakeRefCounted<SingleRequestURLLoaderFactory>(
152 base::BindOnce(&SignedExchangeCertFetcher::OnDataURLRequest,
153 base::Unretained(this)));
154 }
155 url_loader_ = blink::ThrottlingURLLoader::CreateLoaderAndStart(
156 std::move(shared_url_loader_factory_), std::move(throttles_),
157 0 /* routing_id */,
158 signed_exchange_utils::MakeRequestID() /* request_id */,
159 network::mojom::kURLLoadOptionNone, resource_request_.get(), this,
160 kCertFetcherTrafficAnnotation, base::ThreadTaskRunnerHandle::Get());
161 }
162
Abort()163 void SignedExchangeCertFetcher::Abort() {
164 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
165 "SignedExchangeCertFetcher::Abort");
166 MaybeNotifyCompletionToDevtools(
167 network::URLLoaderCompletionStatus(net::ERR_ABORTED));
168 DCHECK(callback_);
169 url_loader_ = nullptr;
170 body_.reset();
171 handle_watcher_ = nullptr;
172 body_string_.clear();
173 devtools_proxy_ = nullptr;
174 std::move(callback_).Run(SignedExchangeLoadResult::kCertFetchError, nullptr,
175 net::IPAddress());
176 }
177
OnHandleReady(MojoResult result)178 void SignedExchangeCertFetcher::OnHandleReady(MojoResult result) {
179 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
180 "SignedExchangeCertFetcher::OnHandleReady");
181 const void* buffer = nullptr;
182 uint32_t num_bytes = 0;
183 MojoResult rv =
184 body_->BeginReadData(&buffer, &num_bytes, MOJO_READ_DATA_FLAG_NONE);
185 if (rv == MOJO_RESULT_OK) {
186 if (body_string_.size() + num_bytes > g_max_cert_size_for_signed_exchange) {
187 body_->EndReadData(num_bytes);
188 signed_exchange_utils::ReportErrorAndTraceEvent(
189 devtools_proxy_,
190 "The response body size of certificate message exceeds the limit.");
191 Abort();
192 return;
193 }
194 body_string_.append(static_cast<const char*>(buffer), num_bytes);
195 body_->EndReadData(num_bytes);
196 } else if (rv == MOJO_RESULT_FAILED_PRECONDITION) {
197 OnDataComplete();
198 } else {
199 DCHECK_EQ(MOJO_RESULT_SHOULD_WAIT, rv);
200 }
201 }
202
OnDataComplete()203 void SignedExchangeCertFetcher::OnDataComplete() {
204 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
205 "SignedExchangeCertFetcher::OnDataComplete");
206 DCHECK(callback_);
207 url_loader_ = nullptr;
208 body_.reset();
209 handle_watcher_ = nullptr;
210
211 // Notify the completion to the devtools here because |this| may be deleted
212 // before OnComplete() is called.
213 MaybeNotifyCompletionToDevtools(network::URLLoaderCompletionStatus(net::OK));
214
215 std::unique_ptr<SignedExchangeCertificateChain> cert_chain =
216 SignedExchangeCertificateChain::Parse(
217 base::as_bytes(base::make_span(body_string_)), devtools_proxy_);
218 body_string_.clear();
219 if (!cert_chain) {
220 signed_exchange_utils::ReportErrorAndTraceEvent(
221 devtools_proxy_, "Failed to get certificate chain from message.");
222 std::move(callback_).Run(SignedExchangeLoadResult::kCertParseError, nullptr,
223 cert_server_ip_address_);
224 return;
225 }
226 std::move(callback_).Run(SignedExchangeLoadResult::kSuccess,
227 std::move(cert_chain), cert_server_ip_address_);
228 }
229
230 // network::mojom::URLLoaderClient
OnReceiveResponse(network::mojom::URLResponseHeadPtr head)231 void SignedExchangeCertFetcher::OnReceiveResponse(
232 network::mojom::URLResponseHeadPtr head) {
233 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
234 "SignedExchangeCertFetcher::OnReceiveResponse");
235 if (devtools_proxy_) {
236 DCHECK(cert_request_id_);
237 devtools_proxy_->CertificateResponseReceived(*cert_request_id_,
238 resource_request_->url, *head);
239 }
240
241 cert_server_ip_address_ = head->remote_endpoint.address();
242
243 // |headers| is null when loading data URL.
244 if (head->headers && head->headers->response_code() != net::HTTP_OK) {
245 signed_exchange_utils::ReportErrorAndTraceEvent(
246 devtools_proxy_, base::StringPrintf("Invalid reponse code: %d",
247 head->headers->response_code()));
248 Abort();
249 return;
250 }
251
252 // https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#cert-chain-format
253 // "The resource at a signature's cert-url MUST have the
254 // application/cert-chain+cbor content type" [spec text]
255 if (head->mime_type != kCertChainMimeType) {
256 signed_exchange_utils::ReportErrorAndTraceEvent(
257 devtools_proxy_,
258 base::StringPrintf(
259 "Content type of cert-url must be application/cert-chain+cbor. "
260 "Actual content type: %s",
261 head->mime_type.c_str()));
262 Abort();
263 return;
264 }
265
266 if (head->content_length > 0) {
267 if (base::checked_cast<size_t>(head->content_length) >
268 g_max_cert_size_for_signed_exchange) {
269 signed_exchange_utils::ReportErrorAndTraceEvent(
270 devtools_proxy_,
271 base::StringPrintf("Invalid content length: %" PRIu64,
272 head->content_length));
273 Abort();
274 return;
275 }
276 body_string_.reserve(head->content_length);
277 }
278
279 UMA_HISTOGRAM_BOOLEAN("SignedExchange.CertificateFetch.CacheHit",
280 head->was_fetched_via_cache);
281 }
282
OnReceiveRedirect(const net::RedirectInfo & redirect_info,network::mojom::URLResponseHeadPtr head)283 void SignedExchangeCertFetcher::OnReceiveRedirect(
284 const net::RedirectInfo& redirect_info,
285 network::mojom::URLResponseHeadPtr head) {
286 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
287 "SignedExchangeCertFetcher::OnReceiveRedirect");
288 // Currently the cert fetcher doesn't allow any redirects.
289 Abort();
290 }
291
OnUploadProgress(int64_t current_position,int64_t total_size,OnUploadProgressCallback callback)292 void SignedExchangeCertFetcher::OnUploadProgress(
293 int64_t current_position,
294 int64_t total_size,
295 OnUploadProgressCallback callback) {
296 // Cert fetching doesn't have request body.
297 NOTREACHED();
298 }
299
OnReceiveCachedMetadata(mojo_base::BigBuffer data)300 void SignedExchangeCertFetcher::OnReceiveCachedMetadata(
301 mojo_base::BigBuffer data) {
302 // Cert fetching doesn't use cached metadata.
303 NOTREACHED();
304 }
305
OnTransferSizeUpdated(int32_t transfer_size_diff)306 void SignedExchangeCertFetcher::OnTransferSizeUpdated(
307 int32_t transfer_size_diff) {
308 // Do nothing.
309 }
310
OnStartLoadingResponseBody(mojo::ScopedDataPipeConsumerHandle body)311 void SignedExchangeCertFetcher::OnStartLoadingResponseBody(
312 mojo::ScopedDataPipeConsumerHandle body) {
313 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
314 "SignedExchangeCertFetcher::OnStartLoadingResponseBody");
315 body_ = std::move(body);
316 handle_watcher_ = std::make_unique<mojo::SimpleWatcher>(
317 FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::AUTOMATIC,
318 base::SequencedTaskRunnerHandle::Get());
319 handle_watcher_->Watch(
320 body_.get(), MOJO_HANDLE_SIGNAL_READABLE,
321 base::BindRepeating(&SignedExchangeCertFetcher::OnHandleReady,
322 base::Unretained(this)));
323 }
324
OnComplete(const network::URLLoaderCompletionStatus & status)325 void SignedExchangeCertFetcher::OnComplete(
326 const network::URLLoaderCompletionStatus& status) {
327 TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
328 "SignedExchangeCertFetcher::OnComplete");
329 MaybeNotifyCompletionToDevtools(status);
330 if (!handle_watcher_)
331 Abort();
332 }
333
OnDataURLRequest(const network::ResourceRequest & resource_request,mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver,mojo::PendingRemote<network::mojom::URLLoaderClient> url_loader_client_remote)334 void SignedExchangeCertFetcher::OnDataURLRequest(
335 const network::ResourceRequest& resource_request,
336 mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver,
337 mojo::PendingRemote<network::mojom::URLLoaderClient>
338 url_loader_client_remote) {
339 mojo::Remote<network::mojom::URLLoaderFactory> factory(
340 DataURLLoaderFactory::Create());
341 factory->CreateLoaderAndStart(
342 std::move(url_loader_receiver), 0, 0, 0, resource_request,
343 std::move(url_loader_client_remote),
344 net::MutableNetworkTrafficAnnotationTag(kCertFetcherTrafficAnnotation));
345 }
346
MaybeNotifyCompletionToDevtools(const network::URLLoaderCompletionStatus & status)347 void SignedExchangeCertFetcher::MaybeNotifyCompletionToDevtools(
348 const network::URLLoaderCompletionStatus& status) {
349 if (!devtools_proxy_ || has_notified_completion_to_devtools_)
350 return;
351 DCHECK(cert_request_id_);
352 devtools_proxy_->CertificateRequestCompleted(*cert_request_id_, status);
353 has_notified_completion_to_devtools_ = true;
354 }
355
356 // static
SetMaxCertSizeForTest(size_t max_cert_size)357 base::ScopedClosureRunner SignedExchangeCertFetcher::SetMaxCertSizeForTest(
358 size_t max_cert_size) {
359 g_max_cert_size_for_signed_exchange = max_cert_size;
360 return base::ScopedClosureRunner(base::BindOnce(&ResetMaxCertSizeForTest));
361 }
362
363 } // namespace content
364