0

Add Trusted Signals Server KVv2 Support - Bidding signal request

Add support to Trusted Signals Server KVv2 for trusted bidding signals
request CBOR encoding.

Bug: 337917489
Change-Id: I53d8026fa4fa65979fbb8f4437b379d9662f2c9c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5521862
Reviewed-by: mmenke <mmenke@chromium.org>
Commit-Queue: Tianyang Xu <xtlsheep@google.com>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1327732}
This commit is contained in:
Tianyang Xu
2024-07-15 19:47:34 +00:00
committed by Chromium LUCI CQ
parent 23ed148d73
commit c0d554350d
7 changed files with 932 additions and 78 deletions

@ -86,6 +86,8 @@ source_set("auction_worklet") {
"shared_storage_bindings.h",
"trusted_signals.cc",
"trusted_signals.h",
"trusted_signals_kvv2_helper.cc",
"trusted_signals_kvv2_helper.h",
"trusted_signals_request_manager.cc",
"trusted_signals_request_manager.h",
"webidl_compat.cc",
@ -107,6 +109,7 @@ source_set("auction_worklet") {
deps = [
":protocol_sources",
"//base",
"//components/cbor:cbor",
"//content:export",
"//content/common",
"//content/public/common:common_sources",
@ -156,6 +159,7 @@ source_set("tests") {
"direct_from_seller_signals_requester_unittest.cc",
"public/cpp/auction_downloader_unittest.cc",
"seller_worklet_unittest.cc",
"trusted_signals_kvv2_helper_unittest.cc",
"trusted_signals_request_manager_unittest.cc",
"trusted_signals_unittest.cc",
"webidl_compat_unittest.cc",
@ -179,6 +183,7 @@ source_set("tests") {
deps = [
"//base",
"//base/test:test_support",
"//components/cbor:cbor",
"//content/common:for_content_tests",
"//content/public/common:common",
"//gin",

@ -3,6 +3,7 @@
# to this service, make sure they're not inadvertently breaking these
# isolation requirements. Please thoroughly discuss additions with OWNERS.
include_rules = [
"+components/cbor",
"+gin",
"+net",
"+services/network/public",

@ -139,81 +139,6 @@ std::map<std::string, AuctionV8Helper::SerializedValue> ParseChildKeyValueMap(
return ParseKeyValueMap(v8_helper, named_object_value.As<v8::Object>(), keys);
}
// Attempts to parse the `priorityVector` value in `v8_per_interest_group_data`,
// expecting it to be a string-to-number mapping. Returns the parsed mapping, or
// nullopt upon failure to find or parse the field. Any case where
// `priorityVector` exists and is an object is considered a success, even if
// it's empty, or some/all keys in it are mapped to things other than numbers.
std::optional<TrustedSignals::Result::PriorityVector> ParsePriorityVector(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_per_interest_group_data) {
v8::Local<v8::Value> priority_vector_value;
if (!v8_per_interest_group_data
->Get(v8_helper->scratch_context(),
v8_helper->CreateStringFromLiteral("priorityVector"))
.ToLocal(&priority_vector_value) ||
!priority_vector_value->IsObject() ||
// Arrays are considered objects, so check explicitly for them.
priority_vector_value->IsArray()) {
return std::nullopt;
}
v8::Local<v8::Object> priority_vector_object =
priority_vector_value.As<v8::Object>();
v8::Local<v8::Array> priority_vector_keys;
if (!priority_vector_object->GetOwnPropertyNames(v8_helper->scratch_context())
.ToLocal(&priority_vector_keys)) {
return std::nullopt;
}
// Put together a list to construct the returned `flat_map` with, since
// insertion is O(n) while construction is O(n log n).
std::vector<std::pair<std::string, double>> priority_vector_pairs;
for (uint32_t i = 0; i < priority_vector_keys->Length(); ++i) {
v8::Local<v8::Value> v8_key;
std::string key;
if (!priority_vector_keys->Get(v8_helper->scratch_context(), i)
.ToLocal(&v8_key) ||
!v8_key->IsString() ||
!gin::ConvertFromV8(v8_helper->isolate(), v8_key, &key)) {
continue;
}
v8::Local<v8::Value> v8_value;
double value;
if (!priority_vector_object->Get(v8_helper->scratch_context(), v8_key)
.ToLocal(&v8_value) ||
!v8_value->IsNumber() ||
!v8_value->NumberValue(v8_helper->scratch_context()).To(&value)) {
continue;
}
priority_vector_pairs.emplace_back(std::move(key), value);
}
return TrustedSignals::Result::PriorityVector(
std::move(priority_vector_pairs));
}
// Attempts to parse the `updateIfOlderThanMs` value in
// `v8_per_interest_group_data`, expecting it to be a double duration in
// milliseconds. Returns the time delta, or nullopt upon failure to find or
// parse the value.
std::optional<base::TimeDelta> ParseUpdateIfOlderThan(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_per_interest_group_data) {
v8::Local<v8::Value> update_if_older_than_ms_value;
double update_if_older_than_ms;
if (!v8_per_interest_group_data
->Get(v8_helper->scratch_context(),
v8_helper->CreateStringFromLiteral("updateIfOlderThanMs"))
.ToLocal(&update_if_older_than_ms_value) ||
!update_if_older_than_ms_value->IsNumber() ||
!update_if_older_than_ms_value->NumberValue(v8_helper->scratch_context())
.To(&update_if_older_than_ms)) {
return std::nullopt;
}
return base::Milliseconds(update_if_older_than_ms);
}
// Attempts to parse the `perInterestGroupData` value in `v8_object`, extracting
// the `priorityVector` fields of all interest group in `interest_group_names`,
// along with `updateIfOlderThanMs`, and putting them all in the returned
@ -256,13 +181,14 @@ TrustedSignals::Result::PerInterestGroupDataMap ParsePerInterestGroupMap(
v8::Local<v8::Object> v8_per_interest_group_data =
per_interest_group_data_value.As<v8::Object>();
std::optional<TrustedSignals::Result::PriorityVector> priority_vector =
ParsePriorityVector(v8_helper, v8_per_interest_group_data);
TrustedSignals::ParsePriorityVector(v8_helper,
v8_per_interest_group_data);
std::optional<base::TimeDelta> update_if_older_than;
if (base::FeatureList::IsEnabled(
features::kInterestGroupUpdateIfOlderThan)) {
update_if_older_than =
ParseUpdateIfOlderThan(v8_helper, v8_per_interest_group_data);
update_if_older_than = TrustedSignals::ParseUpdateIfOlderThan(
v8_helper, v8_per_interest_group_data);
}
if (priority_vector || update_if_older_than) {
out.emplace(interest_group_name, TrustedSignals::Result::PerGroupData(
@ -526,6 +452,75 @@ std::unique_ptr<TrustedSignals> TrustedSignals::LoadScoringSignals(
return trusted_signals;
}
std::optional<TrustedSignals::Result::PriorityVector>
TrustedSignals::ParsePriorityVector(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_per_interest_group_data) {
DCHECK(v8_helper->v8_runner()->RunsTasksInCurrentSequence());
v8::Local<v8::Value> priority_vector_value;
if (!v8_per_interest_group_data
->Get(v8_helper->scratch_context(),
v8_helper->CreateStringFromLiteral("priorityVector"))
.ToLocal(&priority_vector_value) ||
!priority_vector_value->IsObject() ||
// Arrays are considered objects, so check explicitly for them.
priority_vector_value->IsArray()) {
return std::nullopt;
}
v8::Local<v8::Object> priority_vector_object =
priority_vector_value.As<v8::Object>();
v8::Local<v8::Array> priority_vector_keys;
if (!priority_vector_object->GetOwnPropertyNames(v8_helper->scratch_context())
.ToLocal(&priority_vector_keys)) {
return std::nullopt;
}
// Put together a list to construct the returned `flat_map` with, since
// insertion is O(n) while construction is O(n log n).
std::vector<std::pair<std::string, double>> priority_vector_pairs;
for (uint32_t i = 0; i < priority_vector_keys->Length(); ++i) {
v8::Local<v8::Value> v8_key;
std::string key;
if (!priority_vector_keys->Get(v8_helper->scratch_context(), i)
.ToLocal(&v8_key) ||
!v8_key->IsString() ||
!gin::ConvertFromV8(v8_helper->isolate(), v8_key, &key)) {
continue;
}
v8::Local<v8::Value> v8_value;
double value;
if (!priority_vector_object->Get(v8_helper->scratch_context(), v8_key)
.ToLocal(&v8_value) ||
!v8_value->IsNumber() ||
!v8_value->NumberValue(v8_helper->scratch_context()).To(&value)) {
continue;
}
priority_vector_pairs.emplace_back(std::move(key), value);
}
return TrustedSignals::Result::PriorityVector(
std::move(priority_vector_pairs));
}
std::optional<base::TimeDelta> TrustedSignals::ParseUpdateIfOlderThan(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_per_interest_group_data) {
DCHECK(v8_helper->v8_runner()->RunsTasksInCurrentSequence());
v8::Local<v8::Value> update_if_older_than_ms_value;
double update_if_older_than_ms;
if (!v8_per_interest_group_data
->Get(v8_helper->scratch_context(),
v8_helper->CreateStringFromLiteral("updateIfOlderThanMs"))
.ToLocal(&update_if_older_than_ms_value) ||
!update_if_older_than_ms_value->IsNumber() ||
!update_if_older_than_ms_value->NumberValue(v8_helper->scratch_context())
.To(&update_if_older_than_ms)) {
return std::nullopt;
}
return base::Milliseconds(update_if_older_than_ms);
}
TrustedSignals::TrustedSignals(
std::optional<std::set<std::string>> interest_group_names,
std::optional<std::set<std::string>> bidding_signals_keys,

@ -208,6 +208,25 @@ class CONTENT_EXPORT TrustedSignals {
scoped_refptr<AuctionV8Helper> v8_helper,
LoadSignalsCallback load_signals_callback);
// Attempts to parse the `priorityVector` value in
// `v8_per_interest_group_data`,
// expecting it to be a string-to-number mapping. Returns the parsed mapping,
// or nullopt upon failure to find or parse the field. Any case where
// `priorityVector` exists and is an object is considered a success, even if
// it's empty, or some/all keys in it are mapped to things other than numbers.
// Must be called on `v8_helper`'s sequence.
static std::optional<TrustedSignals::Result::PriorityVector>
ParsePriorityVector(AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_per_interest_group_data);
// Attempts to parse the `updateIfOlderThanMs` value in
// `v8_per_interest_group_data`, expecting it to be a double duration in
// milliseconds. Returns the time delta, or nullopt upon failure to find or
// parse the value. Must be called on `v8_helper`'s sequence.
static std::optional<base::TimeDelta> ParseUpdateIfOlderThan(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_per_interest_group_data);
private:
TrustedSignals(
std::optional<std::set<std::string>> interest_group_names,

@ -0,0 +1,314 @@
// 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/services/auction_worklet/trusted_signals_kvv2_helper.h"
#include <sys/types.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <numeric>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base/check.h"
#include "base/containers/span.h"
#include "base/containers/span_writer.h"
#include "base/json/json_reader.h"
#include "base/memory/scoped_refptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.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/types/optional_ref.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "content/common/features.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/trusted_signals.h"
#include "content/services/auction_worklet/trusted_signals_request_manager.h"
#include "url/origin.h"
namespace auction_worklet {
namespace {
// Constants for POST request body.
constexpr std::array<std::string_view, 2> kAcceptCompression = {"none", "gzip"};
constexpr size_t kFramingHeaderSize = 5; // bytes
// Add hardcoded `acceptCompression` to request body.
void AddPostRequestConstants(cbor::Value::MapValue& request_map_value) {
// acceptCompression
cbor::Value::ArrayValue accept_compression(kAcceptCompression.begin(),
kAcceptCompression.end());
request_map_value.try_emplace(cbor::Value("acceptCompression"),
cbor::Value(std::move(accept_compression)));
return;
}
std::string CreateRequestBody(cbor::Value::MapValue request_map_value) {
cbor::Value message(std::move(request_map_value));
std::optional<std::vector<uint8_t>> maybe_msg = cbor::Writer::Write(message);
CHECK(maybe_msg.has_value());
// TODO(crbug.com/337917489): Skip padding for now, and will add padding after
// end to end tests.
std::string request_body;
request_body.resize(kFramingHeaderSize + maybe_msg->size());
base::SpanWriter writer(
base::as_writable_bytes(base::make_span(request_body)));
// First byte includes version and compression format. Always set first bytes
// to 0x00 because request body is not compressed.
writer.WriteU8BigEndian(0x00);
writer.WriteU32BigEndian(base::checked_cast<int>(maybe_msg->size()));
writer.Write(base::as_bytes(base::make_span(*maybe_msg)));
return request_body;
}
// Creates a single entry for the "arguments" array of a partition, with a
// single tag and a variable number of data values.
cbor::Value MakeArgument(std::string_view tag,
const std::set<std::string>& data) {
cbor::Value::MapValue argument;
cbor::Value::ArrayValue tags;
tags.emplace_back(tag);
cbor::Value::ArrayValue cbor_data;
for (const auto& element : data) {
cbor_data.emplace_back(element);
}
argument.emplace(cbor::Value("tags"), cbor::Value(std::move(tags)));
argument.emplace(cbor::Value("data"), cbor::Value(std::move(cbor_data)));
return cbor::Value(std::move(argument));
}
} // namespace
TrustedSignalsKVv2RequestHelper::TrustedSignalsKVv2RequestHelper(
std::string post_request_body)
: post_request_body_(std::move(post_request_body)) {}
TrustedSignalsKVv2RequestHelper::TrustedSignalsKVv2RequestHelper(
TrustedSignalsKVv2RequestHelper&&) = default;
TrustedSignalsKVv2RequestHelper& TrustedSignalsKVv2RequestHelper::operator=(
TrustedSignalsKVv2RequestHelper&&) = default;
TrustedSignalsKVv2RequestHelper::~TrustedSignalsKVv2RequestHelper() = default;
std::string TrustedSignalsKVv2RequestHelper::TakePostRequestBody() {
return std::move(post_request_body_);
}
TrustedSignalsKVv2RequestHelperBuilder ::
~TrustedSignalsKVv2RequestHelperBuilder() = default;
TrustedSignalsKVv2RequestHelperBuilder::TrustedSignalsKVv2RequestHelperBuilder(
std::string hostname,
GURL trusted_signals_url,
std::optional<int> experiment_group_id)
: hostname_(std::move(hostname)),
trusted_signals_url_(std::move(trusted_signals_url)),
experiment_group_id_(experiment_group_id) {}
TrustedSignalsKVv2RequestHelperBuilder::Partition::Partition() = default;
TrustedSignalsKVv2RequestHelperBuilder::Partition::Partition(
int partition_id,
const std::string& interest_group_name,
const std::set<std::string>& bidding_keys,
const std::string& hostname,
const std::optional<int>& experiment_group_id,
std::pair<std::string, std::string> trusted_bidding_signals_slot_size_param)
: partition_id(partition_id),
interest_group_names({interest_group_name}),
bidding_signals_keys(bidding_keys) {
additional_params.Set("hostname", hostname);
if (experiment_group_id.has_value()) {
additional_params.Set("experimentGroupId",
base::NumberToString(experiment_group_id.value()));
}
additional_params.Set(trusted_bidding_signals_slot_size_param.first,
trusted_bidding_signals_slot_size_param.second);
}
TrustedSignalsKVv2RequestHelperBuilder::Partition::Partition(Partition&&) =
default;
TrustedSignalsKVv2RequestHelperBuilder::Partition::~Partition() = default;
TrustedSignalsKVv2RequestHelperBuilder::Partition&
TrustedSignalsKVv2RequestHelperBuilder::Partition::operator=(Partition&&) =
default;
TrustedBiddingSignalsKVv2RequestHelperBuilder::
TrustedBiddingSignalsKVv2RequestHelperBuilder(
const std::string& hostname,
const GURL& trusted_signals_url,
std::optional<int> experiment_group_id,
const std::string& trusted_bidding_signals_slot_size_param)
: TrustedSignalsKVv2RequestHelperBuilder(hostname,
trusted_signals_url,
experiment_group_id) {
// Parse trusted bidding signals slot size parameter to a pair, which
// parameter key is first and value is second.
if (!trusted_bidding_signals_slot_size_param.empty()) {
size_t pos = trusted_bidding_signals_slot_size_param.find('=');
CHECK_NE(pos, std::string::npos);
std::string key = trusted_bidding_signals_slot_size_param.substr(0, pos);
std::string value = trusted_bidding_signals_slot_size_param.substr(pos + 1);
CHECK(key == "slotSize" || key == "allSlotsRequestedSizes");
trusted_bidding_signals_slot_size_param_ = {std::move(key),
std::move(value)};
}
}
TrustedBiddingSignalsKVv2RequestHelperBuilder::
~TrustedBiddingSignalsKVv2RequestHelperBuilder() = default;
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex
TrustedBiddingSignalsKVv2RequestHelperBuilder::AddTrustedSignalsRequest(
base::optional_ref<const std::string> interest_group_name,
base::optional_ref<const std::set<std::string>> bidding_keys,
base::optional_ref<const url::Origin> interest_group_join_origin,
std::optional<blink::mojom::InterestGroup::ExecutionMode> execution_mode) {
DCHECK(interest_group_name.has_value());
DCHECK(bidding_keys.has_value());
DCHECK(interest_group_join_origin.has_value());
DCHECK(execution_mode.has_value());
int partition_id;
int compression_group_id;
// Find or create a compression group.
auto join_origin_compression_id_it =
join_origin_compression_id_map().find(interest_group_join_origin.value());
CompressionGroup* compression_group_ptr;
if (join_origin_compression_id_it == join_origin_compression_id_map().end()) {
// Create a new compression group keyed by joining origin.
compression_group_id = next_compression_group_id();
join_origin_compression_id_map().emplace(interest_group_join_origin.value(),
compression_group_id);
compression_group_ptr =
&compression_groups().try_emplace(compression_group_id).first->second;
} else {
// Found existing compression group.
compression_group_id = join_origin_compression_id_it->second;
DCHECK_EQ(1u, compression_groups().count(compression_group_id));
compression_group_ptr = &compression_groups()[compression_group_id];
}
// Get partition id based on execution mode.
auto partition_it = compression_group_ptr->end();
if (execution_mode ==
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode) {
// Put current interest group to the existing "group-by-origin partition
// which is always index 0.
partition_id = 0;
partition_it = compression_group_ptr->find(partition_id);
} else {
// If execution mode is not `kGroupedByOriginMode`, we assign new partition
// start from index 1. To make partition id consecutive, we use compression
// group size if there is a "group-by-origin" partition, otherwise use size
// plus 1.
if (compression_group_ptr->contains(0)) {
partition_id = compression_group_ptr->size();
} else {
partition_id = compression_group_ptr->size() + 1;
}
DCHECK_EQ(0u, compression_group_ptr->count(partition_id));
}
// Find or create partition.
if (partition_it == compression_group_ptr->end()) {
Partition new_partition(partition_id, interest_group_name.value(),
bidding_keys.value(), hostname(),
experiment_group_id(),
trusted_bidding_signals_slot_size_param_);
compression_group_ptr->emplace(partition_id, std::move(new_partition));
} else {
// We only reuse the group-by-origin partition.
DCHECK_EQ(0, partition_id);
DCHECK_EQ(blink::InterestGroup::ExecutionMode::kGroupedByOriginMode,
execution_mode.value());
partition_it->second.interest_group_names.insert(
interest_group_name.value());
partition_it->second.bidding_signals_keys.insert(bidding_keys->begin(),
bidding_keys->end());
}
return IsolationIndex(compression_group_id, partition_id);
}
TrustedSignalsKVv2RequestHelper
TrustedBiddingSignalsKVv2RequestHelperBuilder::Build() {
cbor::Value::MapValue request_map_value;
AddPostRequestConstants(request_map_value);
cbor::Value::ArrayValue partition_array;
for (const auto& group_pair : compression_groups()) {
int compression_group_id = group_pair.first;
const CompressionGroup& partition_map = group_pair.second;
for (const auto& partition_pair : partition_map) {
const Partition& partition = partition_pair.second;
cbor::Value::MapValue partition_cbor_map = BuildMapForPartition(
partition, partition.partition_id, compression_group_id);
partition_array.emplace_back(partition_cbor_map);
}
}
request_map_value.try_emplace(cbor::Value("partitions"),
cbor::Value(std::move(partition_array)));
std::string request_body = CreateRequestBody(std::move(request_map_value));
return TrustedSignalsKVv2RequestHelper(std::move(request_body));
}
cbor::Value::MapValue
TrustedBiddingSignalsKVv2RequestHelperBuilder::BuildMapForPartition(
const Partition& partition,
int partition_id,
int compression_group_id) {
cbor::Value::MapValue partition_cbor_map;
partition_cbor_map.try_emplace(cbor::Value("id"), cbor::Value(partition_id));
partition_cbor_map.try_emplace(cbor::Value("compressionGroupId"),
cbor::Value(compression_group_id));
// metadata
cbor::Value::MapValue metadata;
for (const auto param : partition.additional_params) {
CHECK(param.second.is_string());
// TODO(xtlsheep): The slot size param probably will be changed to a new
// format in the future. Check if these are still the right types if the
// spec is changed.
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", partition.interest_group_names));
arguments.emplace_back(MakeArgument("keys", partition.bidding_signals_keys));
partition_cbor_map.try_emplace(cbor::Value("arguments"),
cbor::Value(std::move(arguments)));
return partition_cbor_map;
}
} // namespace auction_worklet

@ -0,0 +1,215 @@
// 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.
#ifndef CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SIGNALS_KVV2_HELPER_H_
#define CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SIGNALS_KVV2_HELPER_H_
#include <stdint.h>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "base/types/expected.h"
#include "base/types/optional_ref.h"
#include "components/cbor/values.h"
#include "content/common/content_export.h"
#include "content/services/auction_worklet/trusted_signals.h"
#include "content/services/auction_worklet/trusted_signals_request_manager.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
// Encapsulates the logic for generating trusted signals key-value version 2
// requests.
// TODO(crbug.com/349651946): Remove after KVv2 is migrated to browser process.
namespace auction_worklet {
class CONTENT_EXPORT TrustedSignalsKVv2RequestHelper {
public:
explicit TrustedSignalsKVv2RequestHelper(std::string post_request_body);
TrustedSignalsKVv2RequestHelper(TrustedSignalsKVv2RequestHelper&&);
TrustedSignalsKVv2RequestHelper& operator=(TrustedSignalsKVv2RequestHelper&&);
TrustedSignalsKVv2RequestHelper& operator=(
const TrustedSignalsKVv2RequestHelper&) = delete;
TrustedSignalsKVv2RequestHelper(const TrustedSignalsKVv2RequestHelper&) =
delete;
~TrustedSignalsKVv2RequestHelper();
std::string TakePostRequestBody();
private:
std::string post_request_body_;
};
// A single-use class within `TrustedSignalsRequestManager` is designed to
// gather interest group names, bidding keys, render URLs, and ad component URLs
// for trusted bidding or scoring signals. It encodes this information into CBOR
// format as the POST request body. All data will be structured into a
// `TrustedSignalsKVv2RequestHelper`.
//
// TODO(crbug.com/337917489): Consider to add a cache for compression group id
// to handle missing compression group in response cases.
class CONTENT_EXPORT TrustedSignalsKVv2RequestHelperBuilder {
public:
TrustedSignalsKVv2RequestHelperBuilder& operator=(
const TrustedSignalsKVv2RequestHelperBuilder&);
TrustedSignalsKVv2RequestHelperBuilder(
const TrustedSignalsKVv2RequestHelperBuilder&);
virtual ~TrustedSignalsKVv2RequestHelperBuilder();
// Used in trusted signals requests to store the partition and compression
// group it belongs to, as partition IDs can be duplicated across multiple
// compression groups.
struct CONTENT_EXPORT IsolationIndex {
int compression_group_id;
int partition_id;
bool operator==(const IsolationIndex& other) const = default;
};
// Build the request helper using the helper builder to construct the POST
// body string, noting that the partition IDs will not be sequential.
virtual TrustedSignalsKVv2RequestHelper Build() = 0;
protected:
TrustedSignalsKVv2RequestHelperBuilder(
std::string hostname,
GURL trusted_signals_url,
std::optional<int> experiment_group_id);
// All the data needed to request a particular bidding or scoring signals
// partition.
struct Partition {
Partition();
// Create a new partition for bidding signals based on interest group's
// name, bidding keys, hostname, experiment group id and slot size
// parameter.
Partition(int partition_id,
const std::string& interest_group_name,
const std::set<std::string>& bidding_keys,
const std::string& hostname,
const std::optional<int>& experiment_group_id,
std::pair<std::string, std::string>
trusted_bidding_signals_slot_size_param);
Partition(Partition&&);
~Partition();
Partition& operator=(Partition&&);
int partition_id;
// Parameters for building a bidding signals URL.
std::set<std::string> interest_group_names;
std::set<std::string> bidding_signals_keys;
// Parameters for building a scoring signals URL.
std::set<std::string> render_urls;
std::set<std::string> ad_component_render_urls;
// Valid keys are "hostname", "experimentGroupId",
// "slotSize", and "allSlotsRequestedSizes".
base::Value::Dict additional_params;
};
// A map of partition IDs to partition to indicate a compression group.
using CompressionGroup = std::map<int, Partition>;
std::map<int, CompressionGroup>& compression_groups() {
return compression_groups_;
}
std::map<url::Origin, int>& join_origin_compression_id_map() {
return join_origin_compression_id_map_;
}
const std::string& hostname() const { return hostname_; }
const GURL& trusted_signals_url() const { return trusted_signals_url_; }
const std::optional<int>& experiment_group_id() const {
return experiment_group_id_;
}
// Return next compression group id and increase it by 1.
int next_compression_group_id() { return next_compression_group_id_++; }
private:
// Build a CBOR map for the partition with the provided data and IDs.
virtual cbor::Value::MapValue BuildMapForPartition(
const Partition& partition,
int partition_id,
int compression_group_id) = 0;
// Multiple partitions are keyed by compression group ID. For the Partition
// vector, always place interest groups with the execution mode
// group-by-origin in index-0 position, and then expand for other modes at
// the end.
std::map<int, CompressionGroup> compression_groups_;
// Joining origin to compression group id map
std::map<url::Origin, int> join_origin_compression_id_map_;
const std::string hostname_;
const GURL trusted_signals_url_;
const std::optional<int> experiment_group_id_;
// Initial id for compression groups.
int next_compression_group_id_ = 0;
};
class CONTENT_EXPORT TrustedBiddingSignalsKVv2RequestHelperBuilder
: public TrustedSignalsKVv2RequestHelperBuilder {
public:
TrustedBiddingSignalsKVv2RequestHelperBuilder(
const std::string& hostname,
const GURL& trusted_signals_url,
std::optional<int> experiment_group_id,
const std::string& trusted_bidding_signals_slot_size_param);
TrustedBiddingSignalsKVv2RequestHelperBuilder(
const TrustedBiddingSignalsKVv2RequestHelperBuilder&) = delete;
TrustedBiddingSignalsKVv2RequestHelperBuilder& operator=(
const TrustedBiddingSignalsKVv2RequestHelperBuilder&) = delete;
~TrustedBiddingSignalsKVv2RequestHelperBuilder() override;
// TODO(crbug.com/337917489): Consider a better way to handle identical
// trusted signals requests (e.g., with the same IG name and bidding keys).
// Duplicate requests should be merged with the existing ones, likely
// requiring a map to record the isolation index for IG names to avoid
// searching in partitions.
//
// Adds a request for the specified information to the trusted bidding signals
// helper builder. Returns the IsolationIndex indicating where the requested
// information can be found in the response to the fully assembled request
// once it becomes available.
IsolationIndex AddTrustedSignalsRequest(
base::optional_ref<const std::string> interest_group_name,
base::optional_ref<const std::set<std::string>> bidding_keys,
base::optional_ref<const url::Origin> interest_group_join_origin,
std::optional<blink::mojom::InterestGroup::ExecutionMode> execution_mode);
TrustedSignalsKVv2RequestHelper Build() override;
private:
cbor::Value::MapValue BuildMapForPartition(const Partition& partition,
int partition_id,
int compression_group_id) override;
// Using a pair to store key and value for a trusted bidding signals slot
// size parameter. Valid parameter key are "slotSize" or
// "allSlotsRequestedSizes"
std::pair<std::string, std::string> trusted_bidding_signals_slot_size_param_;
};
} // namespace auction_worklet
#endif // CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SIGNALS_KVV2_HELPER_H_

@ -0,0 +1,305 @@
// 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/services/auction_worklet/trusted_signals_kvv2_helper.h"
#if BUILDFLAG(IS_WIN)
#include <winsock2.h>
#else
#include <netinet/in.h>
#endif
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <optional>
#include "base/containers/span.h"
#include "base/containers/span_writer.h"
#include "base/debug/crash_logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "content/services/auction_worklet/trusted_signals_request_manager.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "v8-context.h"
namespace auction_worklet {
namespace {
const char kHostName[] = "publisher.test";
const int kExperimentGroupId = 12345;
const char kTrustedBiddingSignalsSlotSizeParam[] = "slotSize=100,200";
const char kTrustedSignalsUrl[] = "https://url.test/";
const char kOriginFooUrl[] = "https://foo.test/";
const char kOriginBarUrl[] = "https://bar.test/";
} // namespace
TEST(TrustedSignalsKVv2RequestHelperTest,
TrustedBiddingSignalsRequestEncoding) {
std::unique_ptr<TrustedBiddingSignalsKVv2RequestHelperBuilder>
helper_builder =
std::make_unique<TrustedBiddingSignalsKVv2RequestHelperBuilder>(
kHostName, GURL(kTrustedSignalsUrl), kExperimentGroupId,
kTrustedBiddingSignalsSlotSizeParam);
helper_builder->AddTrustedSignalsRequest(
std::string("groupA"), std::set<std::string>{"keyA", "keyAB"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode);
helper_builder->AddTrustedSignalsRequest(
std::string("groupB"), std::set<std::string>{"keyB", "keyAB"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode);
// Another group in kOriginFooUrl, but with execution mode kCompatibilityMode,
// for scenario of multiple partitions with different keys in one compression
// group.
helper_builder->AddTrustedSignalsRequest(
std::string("groupAB"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode);
helper_builder->AddTrustedSignalsRequest(
std::string("groupC"), std::set<std::string>{"keyC", "keyCD"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode);
helper_builder->AddTrustedSignalsRequest(
std::string("groupD"), std::set<std::string>{"keyD", "keyCD"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode);
// Test interest group name is merged into one partition with same joining
// origin and kGroupedByOriginMode.
helper_builder->AddTrustedSignalsRequest(
std::string("groupD"), std::set<std::string>{},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode);
// Test bidding keys are merged into one partition with same joining origin
// and kGroupedByOriginMode.
helper_builder->AddTrustedSignalsRequest(
std::string("groupD"), std::set<std::string>{"keyDD"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode);
TrustedSignalsKVv2RequestHelper helper = helper_builder->Build();
std::string post_body = helper.TakePostRequestBody();
std::vector<uint8_t> body_bytes(post_body.begin(), post_body.end());
// Use cbor.me to convert from
// {
// "partitions": [
// {
// "id": 0,
// "metadata": {
// "hostname": "publisher.test",
// "slotSize": "100,200",
// "experimentGroupId": "12345"
// },
// "arguments": [
// {
// "data": [
// "groupA",
// "groupB"
// ],
// "tags": [
// "interestGroupNames"
// ]
// },
// {
// "data": [
// "keyA",
// "keyAB",
// "keyB"
// ],
// "tags": [
// "keys"
// ]
// }
// ],
// "compressionGroupId": 0
// },
// {
// "id": 1,
// "metadata": {
// "hostname": "publisher.test",
// "slotSize": "100,200",
// "experimentGroupId": "12345"
// },
// "arguments": [
// {
// "data": [
// "groupAB"
// ],
// "tags": [
// "interestGroupNames"
// ]
// },
// {
// "data": [
// "key"
// ],
// "tags": [
// "keys"
// ]
// }
// ],
// "compressionGroupId": 0
// },
// {
// "id": 0,
// "metadata": {
// "hostname": "publisher.test",
// "slotSize": "100,200",
// "experimentGroupId": "12345"
// },
// "arguments": [
// {
// "data": [
// "groupC",
// "groupD"
// ],
// "tags": [
// "interestGroupNames"
// ]
// },
// {
// "data": [
// "keyC",
// "keyCD",
// "keyD",
// "keyDD"
// ],
// "tags": [
// "keys"
// ]
// }
// ],
// "compressionGroupId": 1
// }
// ],
// "acceptCompression": [
// "none",
// "gzip"
// ]
// }
const std::string kExpectedBodyHex =
"A26A706172746974696F6E7383A462696400686D65746164617461A368686F73746E616D"
"656E7075626C69736865722E7465737468736C6F7453697A65673130302C323030716578"
"706572696D656E7447726F7570496465313233343569617267756D656E747382A2646461"
"7461826667726F7570416667726F75704264746167738172696E74657265737447726F75"
"704E616D6573A2646461746183646B657941656B65794142646B65794264746167738164"
"6B65797372636F6D7072657373696F6E47726F7570496400A462696401686D6574616461"
"7461A368686F73746E616D656E7075626C69736865722E7465737468736C6F7453697A65"
"673130302C323030716578706572696D656E7447726F7570496465313233343569617267"
"756D656E747382A26464617461816767726F7570414264746167738172696E7465726573"
"7447726F75704E616D6573A2646461746181636B6579647461677381646B65797372636F"
"6D7072657373696F6E47726F7570496400A462696400686D65746164617461A368686F73"
"746E616D656E7075626C69736865722E7465737468736C6F7453697A65673130302C3230"
"30716578706572696D656E7447726F7570496465313233343569617267756D656E747382"
"A26464617461826667726F7570436667726F75704464746167738172696E746572657374"
"47726F75704E616D6573A2646461746184646B657943656B65794344646B657944656B65"
"794444647461677381646B65797372636F6D7072657373696F6E47726F75704964017161"
"6363657074436F6D7072657373696F6E82646E6F6E6564677A6970";
// Prefix hex for `kExpectedBodyHex` which includes the compression format
// code and the length.
const std::string kExpectedPrefixHex = "000000025B";
EXPECT_EQ(base::HexEncode(body_bytes), kExpectedPrefixHex + kExpectedBodyHex);
}
// TODO(crbug.com/337917489): When adding an identical IG, it should use the
// existing partition instead of creating a new one. After the implementation,
// the EXPECT_EQ() of second IG H should be failed.
TEST(TrustedSignalsKVv2RequestHelperTest, TrustedBiddingSignalsIsolationIndex) {
// Add the following interest groups:
// IG A[join_origin: foo.com, mode: group-by-origin]
// IG B[join_origin: foo.com, mode: group-by-origin]
// IG C[join_origin: foo.com, mode: compatibility]
// IG D[join_origin: foo.com, mode: compatibility]
// IG E[join_origin: bar.com, mode: compatibility]
// IG F[join_origin: bar.com, mode: group-by-origin]
// IG G[join_origin: bar.com, mode: compatibility]
// IG H[join_origin: bar.com, mode: compatibility]
// IG H, a dulplicate IG, aiming to test how request builder can handle a
// identical IG.
// will result the following groups: Compression: 0 -
// partition 0: A, B
// partition 1: C
// partition 2: D
// Compression: 1 -
// partition 0: F
// partition 1: E
// partition 2: G
// partition 3: H
// partition 4: H
std::unique_ptr<TrustedBiddingSignalsKVv2RequestHelperBuilder>
helper_builder =
std::make_unique<TrustedBiddingSignalsKVv2RequestHelperBuilder>(
kHostName, GURL(kTrustedSignalsUrl), kExperimentGroupId,
kTrustedBiddingSignalsSlotSizeParam);
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(0, 0),
helper_builder->AddTrustedSignalsRequest(
std::string("groupA"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(0, 0),
helper_builder->AddTrustedSignalsRequest(
std::string("groupB"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(0, 1),
helper_builder->AddTrustedSignalsRequest(
std::string("groupC"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(0, 2),
helper_builder->AddTrustedSignalsRequest(
std::string("groupD"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginFooUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(1, 1),
helper_builder->AddTrustedSignalsRequest(
std::string("groupE"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(1, 0),
helper_builder->AddTrustedSignalsRequest(
std::string("groupF"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(1, 2),
helper_builder->AddTrustedSignalsRequest(
std::string("groupG"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(1, 3),
helper_builder->AddTrustedSignalsRequest(
std::string("groupH"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode));
EXPECT_EQ(
TrustedSignalsKVv2RequestHelperBuilder::IsolationIndex(1, 4),
helper_builder->AddTrustedSignalsRequest(
std::string("groupH"), std::set<std::string>{"key"},
url::Origin::Create(GURL(kOriginBarUrl)),
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode));
}
} // namespace auction_worklet