File: qwac_web_contents_observer.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (316 lines) | stat: -rw-r--r-- 11,906 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/net/qwac_web_contents_observer.h"

#include "base/metrics/histogram_functions.h"
#include "base/task/sequenced_task_runner.h"
#include "components/link_header_util/link_header_util.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/features.h"
#include "net/cert/cert_status_flags.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/referrer_policy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "url/origin.h"
#include "url/scheme_host_port.h"

namespace {

// TODO(crbug.com/392931069): These limits are arbitrary. Decide if they are
// okay or need tweaking.
constexpr base::TimeDelta k2QwacLoaderTimeout = base::Seconds(15);
constexpr size_t k2QwacMaxSize = 100 * 1024;

// Traffic annotation for RequestDelegate.
const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("certificate_verifier_2qwac_loader", R"(
      semantics {
        sender: "Certificate Verifier"
        description:
          "Web pages can use a Qualified certificate for Website "
          "Authentication (QWAC) which is delivered separately from the "
          "TLS connection certificate. See ETSI TS 119 411-5."
        trigger:
          "User visits an HTTPS web page containing a link header with "
          "rel=\"tls-certificate-binding\"."
        data: "None"
        user_data {
          type: NONE
        }
        destination: WEBSITE
        internal {
          contacts {
            email: "chrome-secure-web-and-net@chromium.org"
          }
        }
        last_reviewed: "2025-05-16"
      }
      policy {
        cookies_allowed: NO
        setting: "This feature cannot be disabled in settings."
        policy_exception_justification: "QWAC verification is required."
      })");

void RecordHistogram(QwacWebContentsObserver::QwacLinkProcessingResult result) {
  base::UmaHistogramEnumeration("Net.CertVerifier.Qwac.2QwacLinkProcessing",
                                result);
}

}  // namespace

PAGE_USER_DATA_KEY_IMPL(QwacWebContentsObserver::QwacStatus);

QwacWebContentsObserver::QwacStatus::QwacStatus(
    content::Page& page,
    std::string hostname,
    scoped_refptr<net::X509Certificate> tls_cert,
    GURL qwac_url,
    const url::Origin& initiator,
    mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory)
    : content::PageUserData<QwacStatus>(page),
      hostname_(std::move(hostname)),
      tls_cert_(std::move(tls_cert)) {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = std::move(qwac_url);
  resource_request->request_initiator = initiator;

  // Set referrer_policy to a safe default. (It shouldn't matter since we set
  // mode to same-origin and we don't set `referrer` anyway.)
  resource_request->referrer_policy =
      net::ReferrerPolicy::CLEAR_ON_TRANSITION_CROSS_ORIGIN;

  // We don't expect that credentials should be needed to fetch the 2-QWAC, so
  // use a safe default.
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  // The QWAC url must be relative, which we take to mean that it must be
  // served from the same origin, so set same-origin mode to enforce that it
  // doesn't redirect to a different origin.
  resource_request->mode = network::mojom::RequestMode::kSameOrigin;

  simple_url_loader_ = network::SimpleURLLoader::Create(
      std::move(resource_request), kTrafficAnnotation);
  simple_url_loader_->SetTimeoutDuration(k2QwacLoaderTimeout);

  // TODO(crbug.com/392931069): Is it possible to link this request to the
  // initiating request in the netlog? ResourceRequest has
  // `net_log_create_info` and `net_log_reference_info` but the comments say
  // they should only be used from within the network service, and I don't know
  // if the netlog id of the initiating request is even available here.

  // If the loader is destroyed, the callback will be canceled, so using
  // base::Unretained here is safe.
  simple_url_loader_->DownloadToString(
      url_loader_factory.get(),
      base::BindOnce(&QwacStatus::On2QwacDownloadComplete,
                     base::Unretained(this)),
      k2QwacMaxSize);
}

QwacWebContentsObserver::QwacStatus::~QwacStatus() {
  if (!is_finished_) {
    RecordHistogram(QwacLinkProcessingResult::kDestroyedBeforeFinish);
  }
}

base::CallbackListSubscription
QwacWebContentsObserver::QwacStatus::RegisterCallback(
    CallbackList::CallbackType cb) {
  CHECK(!is_finished());
  return callback_list_.Add(std::move(cb));
}

void QwacWebContentsObserver::QwacStatus::On2QwacDownloadComplete(
    std::optional<std::string> response_body) {
  if (!response_body) {
    RecordHistogram(QwacLinkProcessingResult::kDownloadFailed);
    is_finished_ = true;
    callback_list_.Notify();
    return;
  }

  page()
      .GetMainDocument()
      .GetProcess()
      ->GetStoragePartition()
      ->GetNetworkContext()
      ->Verify2QwacCertBinding(
          *response_body, hostname_, tls_cert_,
          base::BindOnce(&QwacStatus::On2QwacVerificationComplete,
                         weak_ptr_factory_.GetWeakPtr()));
}

void QwacWebContentsObserver::QwacStatus::On2QwacVerificationComplete(
    const scoped_refptr<net::X509Certificate>& verified_2qwac) {
  RecordHistogram(verified_2qwac
                      ? QwacLinkProcessingResult::kValid2Qwac
                      : QwacLinkProcessingResult::k2QwacVerificationFailed);
  is_finished_ = true;
  verified_2qwac_ = verified_2qwac;
  callback_list_.Notify();
}

QwacWebContentsObserver::QwacWebContentsObserver(tabs::TabInterface& tab)
    : content::WebContentsObserver(tab.GetContents()) {
  // Unretained is safe as the callback will be unregistered when the
  // CallbackListSubscription is destroyed.
  tab_subscription_ = tab.RegisterWillDiscardContents(base::BindRepeating(
      &QwacWebContentsObserver::WillDiscardContents, base::Unretained(this)));
}

