| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/interest_group/trusted_signals_fetcher.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <bit> |
| #include <map> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/containers/flat_set.h" |
| #include "base/containers/span.h" |
| #include "base/containers/span_reader.h" |
| #include "base/containers/span_writer.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "base/types/expected.h" |
| #include "base/unguessable_token.h" |
| #include "bidding_and_auction_server_key_fetcher.h" |
| #include "components/cbor/values.h" |
| #include "components/cbor/writer.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/interest_group/auction_downloader_delegate.h" |
| #include "content/browser/interest_group/data_decoder_manager.h" |
| #include "content/browser/interest_group/devtools_enums.h" |
| #include "content/browser/renderer_host/private_network_access_util.h" |
| #include "content/common/content_export.h" |
| #include "content/public/browser/frame_tree_node_id.h" |
| #include "content/services/auction_worklet/public/cpp/auction_downloader.h" |
| #include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h" |
| #include "net/base/isolation_info.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/third_party/quiche/src/quiche/oblivious_http/buffers/oblivious_http_request.h" |
| #include "net/third_party/quiche/src/quiche/oblivious_http/buffers/oblivious_http_response.h" |
| #include "net/third_party/quiche/src/quiche/oblivious_http/common/oblivious_http_header_key_config.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/client_security_state.mojom.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "services/network/public/mojom/ip_address_space.mojom.h" |
| #include "services/network/public/mojom/url_loader_completion_status.mojom-forward.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/boringssl/src/include/openssl/hpke.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Supported compression formats. |
| constexpr std::array<std::string_view, 2> kAcceptCompression = {"none", "gzip"}; |
| |
| // Lengths of various components of request and response header components. |
| constexpr size_t kCompressionFormatSize = 1; // bytes |
| constexpr size_t kCborStringLengthSize = 4; // bytes |
| constexpr size_t kOhttpHeaderSize = 55; // bytes |
| |
| struct IsolationIndex { |
| int compression_group_id; |
| int partition_id; |
| }; |
| |
| // Creates a single entry for the "arguments" array of a partition, with a |
| // single tag and an array of values. |
| cbor::Value MakeArgument(std::string_view tag, cbor::Value::ArrayValue data) { |
| cbor::Value::MapValue argument; |
| |
| cbor::Value::ArrayValue tags; |
| tags.emplace_back(tag); |
| argument.try_emplace(cbor::Value("tags"), cbor::Value(std::move(tags))); |
| argument.try_emplace(cbor::Value("data"), std::move(data)); |
| |
| return cbor::Value(std::move(argument)); |
| } |
| |
| // Creates a single entry for the "arguments" array of a partition, with a |
| // single tag and an array that contains the single passed-in `data` value. |
| cbor::Value MakeArgument(std::string_view tag, cbor::Value data) { |
| cbor::Value::ArrayValue cbor_array; |
| cbor_array.emplace_back(std::move(data)); |
| return MakeArgument(tag, std::move(cbor_array)); |
| } |
| |
| // Creates a single entry for the "arguments" array of a partition, with a |
| // single tag and a variable number of string data values, from a set of |
| // strings. |
| cbor::Value MakeArgument(std::string_view tag, |
| const std::set<std::string>& data) { |
| cbor::Value::ArrayValue cbor_data; |
| for (const auto& element : data) { |
| cbor_data.emplace_back(element); |
| } |
| return MakeArgument(tag, std::move(cbor_data)); |
| } |
| |
| // BiddingPartition overload of BuildMapForPartition(). |
| cbor::Value::MapValue BuildMapForPartition( |
| int compression_group_id, |
| const TrustedSignalsFetcher::BiddingPartition& bidding_partition) { |
| cbor::Value::MapValue partition_cbor_map; |
| |
| partition_cbor_map.try_emplace(cbor::Value("compressionGroupId"), |
| cbor::Value(compression_group_id)); |
| partition_cbor_map.try_emplace(cbor::Value("id"), |
| cbor::Value(bidding_partition.partition_id)); |
| |
| if (!bidding_partition.additional_params->empty()) { |
| cbor::Value::MapValue metadata; |
| for (const auto param : *bidding_partition.additional_params) { |
| // TODO(crbug.com/333445540): Consider switching to taking |
| // `additional_params` as a cbor::Value, for greater flexibility. The |
| // `slotSizes` parameter, in particular, might be best represented as an |
| // array. cbor::Value doesn't have operator<, having a Less comparator |
| // instead, so would need to add that. |
| // |
| // Alternatively, could split this up into the data used to construct it. |
| CHECK(param.second.is_string()); |
| metadata.try_emplace(cbor::Value(param.first), |
| cbor::Value(param.second.GetString())); |
| } |
| partition_cbor_map.try_emplace(cbor::Value("metadata"), |
| cbor::Value(std::move(metadata))); |
| } |
| |
| cbor::Value::ArrayValue arguments; |
| arguments.emplace_back(MakeArgument("interestGroupNames", |
| *bidding_partition.interest_group_names)); |
| arguments.emplace_back(MakeArgument("keys", *bidding_partition.keys)); |
| partition_cbor_map.try_emplace(cbor::Value("arguments"), |
| cbor::Value(std::move(arguments))); |
| |
| return partition_cbor_map; |
| } |
| |
| // ScoringPartition overload of BuildMapForPartition(). |
| cbor::Value::MapValue BuildMapForPartition( |
| int compression_group_id, |
| const TrustedSignalsFetcher::ScoringPartition& scoring_partition) { |
| cbor::Value::MapValue partition_cbor_map; |
| |
| partition_cbor_map.try_emplace(cbor::Value("compressionGroupId"), |
| cbor::Value(compression_group_id)); |
| partition_cbor_map.try_emplace(cbor::Value("id"), |
| cbor::Value(scoring_partition.partition_id)); |
| |
| if (!scoring_partition.additional_params->empty()) { |
| cbor::Value::MapValue metadata; |
| for (const auto param : *scoring_partition.additional_params) { |
| // TODO(crbug.com/333445540): Consider switching to taking |
| // `additional_params` as a cbor::Value, for greater flexibility. |
| // |
| // Alternatively, could split this up into the data used to construct it. |
| CHECK(param.second.is_string()); |
| metadata.try_emplace(cbor::Value(param.first), |
| cbor::Value(param.second.GetString())); |
| } |
| partition_cbor_map.try_emplace(cbor::Value("metadata"), |
| cbor::Value(std::move(metadata))); |
| } |
| |
| cbor::Value::ArrayValue arguments; |
| arguments.emplace_back(MakeArgument( |
| "renderURLs", cbor::Value(scoring_partition.render_url->spec()))); |
| |
| if (!scoring_partition.component_render_urls->empty()) { |
| cbor::Value::ArrayValue component_urls; |
| for (const GURL& component_render_urls : |
| *scoring_partition.component_render_urls) { |
| component_urls.emplace_back(component_render_urls.spec()); |
| } |
| arguments.emplace_back( |
| MakeArgument("adComponentRenderURLs", std::move(component_urls))); |
| } |
| |
| partition_cbor_map.try_emplace(cbor::Value("arguments"), |
| cbor::Value(std::move(arguments))); |
| |
| return partition_cbor_map; |
| } |
| |
| // BiddingPartition overload of CollectContextualData(). |
| void CollectContextualData( |
| int compression_group_id, |
| const TrustedSignalsFetcher::BiddingPartition& bidding_partition, |
| std::map<std::string, std::vector<IsolationIndex>>& |
| contextual_data_ids_map) { |
| if (bidding_partition.buyer_tkv_signals != nullptr) { |
| contextual_data_ids_map[*bidding_partition.buyer_tkv_signals].emplace_back( |
| compression_group_id, bidding_partition.partition_id); |
| } |
| } |
| |
| // ScoringPartition overload of CollectContextualData(). |
| void CollectContextualData( |
| int compression_group_id, |
| const TrustedSignalsFetcher::ScoringPartition& scoring_partition, |
| std::map<std::string, std::vector<IsolationIndex>>& |
| contextual_data_ids_map) { |
| if (scoring_partition.seller_tkv_signals != nullptr) { |
| contextual_data_ids_map[*scoring_partition.seller_tkv_signals].emplace_back( |
| compression_group_id, scoring_partition.partition_id); |
| } |
| } |
| |
| void AddPerPartitionMetadata( |
| cbor::Value::MapValue& request_map_value, |
| size_t partition_count, |
| const std::map<std::string, std::vector<IsolationIndex>>& |
| contextual_data_ids_map) { |
| if (contextual_data_ids_map.empty()) { |
| return; |
| } |
| |
| cbor::Value::MapValue partitioned_metadata_map; |
| cbor::Value::ArrayValue contextual_data_array; |
| |
| for (const auto& signal_index_pair : contextual_data_ids_map) { |
| cbor::Value::MapValue contextual_data_map; |
| |
| // Add signal string to `contextualData`. |
| contextual_data_map.try_emplace(cbor::Value("value"), |
| cbor::Value(signal_index_pair.first)); |
| |
| // The `ids` list is omitted if all partitions share the same contextual |
| // signal value. In other words, if a signal corresponds to a number of |
| // partitions less than the total, its entry in `contextualData` must |
| // include the `ids` list. |
| if (signal_index_pair.second.size() != partition_count) { |
| cbor::Value::ArrayValue ids_array; |
| |
| for (const auto& index : signal_index_pair.second) { |
| cbor::Value::ArrayValue id_pair; |
| // Emplace compression group id and partition id in order. |
| id_pair.emplace_back(index.compression_group_id); |
| id_pair.emplace_back(index.partition_id); |
| ids_array.emplace_back(std::move(id_pair)); |
| } |
| |
| contextual_data_map.try_emplace(cbor::Value("ids"), |
| cbor::Value(std::move(ids_array))); |
| } |
| |
| contextual_data_array.emplace_back(std::move(contextual_data_map)); |
| } |
| |
| partitioned_metadata_map.try_emplace( |
| cbor::Value("contextualData"), |
| cbor::Value(std::move(contextual_data_array))); |
| request_map_value.try_emplace( |
| cbor::Value("perPartitionMetadata"), |
| cbor::Value(std::move(partitioned_metadata_map))); |
| } |
| |
| std::string CreateRequestBodyFromCbor(cbor::Value cbor_value) { |
| std::optional<std::vector<uint8_t>> maybe_cbor_bytes = |
| cbor::Writer::Write(cbor_value); |
| CHECK(maybe_cbor_bytes.has_value()); |
| |
| std::string request_body; |
| size_t size_before_padding = kOhttpHeaderSize + kCompressionFormatSize + |
| kCborStringLengthSize + maybe_cbor_bytes->size(); |
| size_t size_with_padding = std::bit_ceil(size_before_padding); |
| size_t request_body_size = size_with_padding - kOhttpHeaderSize; |
| request_body.resize(request_body_size, 0x00); |
| |
| base::SpanWriter writer(base::as_writable_byte_span(request_body)); |
| |
| // Add framing header. First byte includes version and compression format. |
| // Always set first byte to 0x00 because request body is uncompressed. |
| writer.WriteU8BigEndian(0x00); |
| writer.WriteU32BigEndian( |
| base::checked_cast<uint32_t>(maybe_cbor_bytes->size())); |
| |
| // Add CBOR string. |
| writer.Write(base::as_byte_span(*maybe_cbor_bytes)); |
| |
| DCHECK_EQ(writer.num_written(), size_before_padding - kOhttpHeaderSize); |
| |
| // TODO(crbug.com/333445540): Add encryption. |
| |
| return request_body; |
| } |
| |
| // Builds the request body for bidding and scoring requests. The outer body is |
| // the same, only the data in the partitions is different, so a template works |
| // well for this. PartitionType is either BiddingPartition or ScoringPartition. |
| template <typename PartitionType> |
| std::string BuildSignalsRequestBody( |
| std::string_view hostname, |
| const std::map<int, std::vector<PartitionType>>& compression_groups) { |
| cbor::Value::MapValue request_map_value; |
| cbor::Value::ArrayValue accept_compression(kAcceptCompression.begin(), |
| kAcceptCompression.end()); |
| request_map_value.emplace(cbor::Value("acceptCompression"), |
| cbor::Value(std::move(accept_compression))); |
| |
| cbor::Value::MapValue metadata; |
| metadata.try_emplace(cbor::Value("hostname"), cbor::Value(hostname)); |
| request_map_value.try_emplace(cbor::Value("metadata"), |
| cbor::Value(std::move(metadata))); |
| |
| cbor::Value::ArrayValue partition_array; |
| size_t partition_count = 0; |
| // A map of `contextual_data` to indices for each partition, where |
| // the keys are signal strings and the values are lists of compression group |
| // id and partition id pair. |
| std::map<std::string, std::vector<IsolationIndex>> contextual_data_ids_map; |
| |
| for (const auto& group_pair : compression_groups) { |
| int compression_group_id = group_pair.first; |
| for (const auto& partition : group_pair.second) { |
| cbor::Value::MapValue partition_cbor_map = |
| BuildMapForPartition(compression_group_id, partition); |
| partition_array.emplace_back(partition_cbor_map); |
| ++partition_count; |
| |
| CollectContextualData(compression_group_id, partition, |
| contextual_data_ids_map); |
| } |
| } |
| |
| AddPerPartitionMetadata(request_map_value, partition_count, |
| contextual_data_ids_map); |
| request_map_value.emplace(cbor::Value("partitions"), |
| cbor::Value(std::move(partition_array))); |
| |
| return CreateRequestBodyFromCbor(cbor::Value(std::move(request_map_value))); |
| } |
| |
| } // namespace |
| |
| TrustedSignalsFetcher::BiddingPartition::BiddingPartition( |
| int partition_id, |
| const std::set<std::string>* interest_group_names, |
| const std::set<std::string>* keys, |
| const base::Value::Dict* additional_params, |
| const std::string* buyer_tkv_signals) |
| : partition_id(partition_id), |
| interest_group_names(*interest_group_names), |
| keys(*keys), |
| additional_params(*additional_params), |
| buyer_tkv_signals(buyer_tkv_signals) {} |
| |
| TrustedSignalsFetcher::BiddingPartition::BiddingPartition(BiddingPartition&&) = |
| default; |
| |
| TrustedSignalsFetcher::BiddingPartition::~BiddingPartition() = default; |
| |
| TrustedSignalsFetcher::BiddingPartition& |
| TrustedSignalsFetcher::BiddingPartition::operator=(BiddingPartition&&) = |
| default; |
| |
| TrustedSignalsFetcher::ScoringPartition::ScoringPartition( |
| int partition_id, |
| const GURL* render_url, |
| const std::set<GURL>* component_render_urls, |
| const base::Value::Dict* additional_params, |
| const std::string* seller_tkv_signals) |
| : partition_id(partition_id), |
| render_url(*render_url), |
| component_render_urls(*component_render_urls), |
| additional_params(*additional_params), |
| seller_tkv_signals(seller_tkv_signals) {} |
| |
| TrustedSignalsFetcher::ScoringPartition::ScoringPartition(ScoringPartition&&) = |
| default; |
| |
| TrustedSignalsFetcher::ScoringPartition::~ScoringPartition() = default; |
| |
| TrustedSignalsFetcher::ScoringPartition& |
| TrustedSignalsFetcher::ScoringPartition::operator=(ScoringPartition&&) = |
| default; |
| |
| TrustedSignalsFetcher::CompressionGroupResult::CompressionGroupResult() = |
| default; |
| TrustedSignalsFetcher::CompressionGroupResult::CompressionGroupResult( |
| CompressionGroupResult&&) = default; |
| |
| TrustedSignalsFetcher::CompressionGroupResult::~CompressionGroupResult() = |
| default; |
| |
| TrustedSignalsFetcher::CompressionGroupResult& |
| TrustedSignalsFetcher::CompressionGroupResult::operator=( |
| CompressionGroupResult&&) = default; |
| |
| TrustedSignalsFetcher::TrustedSignalsFetcher() = default; |
| |
| TrustedSignalsFetcher::~TrustedSignalsFetcher() = default; |
| |
| void TrustedSignalsFetcher::FetchBiddingSignals( |
| DataDecoderManager& data_decoder_manager, |
| network::mojom::URLLoaderFactory* url_loader_factory, |
| FrameTreeNodeId frame_tree_node_id, |
| base::flat_set<std::string> devtools_auction_ids, |
| const url::Origin& main_frame_origin, |
| network::mojom::IPAddressSpace ip_address_space, |
| base::UnguessableToken network_partition_nonce, |
| const url::Origin& script_origin, |
| const GURL& trusted_bidding_signals_url, |
| const BiddingAndAuctionServerKey& bidding_and_auction_key, |
| const std::map<int, std::vector<BiddingPartition>>& compression_groups, |
| Callback callback) { |
| EncryptRequestBodyAndStart( |
| data_decoder_manager, url_loader_factory, |
| InterestGroupAuctionFetchType::kBidderTrustedSignals, frame_tree_node_id, |
| std::move(devtools_auction_ids), main_frame_origin, ip_address_space, |
| network_partition_nonce, script_origin, trusted_bidding_signals_url, |
| bidding_and_auction_key, |
| BuildSignalsRequestBody(main_frame_origin.host(), compression_groups), |
| std::move(callback)); |
| } |
| |
| void TrustedSignalsFetcher::FetchScoringSignals( |
| DataDecoderManager& data_decoder_manager, |
| network::mojom::URLLoaderFactory* url_loader_factory, |
| FrameTreeNodeId frame_tree_node_id, |
| base::flat_set<std::string> devtools_auction_ids, |
| const url::Origin& main_frame_origin, |
| network::mojom::IPAddressSpace ip_address_space, |
| base::UnguessableToken network_partition_nonce, |
| const url::Origin& script_origin, |
| const GURL& trusted_scoring_signals_url, |
| const BiddingAndAuctionServerKey& bidding_and_auction_key, |
| const std::map<int, std::vector<ScoringPartition>>& compression_groups, |
| Callback callback) { |
| EncryptRequestBodyAndStart( |
| data_decoder_manager, url_loader_factory, |
| InterestGroupAuctionFetchType::kSellerTrustedSignals, frame_tree_node_id, |
| std::move(devtools_auction_ids), main_frame_origin, ip_address_space, |
| network_partition_nonce, script_origin, trusted_scoring_signals_url, |
| bidding_and_auction_key, |
| BuildSignalsRequestBody(main_frame_origin.host(), compression_groups), |
| std::move(callback)); |
| } |
| |
| void TrustedSignalsFetcher::EncryptRequestBodyAndStart( |
| DataDecoderManager& data_decoder_manager, |
| network::mojom::URLLoaderFactory* url_loader_factory, |
| InterestGroupAuctionFetchType fetch_type, |
| FrameTreeNodeId frame_tree_node_id, |
| base::flat_set<std::string> devtools_auction_ids, |
| const url::Origin& main_frame_origin, |
| network::mojom::IPAddressSpace ip_address_space, |
| base::UnguessableToken network_partition_nonce, |
| const url::Origin& script_origin, |
| const GURL& trusted_signals_url, |
| const BiddingAndAuctionServerKey& bidding_and_auction_key, |
| std::string plaintext_request_body, |
| Callback callback) { |
| DCHECK(!auction_downloader_); |
| DCHECK(!callback_); |
| trusted_signals_url_ = trusted_signals_url; |
| |
| // Request a DataDecoder now to pre-warm it for when data is received. |
| decoder_handle_ = |
| data_decoder_manager.GetHandle(main_frame_origin, script_origin); |
| // Need to call GetService() to actually trigger creation of the underlying |
| // service. |
| decoder_handle_->data_decoder().GetService(); |
| |
| callback_ = std::move(callback); |
| |
| uint32_t key_id = 0; |
| bool success = base::HexStringToUInt( |
| std::string_view(bidding_and_auction_key.id).substr(0, 2), &key_id); |
| DCHECK(success); |
| |
| // Add encryption for request body. |
| auto maybe_key_config = quiche::ObliviousHttpHeaderKeyConfig::Create( |
| key_id, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, |
| EVP_HPKE_AES_256_GCM); |
| CHECK(maybe_key_config.ok()); |
| |
| auto maybe_ciphertext_request_body = |
| quiche::ObliviousHttpRequest::CreateClientObliviousRequest( |
| std::move(plaintext_request_body), bidding_and_auction_key.key, |
| *maybe_key_config, kRequestMediaType); |
| CHECK(maybe_ciphertext_request_body.ok()); |
| |
| network::ResourceRequest::TrustedParams trusted_params; |
| |
| // IsolationInfos usually use main frame origin and frame origin, to separate |
| // the disk cache, and prevent frames from spying on each other's cache |
| // entries. These requests aren't cached (due to being POSTs), and use their |
| // own nonce, so no frames can pull the responses from the cache, even the |
| // main frame. |
| // |
| // The frame origin is also used to populate a cross-origin bit in the |
| // NetworkIsolationKey, to separate out other network resources for connection |
| // spying. The nonce similarly makes that sort of spying not useful |
| // - the only leak is the run time of entire auction, which leaks minimal |
| // information about whether there's a pre-existing connection. There's no way |
| // for frames to probe in depth connection info more directly, since they |
| // can't make network requests directly using the `network_partition_nonce`. |
| trusted_params.isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, |
| /*top_frame_origin=*/main_frame_origin, |
| /*frame_origin=*/main_frame_origin, net::SiteForCookies(), |
| network_partition_nonce); |
| |
| auto client_security_state = network::mojom::ClientSecurityState::New(); |
| client_security_state->ip_address_space = ip_address_space; |
| client_security_state->is_web_secure_context = true; |
| client_security_state->private_network_request_policy = |
| network::mojom::PrivateNetworkRequestPolicy::kBlock; |
| trusted_params.client_security_state = std::move(client_security_state); |
| |
| auction_downloader_ = std::make_unique<auction_worklet::AuctionDownloader>( |
| url_loader_factory, trusted_signals_url, |
| auction_worklet::AuctionDownloader::DownloadMode::kActualDownload, |
| auction_worklet::AuctionDownloader::MimeType::kAdAuctionTrustedSignals, |
| maybe_ciphertext_request_body->EncapsulateAndSerialize(), |
| std::string(kRequestMediaType), |
| /*request_initiator=*/script_origin, std::move(trusted_params), |
| base::BindOnce(&TrustedSignalsFetcher::OnRequestComplete, |
| base::Unretained(this)), |
| AuctionDownloaderDelegate::MaybeCreate(frame_tree_node_id)); |
| ohttp_context_ = std::make_unique<quiche::ObliviousHttpRequest::Context>( |
| std::move(maybe_ciphertext_request_body).value().ReleaseContext()); |
| if (frame_tree_node_id && |
| devtools_instrumentation::NeedInterestGroupAuctionEvents( |
| frame_tree_node_id)) { |
| devtools_instrumentation::OnInterestGroupAuctionNetworkRequestCreated( |
| frame_tree_node_id, fetch_type, auction_downloader_->request_id(), |
| std::move(devtools_auction_ids).extract()); |
| } |
| } |
| |
| void TrustedSignalsFetcher::OnRequestComplete( |
| std::unique_ptr<std::string> response_body, |
| scoped_refptr<net::HttpResponseHeaders> headers, |
| std::optional<std::string> error) { |
| // `auction_downloader_` is no longer needed. |
| auction_downloader_.reset(); |
| |
| if (!response_body) { |
| std::move(callback_).Run(base::unexpected(std::move(error).value())); |
| return; |
| } |
| |
| // The oblivious HTTP code returns an error on empty response bodies, so only |
| // try and decrypt if the body is not empty, to give an error about size |
| // rather than OHTTP failing in that case. |
| std::string plaintext_response_body; |
| if (response_body->size() > 0u) { |
| auto maybe_plaintext_response_body = |
| quiche::ObliviousHttpResponse::CreateClientObliviousResponse( |
| std::move(*response_body), *ohttp_context_, kResponseMediaType); |
| // `ohttp_context_` is no longer needed. |
| ohttp_context_.reset(); |
| |
| if (!maybe_plaintext_response_body.ok()) { |
| // Don't output OHTTP error strings, directly, as they're often not very |
| // user-friendly. |
| std::move(callback_).Run( |
| base::unexpected(CreateError("OHTTP decryption failed"))); |
| return; |
| } |
| plaintext_response_body = |
| std::move(maybe_plaintext_response_body).value().ConsumePlaintextData(); |
| } |
| |
| base::SpanReader reader(base::as_byte_span(plaintext_response_body)); |
| uint8_t compression_scheme_bytes; |
| uint32_t cbor_length; |
| if (!reader.ReadU8BigEndian(compression_scheme_bytes) || |
| !reader.ReadU32BigEndian(cbor_length)) { |
| std::move(callback_).Run(base::unexpected(CreateError( |
| base::StringPrintf("Response body is shorter than a %s header", |
| kResponseMediaType.data())))); |
| return; |
| } |
| |
| // Only the first to bits are used for compression format in the whole byte. |
| compression_scheme_bytes &= 0x03; |
| if (compression_scheme_bytes == 0x00) { |
| compression_scheme_ = |
| auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone; |
| } else if (compression_scheme_bytes == 0x02) { |
| compression_scheme_ = |
| auction_worklet::mojom::TrustedSignalsCompressionScheme::kGzip; |
| } else { |
| std::move(callback_).Run(base::unexpected(CreateError(base::StringPrintf( |
| "Unsupported compression scheme: %u", compression_scheme_bytes)))); |
| return; |
| } |
| |
| base::span<const uint8_t> remaining_span = reader.remaining_span(); |
| if (remaining_span.size() < cbor_length) { |
| std::move(callback_).Run( |
| base::unexpected(CreateError("Length header exceeds body size"))); |
| return; |
| } |
| |
| base::span<const uint8_t> cbor = remaining_span.first(cbor_length); |
| decoder_handle_->data_decoder().ParseCbor( |
| cbor, base::BindOnce(&TrustedSignalsFetcher::OnCborParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void TrustedSignalsFetcher::OnCborParsed( |
| data_decoder::DataDecoder::ValueOrError value_or_error) { |
| std::move(callback_).Run(ParseDataDecoderResult(std::move(value_or_error))); |
| } |
| |
| TrustedSignalsFetcher::SignalsFetchResult |
| TrustedSignalsFetcher::ParseDataDecoderResult( |
| data_decoder::DataDecoder::ValueOrError value_or_error) { |
| if (!value_or_error.has_value()) { |
| return base::unexpected(CreateError("Failed to parse response as CBOR")); |
| } |
| |
| if (!value_or_error->is_dict()) { |
| // Since the data is CBOR, use CBOR type names in error messages ("map" |
| // instead of JSON "object" or Value "dict"). |
| return base::unexpected(CreateError("Response body is not a map")); |
| } |
| |
| base::Value::Dict& dict = value_or_error->GetDict(); |
| |
| // Get compression groups. |
| base::Value::List* compression_groups = dict.FindList("compressionGroups"); |
| if (!compression_groups) { |
| return base::unexpected( |
| CreateError("Response is missing compressionGroups array")); |
| } |
| |
| CompressionGroupResultMap compression_groups_out; |
| for (auto& compression_group : *compression_groups) { |
| int compression_group_id; |
| // This consumes each value of the list, to avoid having to copy the |
| // contents of each compression group. |
| auto compression_group_result = ParseCompressionGroup( |
| std::move(compression_group), compression_group_id); |
| |
| if (!compression_group_result.has_value()) { |
| return base::unexpected(std::move(compression_group_result).error()); |
| } |
| |
| if (!compression_groups_out |
| .try_emplace(compression_group_id, |
| std::move(compression_group_result).value()) |
| .second) { |
| return base::unexpected(CreateError(base::StringPrintf( |
| "Response contains two compression groups with id %i", |
| compression_group_id))); |
| } |
| } |
| |
| return compression_groups_out; |
| } |
| |
| base::expected<TrustedSignalsFetcher::CompressionGroupResult, std::string> |
| TrustedSignalsFetcher::ParseCompressionGroup( |
| base::Value compression_group_value, |
| int& compression_group_id) { |
| if (!compression_group_value.is_dict()) { |
| return base::unexpected(CreateError( |
| base::StringPrintf("Compression group is not of type map"))); |
| } |
| |
| base::Value::Dict& compression_group_dict = compression_group_value.GetDict(); |
| std::optional<int> compression_group_id_opt = |
| compression_group_dict.FindInt("compressionGroupId"); |
| if (!compression_group_id_opt.has_value() || *compression_group_id_opt < 0) { |
| return base::unexpected(CreateError( |
| base::StringPrintf("Compression group must have a non-negative integer " |
| "compressionGroupId"))); |
| } |
| |
| const base::Value* ttl_ms_value = compression_group_dict.Find("ttlMs"); |
| // Default TTL is 0. |
| base::TimeDelta ttl; |
| if (ttl_ms_value) { |
| if (!ttl_ms_value->is_int()) { |
| return base::unexpected(CreateError(base::StringPrintf( |
| "Compression group %i ttlMs value is not an integer", |
| *compression_group_id_opt))); |
| } |
| // Treat negative values as 0. Using zero is more robust if these values are |
| // ever used to set a timer. |
| ttl = base::Milliseconds(std::max(0, ttl_ms_value->GetInt())); |
| } |
| |
| auto* content = compression_group_dict.FindBlob("content"); |
| if (!content) { |
| return base::unexpected(CreateError(base::StringPrintf( |
| "Compression group %i missing binary string \"content\"", |
| *compression_group_id_opt))); |
| } |
| |
| compression_group_id = *compression_group_id_opt; |
| |
| CompressionGroupResult result; |
| result.compression_scheme = compression_scheme_; |
| result.compression_group_data = std::move(*content); |
| result.ttl = ttl; |
| return result; |
| } |
| |
| std::string TrustedSignalsFetcher::CreateError( |
| const std::string& error_message) { |
| return base::StringPrintf("Failed to load %s: %s.", |
| trusted_signals_url_.spec().c_str(), |
| error_message.c_str()); |
| } |
| |
| } // namespace content |