1 // Copyright 2024 The Chromium Authors
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 "net/device_bound_sessions/registration_fetcher.h"
6
7 #include <memory>
8 #include <utility>
9 #include <vector>
10
11 #include "components/unexportable_keys/background_task_priority.h"
12 #include "components/unexportable_keys/unexportable_key_service.h"
13 #include "net/base/io_buffer.h"
14 #include "net/device_bound_sessions/registration_request_param.h"
15 #include "net/device_bound_sessions/session_binding_utils.h"
16 #include "net/device_bound_sessions/session_challenge_param.h"
17 #include "net/device_bound_sessions/session_json_utils.h"
18 #include "net/traffic_annotation/network_traffic_annotation.h"
19 #include "net/url_request/url_request.h"
20 #include "net/url_request/url_request_context.h"
21
22 namespace net::device_bound_sessions {
23
24 namespace {
25
26 constexpr char kSessionIdHeaderName[] = "Sec-Session-Id";
27 constexpr char kJwtSessionHeaderName[] = "Sec-Session-Response";
28 constexpr net::NetworkTrafficAnnotationTag kRegistrationTrafficAnnotation =
29 net::DefineNetworkTrafficAnnotation("dbsc_registration", R"(
30 semantics {
31 sender: "Device Bound Session Credentials API"
32 description:
33 "Device Bound Session Credentials (DBSC) let a server create a "
34 "session with the local device. For more info see "
35 "https://github.com/WICG/dbsc."
36 trigger:
37 "Server sending a response with a Sec-Session-Registration header."
38 data: "A signed JWT with the new key created for this session."
39 destination: WEBSITE
40 last_reviewed: "2024-04-10"
41 user_data {
42 type: ACCESS_TOKEN
43 }
44 internal {
45 contacts {
46 email: "kristianm@chromium.org"
47 }
48 contacts {
49 email: "chrome-counter-abuse-alerts@google.com"
50 }
51 }
52 }
53 policy {
54 cookies_allowed: YES
55 cookies_store: "user"
56 setting: "There is no separate setting for this feature, but it will "
57 "follow the cookie settings."
58 policy_exception_justification: "Not implemented."
59 })");
60
61 constexpr int kBufferSize = 4096;
62
63 // New session registration doesn't block the user and can be done with a delay.
64 constexpr unexportable_keys::BackgroundTaskPriority kTaskPriority =
65 unexportable_keys::BackgroundTaskPriority::kBestEffort;
66
OnDataSigned(crypto::SignatureVerifier::SignatureAlgorithm algorithm,unexportable_keys::UnexportableKeyService & unexportable_key_service,std::string header_and_payload,unexportable_keys::UnexportableKeyId key_id,base::OnceCallback<void (std::optional<RegistrationFetcher::RegistrationTokenResult>)> callback,unexportable_keys::ServiceErrorOr<std::vector<uint8_t>> result)67 void OnDataSigned(
68 crypto::SignatureVerifier::SignatureAlgorithm algorithm,
69 unexportable_keys::UnexportableKeyService& unexportable_key_service,
70 std::string header_and_payload,
71 unexportable_keys::UnexportableKeyId key_id,
72 base::OnceCallback<void(
73 std::optional<RegistrationFetcher::RegistrationTokenResult>)> callback,
74 unexportable_keys::ServiceErrorOr<std::vector<uint8_t>> result) {
75 if (!result.has_value()) {
76 std::move(callback).Run(std::nullopt);
77 return;
78 }
79
80 const std::vector<uint8_t>& signature = result.value();
81 std::optional<std::string> registration_token =
82 AppendSignatureToHeaderAndPayload(header_and_payload, algorithm,
83 signature);
84 if (!registration_token.has_value()) {
85 std::move(callback).Run(std::nullopt);
86 return;
87 }
88
89 std::move(callback).Run(RegistrationFetcher::RegistrationTokenResult(
90 registration_token.value(), key_id));
91 }
92
93 void SignChallengeWithKey(
94 unexportable_keys::UnexportableKeyService& unexportable_key_service,
95 unexportable_keys::UnexportableKeyId& key_id,
96 const GURL& registration_url,
97 std::string_view challenge,
98 std::optional<std::string> authorization,
99 base::OnceCallback<
100 void(std::optional<RegistrationFetcher::RegistrationTokenResult>)>
101 callback) {
102 auto expected_algorithm = unexportable_key_service.GetAlgorithm(key_id);
103 auto expected_public_key =
104 unexportable_key_service.GetSubjectPublicKeyInfo(key_id);
105 if (!expected_algorithm.has_value() || !expected_public_key.has_value()) {
106 std::move(callback).Run(std::nullopt);
107 return;
108 }
109
110 std::optional<std::string> optional_header_and_payload =
111 CreateKeyRegistrationHeaderAndPayload(
112 challenge, registration_url, expected_algorithm.value(),
113 expected_public_key.value(), base::Time::Now(),
114 std::move(authorization));
115
116 if (!optional_header_and_payload.has_value()) {
117 std::move(callback).Run(std::nullopt);
118 return;
119 }
120
121 std::string header_and_payload =
122 std::move(optional_header_and_payload.value());
123 unexportable_key_service.SignSlowlyAsync(
124 key_id, base::as_byte_span(header_and_payload), kTaskPriority,
125 base::BindOnce(&OnDataSigned, expected_algorithm.value(),
126 std::ref(unexportable_key_service),
127 std::move(header_and_payload), key_id,
128 std::move(callback)));
129 }
130
131 class RegistrationFetcherImpl : public URLRequest::Delegate {
132 public:
133 // URLRequest::Delegate
134
135 void OnReceivedRedirect(URLRequest* request,
136 const RedirectInfo& redirect_info,
137 bool* defer_redirect) override {
138 if (!redirect_info.new_url.SchemeIsCryptographic()) {
139 request->Cancel();
140 OnResponseCompleted();
141 // *this is deleted here
142 }
143 }
144
145 // TODO(kristianm): Look into if OnAuthRequired might need to be customize
146 // for DBSC
147
148 // TODO(kristianm): Think about what to do for DBSC with
149 // OnCertificateRequested, leaning towards not supporting it but not sure.
150
151 // Always cancel requests on SSL errors, this is the default implementation
152 // of OnSSLCertificateError.
153
154 // This is always called unless the request is deleted before it is called.
155 void OnResponseStarted(URLRequest* request, int net_error) override {
156 if (net_error != OK) {
157 OnResponseCompleted();
158 // *this is deleted here
159 return;
160 }
161
162 HttpResponseHeaders* headers = request->response_headers();
163 const int response_code = headers ? headers->response_code() : 0;
164
165 if (response_code == 401) {
166 auto challenge_params =
167 device_bound_sessions::SessionChallengeParam::CreateIfValid(
168 fetcher_endpoint_, headers);
169 OnChallengeNeeded(std::move(challenge_params));
170 // *this is preserved here.
171 return;
172 }
173
174 if (response_code < 200 || response_code >= 300) {
175 OnResponseCompleted();
176 // *this is deleted here
177 return;
178 }
179
180 // Initiate the first read.
181 int bytes_read = request->Read(buf_.get(), kBufferSize);
182 if (bytes_read >= 0) {
183 OnReadCompleted(request, bytes_read);
184 } else if (bytes_read != ERR_IO_PENDING) {
185 OnResponseCompleted();
186 // *this is deleted here
187 }
188 }
189
190 void OnReadCompleted(URLRequest* request, int bytes_read) override {
191 data_received_.append(buf_->data(), bytes_read);
192 while (bytes_read > 0) {
193 bytes_read = request->Read(buf_.get(), kBufferSize);
194 if (bytes_read > 0) {
195 data_received_.append(buf_->data(), bytes_read);
196 }
197 }
198
199 if (bytes_read != ERR_IO_PENDING) {
200 OnResponseCompleted();
201 // *this is deleted here
202 }
203 }
204
205 RegistrationFetcherImpl(
206 const GURL& fetcher_endpoint,
207 std::optional<std::string> session_identifier,
208 unexportable_keys::UnexportableKeyService& key_service,
209 const unexportable_keys::UnexportableKeyId& key_id,
210 const URLRequestContext* context,
211 const IsolationInfo& isolation_info,
212 RegistrationFetcher::RegistrationCompleteCallback callback)
213 : fetcher_endpoint_(fetcher_endpoint),
214 session_identifier_(std::move(session_identifier)),
215 key_service_(key_service),
216 key_id_(key_id),
217 context_(context),
218 isolation_info_(isolation_info),
219 callback_(std::move(callback)),
220 buf_(base::MakeRefCounted<IOBufferWithSize>(kBufferSize)) {}
221
222 ~RegistrationFetcherImpl() override { CHECK(!callback_); }
223
224 void Start(std::optional<std::string> challenge,
225 std::optional<std::string> authorization) {
226 if (challenge.has_value()) {
227 SignChallengeWithKey(
228 *key_service_, key_id_, fetcher_endpoint_, *challenge,
229 std::move(authorization),
230 base::BindOnce(&RegistrationFetcherImpl::OnRegistrationTokenCreated,
231 base::Unretained(this)));
232 return;
233 }
234
235 // Start a request to get a challenge with the session identifier.
236 // `RegistrationRequestParam::Create` guarantees `session_identifier_` is
237 // set when `challenge_` is missing.
238 if (session_identifier_.has_value()) {
239 request_ = CreateBaseRequest();
240 request_->Start();
241 }
242 }
243
244 private:
245 void OnRegistrationTokenCreated(
246 std::optional<RegistrationFetcher::RegistrationTokenResult> result) {
247 if (!result) {
248 RunCallbackAndDeleteSelf(std::nullopt);
249 return;
250 }
251
252 request_ = CreateBaseRequest();
253 request_->SetExtraRequestHeaderByName(
254 kJwtSessionHeaderName, result->registration_token, /*overwrite*/ true);
255 request_->Start();
256 }
257
258 std::unique_ptr<net::URLRequest> CreateBaseRequest() {
259 std::unique_ptr<net::URLRequest> request = context_->CreateRequest(
260 fetcher_endpoint_, IDLE, this, kRegistrationTrafficAnnotation);
261 request->set_method("POST");
262 request->SetLoadFlags(LOAD_DISABLE_CACHE);
263 request->set_allow_credentials(true);
264
265 request->set_site_for_cookies(isolation_info_.site_for_cookies());
266 // TODO(kristianm): Set initiator to the URL of the registration header.
267 request->set_initiator(url::Origin());
268 request->set_isolation_info(isolation_info_);
269
270 if (session_identifier_.has_value()) {
271 request->SetExtraRequestHeaderByName(
272 kSessionIdHeaderName, *session_identifier_, /*overwrite*/ true);
273 }
274
275 return request;
276 }
277
278 void OnChallengeNeeded(
279 std::optional<std::vector<SessionChallengeParam>> challenge_params) {
280 if (!challenge_params || challenge_params->empty()) {
281 RunCallbackAndDeleteSelf(std::nullopt);
282 return;
283 }
284
285 // TODO(kristianm): Log if there is more than one challenge
286 // TODO(kristianm): Handle if session identifiers don't match
287 const std::string& challenge = (*challenge_params)[0].challenge();
288 Start(challenge, std::nullopt);
289 }
290
291 void OnResponseCompleted() {
292 if (!data_received_.empty()) {
293 std::optional<SessionParams> params =
294 ParseSessionInstructionJson(data_received_);
295 if (params) {
296 RunCallbackAndDeleteSelf(
297 std::make_optional<RegistrationFetcher::RegistrationCompleteParams>(
298 std::move(*params), key_id_, request_->url(),
299 std::move(session_identifier_)));
300 return;
301 }
302 }
303
304 RunCallbackAndDeleteSelf(std::nullopt);
305 }
306
307 // Running callback when fetching is complete or on error.
308 // Deletes `this` afterwards.
309 void RunCallbackAndDeleteSelf(
310 std::optional<RegistrationFetcher::RegistrationCompleteParams> params) {
311 std::move(callback_).Run(std::move(params));
312 delete this;
313 }
314
315 // State passed in to constructor
316 GURL fetcher_endpoint_;
317 std::optional<std::string> session_identifier_;
318 const raw_ref<unexportable_keys::UnexportableKeyService> key_service_;
319 unexportable_keys::UnexportableKeyId key_id_;
320 raw_ptr<const URLRequestContext> context_;
321 IsolationInfo isolation_info_;
322 RegistrationFetcher::RegistrationCompleteCallback callback_;
323
324 // Created to fetch data
325 std::unique_ptr<URLRequest> request_;
326 scoped_refptr<IOBuffer> buf_;
327 std::string data_received_;
328 };
329
330 RegistrationFetcher::FetcherType g_mock_fetcher = nullptr;
331
332 } // namespace
333
334 RegistrationFetcher::RegistrationCompleteParams::RegistrationCompleteParams(
335 SessionParams params,
336 unexportable_keys::UnexportableKeyId key_id,
337 const GURL& url,
338 std::optional<std::string> referral_session_identifier)
339 : params(std::move(params)),
340 key_id(std::move(key_id)),
341 url(url),
342 referral_session_identifier(std::move(referral_session_identifier)) {}
343
344 RegistrationFetcher::RegistrationCompleteParams::RegistrationCompleteParams(
345 RegistrationFetcher::RegistrationCompleteParams&& other) noexcept = default;
346 RegistrationFetcher::RegistrationCompleteParams&
347 RegistrationFetcher::RegistrationCompleteParams::operator=(
348 RegistrationFetcher::RegistrationCompleteParams&& other) noexcept = default;
349
350 RegistrationFetcher::RegistrationCompleteParams::~RegistrationCompleteParams() =
351 default;
352
353 // static
354 void RegistrationFetcher::StartCreateTokenAndFetch(
355 RegistrationFetcherParam registration_params,
356 unexportable_keys::UnexportableKeyService& key_service,
357 // TODO(kristianm): Check the lifetime of context and make sure this use
358 // is safe.
359 const URLRequestContext* context,
360 const IsolationInfo& isolation_info,
361 RegistrationCompleteCallback callback) {
362 // Using mock fetcher for testing
363 if (g_mock_fetcher) {
364 std::move(callback).Run(g_mock_fetcher());
365 return;
366 }
367
368 const auto supported_algos = registration_params.supported_algos();
369 auto request_params =
370 RegistrationRequestParam::Create(std::move(registration_params));
371 // `key_service` is created along with `SessionService` and will be valid
372 // until the browser ends, hence `std::ref` is safe here.
373 key_service.GenerateSigningKeySlowlyAsync(
374 supported_algos, kTaskPriority,
375 base::BindOnce(&RegistrationFetcher::StartFetchWithExistingKey,
376 std::move(request_params), std::ref(key_service), context,
377 isolation_info, std::move(callback)));
378 }
379
380 // static
381 void RegistrationFetcher::StartFetchWithExistingKey(
382 RegistrationRequestParam request_params,
383 unexportable_keys::UnexportableKeyService& unexportable_key_service,
384 const URLRequestContext* context,
385 const IsolationInfo& isolation_info,
386 RegistrationFetcher::RegistrationCompleteCallback callback,
387 unexportable_keys::ServiceErrorOr<unexportable_keys::UnexportableKeyId>
388 key_id) {
389 // Using mock fetcher for testing.
390 if (g_mock_fetcher) {
391 std::move(callback).Run(g_mock_fetcher());
392 return;
393 }
394
395 if (!key_id.has_value()) {
396 std::move(callback).Run(std::nullopt);
397 return;
398 }
399
400 // RegistrationFetcherImpl manages its own lifetime.
401 RegistrationFetcherImpl* fetcher = new RegistrationFetcherImpl(
402 request_params.TakeRegistrationEndpoint(),
403 request_params.TakeSessionIdentifier(), unexportable_key_service,
404 key_id.value(), context, isolation_info, std::move(callback));
405
406 fetcher->Start(request_params.TakeChallenge(),
407 request_params.TakeAuthorization());
408 }
409
410 void RegistrationFetcher::SetFetcherForTesting(FetcherType func) {
411 if (g_mock_fetcher) {
412 CHECK(!func);
413 g_mock_fetcher = nullptr;
414 } else {
415 g_mock_fetcher = func;
416 }
417 }
418
419 void RegistrationFetcher::CreateTokenAsyncForTesting(
420 unexportable_keys::UnexportableKeyService& unexportable_key_service,
421 std::string challenge,
422 const GURL& registration_url,
423 std::optional<std::string> authorization,
424 base::OnceCallback<
425 void(std::optional<RegistrationFetcher::RegistrationTokenResult>)>
426 callback) {
427 static constexpr crypto::SignatureVerifier::SignatureAlgorithm
428 kSupportedAlgos[] = {crypto::SignatureVerifier::ECDSA_SHA256,
429 crypto::SignatureVerifier::RSA_PKCS1_SHA256};
430 unexportable_key_service.GenerateSigningKeySlowlyAsync(
431 kSupportedAlgos, kTaskPriority,
432 base::BindOnce(
433 [](unexportable_keys::UnexportableKeyService& key_service,
434 const GURL& registration_url, const std::string& challenge,
435 std::optional<std::string>&& authorization,
436 base::OnceCallback<void(
437 std::optional<RegistrationFetcher::RegistrationTokenResult>)>
438 callback,
439 unexportable_keys::ServiceErrorOr<
440 unexportable_keys::UnexportableKeyId> key_result) {
441 if (!key_result.has_value()) {
442 std::move(callback).Run(std::nullopt);
443 return;
444 }
445
446 SignChallengeWithKey(key_service, key_result.value(),
447 registration_url, challenge,
448 std::move(authorization), std::move(callback));
449 },
450 std::ref(unexportable_key_service), registration_url,
451 std::move(challenge), std::move(authorization), std::move(callback)));
452 }
453
454 } // namespace net::device_bound_sessions
455