QwacWebContentsObserver::QwacWebContentsObserver(
    content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents) {}
QwacWebContentsObserver::~QwacWebContentsObserver() = default;

void QwacWebContentsObserver::WillDiscardContents(
    tabs::TabInterface* tab,
    content::WebContents* old_contents,
    content::WebContents* new_contents) {
  Observe(new_contents);
}

namespace {

bool NavigationHandleHasAcceptableSSLInfo(
    content::NavigationHandle* navigation_handle) {
  // Only TLS connections with a valid certificate are eligible to be a 2-QWAC.
  // ETSI TS 119 411-5 V2.1.1 - 6.2.2.1:
  //   Establish a secure TLS connection with the site using the web browsers'
  //   procedures and configuration ... If this step fails, the procedure
  //   finishes negatively.
  //
  // Also if the connection used a valid 1-QWAC certificate, there's no point
  // to checking for 2-QWACs.
  const std::optional<net::SSLInfo>& ssl_info = navigation_handle->GetSSLInfo();
  return (ssl_info.has_value() && ssl_info->is_valid() &&
          !net::IsCertStatusError(ssl_info->cert_status) &&
          !(ssl_info->cert_status & net::CERT_STATUS_IS_QWAC));
}

}  // namespace

void QwacWebContentsObserver::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->IsInPrimaryMainFrame() ||
      navigation_handle->IsSameDocument() ||
      !navigation_handle->HasCommitted()) {
    return;
  }

  // Ignore non-TLS navigations.
  // Also, navigation might not have actually loaded a resource from the
  // network. The !navigation_handle->IsSameDocument() check above should
  // catch these, but if there are other cases of navigations that don't have
  // the network information, safely ignore those too.
  if (!navigation_handle->GetSSLInfo() ||
      !navigation_handle->GetResponseHeaders()) {
    return;
  }

  content::RenderFrameHost* render_frame_host =
      navigation_handle->GetRenderFrameHost();
  if (!render_frame_host) {
    return;
  }

  content::Page& page = render_frame_host->GetPage();

  // A QwacStatus may have already been created for this page. Don't recreate
  // it if it is still applicable.
  if (QwacStatus* status = QwacStatus::GetForPage(page); status) {
    if (NavigationHandleHasAcceptableSSLInfo(navigation_handle) &&
        navigation_handle->GetSSLInfo()->cert->EqualsIncludingChain(
            status->tls_cert())) {
      // If the page wasn't reloaded, or was reloaded but the certificate
      // didn't change, then we can just reuse the existing QwacStatus.
      RecordHistogram(QwacLinkProcessingResult::kQwacStatusAlreadyPresent);
      return;
    }
    // Otherwise, clear the existing entry and go through the fetching and
    // verification again.
    QwacStatus::DeleteForPage(page);
  }

  if (!NavigationHandleHasAcceptableSSLInfo(navigation_handle)) {
    RecordHistogram(QwacLinkProcessingResult::kUnacceptableSslInfo);
    return;
  }

  // ETSI TS 119 411-5 V2.1.1 - 6.2.2.2:
  //   Examine the HTTP headers included in any main frame navigation response
  //   from the server (relating to navigation by the web browser to the
  //   address as displayed in the address bar) for a HTTP 'Link' response
  //   header (as defined in IETF RFC 8288 [6]) with a rel value of
  //   tls-certificate-binding.
  std::optional<std::string> link_header =
      navigation_handle->GetResponseHeaders()->GetNormalizedHeader("link");
  if (!link_header.has_value()) {
    RecordHistogram(QwacLinkProcessingResult::kNoQwacLinkHeader);
    return;
  }

  std::string qwac_binding_url;
  for (const auto& value : link_header_util::SplitLinkHeader(*link_header)) {
    std::unordered_map<std::string, std::optional<std::string>> params;
    std::optional<std::string> link_url =
        link_header_util::ParseLinkHeaderValue(value, params);
    if (!link_url) {
      continue;
    }

    auto rel = params.find("rel");
    if (rel == params.end()) {
      continue;
    }
    if (!rel->second) {
      continue;
    }
    if (rel->second == "tls-certificate-binding") {
      qwac_binding_url = *link_url;
      break;
    }
  }

  if (qwac_binding_url.empty()) {
    RecordHistogram(QwacLinkProcessingResult::kNoQwacLinkHeader);
    return;
  }

  GURL full_qwac_url = navigation_handle->GetURL().Resolve(qwac_binding_url);
  if (!full_qwac_url.is_valid()) {
    RecordHistogram(QwacLinkProcessingResult::kInvalidQwacLinkHeader);
    return;
  }

  // The 2-QWAC url must be relative:
  // ETSI TS 119 411-5 V2.1.1 - 5.2:
  //   When using a 2-QWAC, website operators shall:
  //   ...
  //   Configure their website to serve: an HTTP 'Link' response header (as
  //   defined in IETF RFC 8288 [6]) with a relative reference to the TLS
  //   Certificate Binding, and a rel value of tls-certificate-binding
  if (url::SchemeHostPort(full_qwac_url) !=
      url::SchemeHostPort(navigation_handle->GetURL())) {
    RecordHistogram(QwacLinkProcessingResult::kNonrelativeQwacLinkUrl);
    return;
  }

  mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory;
  render_frame_host->CreateNetworkServiceDefaultFactory(
      url_loader_factory.BindNewPipeAndPassReceiver());

  QwacStatus::CreateForPage(
      page, navigation_handle->GetURL().host(),
      navigation_handle->GetSSLInfo()->cert, std::move(full_qwac_url),
      /*initiator=*/render_frame_host->GetLastCommittedOrigin(),
      std::move(url_loader_factory));
}