| // Copyright 2023 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/additional_bids_util.h" |
| |
| #include <stdint.h> |
| |
| #include <array> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/containers/flat_set.h" |
| #include "base/feature_list.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/strcat.h" |
| #include "base/time/time.h" |
| #include "base/types/expected.h" |
| #include "base/types/optional_ref.h" |
| #include "base/uuid.h" |
| #include "base/values.h" |
| #include "content/browser/interest_group/auction_metrics_recorder.h" |
| #include "content/browser/interest_group/interest_group_auction.h" |
| #include "content/common/content_export.h" |
| #include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom-forward.h" |
| #include "crypto/sha2.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/interest_group/ad_auction_constants.h" |
| #include "third_party/blink/public/common/interest_group/ad_display_size.h" |
| #include "third_party/boringssl/src/include/openssl/curve25519.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Returns error string on failure. |
| template <size_t N> |
| std::optional<std::string> DecodeBase64Fixed(std::string_view field, |
| const std::string& in, |
| std::array<uint8_t, N>& out) { |
| std::string decoded; |
| if (!base::Base64Decode(in, &decoded, base::Base64DecodePolicy::kForgiving)) { |
| return base::StrCat({"Field '", field, "' is not valid base64."}); |
| } |
| if (decoded.size() != N) { |
| return base::StrCat({"Field '", field, "' has unexpected length."}); |
| } |
| std::copy(decoded.begin(), decoded.end(), out.data()); |
| |
| return std::nullopt; |
| } |
| |
| bool AdditionalBidKeyHasMatchingValidSignature( |
| const std::vector<SignedAdditionalBidSignature>& signatures, |
| const std::vector<size_t>& valid_signatures, |
| const blink::InterestGroup::AdditionalBidKey& key) { |
| for (size_t i : valid_signatures) { |
| CHECK_LT(i, signatures.size()); |
| if (signatures[i].key == key) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::string ComputeBidNonce(std::string_view auction_nonce_from_header, |
| std::string_view seller_nonce_from_header) { |
| return base::Base64Encode(crypto::SHA256HashString( |
| base::StrCat({auction_nonce_from_header, seller_nonce_from_header}))); |
| } |
| |
| } // namespace |
| |
| AdditionalBidDecodeResult::AdditionalBidDecodeResult() = default; |
| AdditionalBidDecodeResult::AdditionalBidDecodeResult( |
| AdditionalBidDecodeResult&& other) = default; |
| AdditionalBidDecodeResult::~AdditionalBidDecodeResult() = default; |
| |
| AdditionalBidDecodeResult& AdditionalBidDecodeResult::operator=( |
| AdditionalBidDecodeResult&&) = default; |
| |
| base::expected<AdditionalBidDecodeResult, std::string> DecodeAdditionalBid( |
| InterestGroupAuction* auction, |
| const base::Value& bid_in, |
| const base::Uuid& auction_nonce_from_header, |
| base::optional_ref<const std::string> seller_nonce_from_header, |
| const base::flat_set<url::Origin>& interest_group_buyers, |
| const url::Origin& seller, |
| base::optional_ref<const url::Origin> top_level_seller) { |
| const base::Value::Dict* result_dict = bid_in.GetIfDict(); |
| if (!result_dict) { |
| return base::unexpected( |
| base::StrCat({"Additional bid on auction with seller '", |
| seller.Serialize(), "' is not a dictionary."})); |
| } |
| |
| const std::string* auction_nonce_from_bid = |
| result_dict->FindString("auctionNonce"); |
| if (!base::FeatureList::IsEnabled(blink::features::kFledgeSellerNonce)) { |
| if (!auction_nonce_from_bid || |
| *auction_nonce_from_bid != |
| auction_nonce_from_header.AsLowercaseString()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing or incorrect nonce."})); |
| } |
| } else { |
| const std::string* bid_nonce_from_bid = result_dict->FindString("bidNonce"); |
| |
| if (!auction_nonce_from_bid && !bid_nonce_from_bid) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to no auctionNonce or bidNonce in bid -- exactly " |
| "one is required."})); |
| } |
| |
| if (auction_nonce_from_bid && bid_nonce_from_bid) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to both auctionNonce and bidNonce in bid -- exactly " |
| "one is required."})); |
| } |
| |
| if (seller_nonce_from_header) { |
| if (!bid_nonce_from_bid) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing bidNonce on a bid returned with a " |
| "seller nonce."})); |
| } |
| |
| const std::string bid_nonce_expected = |
| ComputeBidNonce(auction_nonce_from_header.AsLowercaseString(), |
| *seller_nonce_from_header); |
| if (*bid_nonce_from_bid != bid_nonce_expected) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to bidNonce from bid (", *bid_nonce_from_bid, |
| ") not matching its expectation (", bid_nonce_expected, |
| ") as calculated from the header auctionNonce (", |
| auction_nonce_from_header.AsLowercaseString(), |
| ") and sellerNonce (", *seller_nonce_from_header, ")."})); |
| } |
| } else { |
| if (!auction_nonce_from_bid) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing auctionNonce on a bid returned without " |
| "a seller nonce."})); |
| } |
| |
| if (*auction_nonce_from_bid != |
| auction_nonce_from_header.AsLowercaseString()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to auctionNonce from bid (", |
| *auction_nonce_from_bid, |
| ") not matching the header auctionNonce (", |
| auction_nonce_from_header.AsLowercaseString(), ")."})); |
| } |
| } |
| } |
| |
| const std::string* bid_seller = result_dict->FindString("seller"); |
| if (!bid_seller || url::Origin::Create(GURL(*bid_seller)) != seller) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing or incorrect seller."})); |
| } |
| |
| const std::string* bid_top_level_seller = |
| result_dict->FindString("topLevelSeller"); |
| if (top_level_seller.has_value()) { |
| // Component auction. |
| if (!bid_top_level_seller || |
| url::Origin::Create(GURL(*bid_top_level_seller)) != *top_level_seller) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing or incorrect topLevelSeller."})); |
| } |
| } else { |
| // Top-level or single-level auction. |
| if (bid_top_level_seller) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to specifying topLevelSeller in a non-component " |
| "auction."})); |
| } |
| } |
| |
| const std::string* ig_name = |
| result_dict->FindStringByDottedPath("interestGroup.name"); |
| if (!ig_name) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing interest group name."})); |
| } |
| |
| const std::string* ig_bidding_url_str = |
| result_dict->FindStringByDottedPath("interestGroup.biddingLogicURL"); |
| if (!ig_bidding_url_str) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing interest group bidding URL."})); |
| } |
| |
| GURL ig_bidding_url = GURL(*ig_bidding_url_str); |
| if (!ig_bidding_url.is_valid()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid interest group bidding URL."})); |
| } |
| |
| const std::string* ig_owner_string = |
| result_dict->FindStringByDottedPath("interestGroup.owner"); |
| if (!ig_owner_string) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing interest group owner."})); |
| } |
| GURL ig_owner_url(*ig_owner_string); |
| if (!ig_owner_url.is_valid()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid interest group owner URL."})); |
| } |
| if (!ig_owner_url.SchemeIs("https")) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to non-https interest group owner URL."})); |
| } |
| url::Origin ig_owner = url::Origin::Create(ig_owner_url); |
| |
| if (interest_group_buyers.find(ig_owner) == interest_group_buyers.end()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected because the additional bid's owner, '", |
| ig_owner.Serialize(), "', is not in interestGroupBuyers."})); |
| } |
| |
| if (!ig_owner.IsSameOriginWith(ig_bidding_url)) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid origin of biddingLogicURL."})); |
| } |
| |
| auto synth_interest_group = StorageInterestGroup(); |
| synth_interest_group.interest_group.name = *ig_name; |
| synth_interest_group.interest_group.owner = std::move(ig_owner); |
| synth_interest_group.interest_group.bidding_url = std::move(ig_bidding_url); |
| // Add ads. |
| const base::Value::Dict* bid_dict = result_dict->FindDict("bid"); |
| if (!bid_dict) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing bid info."})); |
| } |
| |
| const std::string* render_url_str = bid_dict->FindString("render"); |
| GURL render_url; |
| if (render_url_str) { |
| render_url = GURL(*render_url_str); |
| } |
| if (!render_url.is_valid()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing or invalid creative URL."})); |
| } |
| |
| // Create ad vector and its first entry. |
| synth_interest_group.interest_group.ads.emplace(); |
| synth_interest_group.interest_group.ads->emplace_back( |
| render_url, /*metadata=*/std::nullopt); |
| |
| std::optional<double> bid_val = bid_dict->FindDouble("bid"); |
| if (!bid_val || bid_val.value() <= 0) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing or invalid bid value."})); |
| } |
| |
| std::string ad_metadata = "null"; |
| const base::Value* ad_metadata_val = bid_dict->Find("ad"); |
| if (ad_metadata_val) { |
| std::optional<std::string> serialized_metadata = |
| base::WriteJson(*ad_metadata_val); |
| if (serialized_metadata) { |
| ad_metadata = std::move(serialized_metadata).value(); |
| } |
| } |
| |
| std::optional<blink::AdCurrency> bid_currency; |
| const base::Value* bid_currency_val = bid_dict->Find("bidCurrency"); |
| if (bid_currency_val) { |
| const std::string* bid_currency_str = bid_currency_val->GetIfString(); |
| if (!bid_currency_str || !blink::IsValidAdCurrencyCode(*bid_currency_str)) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid bidCurrency."})); |
| } else { |
| bid_currency = blink::AdCurrency::From(*bid_currency_str); |
| } |
| } |
| |
| std::optional<double> ad_cost; |
| const base::Value* ad_cost_val = bid_dict->Find("adCost"); |
| if (ad_cost_val) { |
| ad_cost = ad_cost_val->GetIfDouble(); |
| if (!ad_cost.has_value()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid adCost."})); |
| } |
| } |
| |
| // modelingSignals in generateBid() ignores out-of-range values, so this |
| // matches the behavior. |
| std::optional<double> modeling_signals; |
| const base::Value* modeling_signals_val = bid_dict->Find("modelingSignals"); |
| if (modeling_signals_val) { |
| std::optional<double> modeling_signals_in = |
| modeling_signals_val->GetIfDouble(); |
| if (!modeling_signals_in.has_value()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to non-numeric modelingSignals."})); |
| } |
| if (*modeling_signals_in >= 0 && *modeling_signals_in < 4096) { |
| modeling_signals = modeling_signals_in; |
| } |
| } |
| |
| std::optional<std::string> aggregate_win_signals; |
| const base::Value* aggregate_win_signals_val = |
| bid_dict->Find("aggregateWinSignals"); |
| if (aggregate_win_signals_val) { |
| std::optional<std::string> serialized_aggregate_win_signals = |
| base::WriteJson(*aggregate_win_signals_val); |
| if (serialized_aggregate_win_signals) { |
| aggregate_win_signals = |
| std::move(serialized_aggregate_win_signals).value(); |
| } else { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid aggregateWinSignals."})); |
| } |
| } |
| const base::Value* ad_components_val = bid_dict->Find("adComponents"); |
| std::vector<InterestGroupAuction::Bid::ComponentAdInfo> ad_components; |
| if (ad_components_val) { |
| const base::Value::List* ad_components_list = |
| ad_components_val->GetIfList(); |
| if (!ad_components_list) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid adComponents."})); |
| } |
| if (ad_components_list->size() > blink::MaxAdAuctionAdComponents()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to too many ad component URLs."})); |
| } |
| |
| synth_interest_group.interest_group.ad_components.emplace(); |
| for (const base::Value& ad_component : *ad_components_list) { |
| const std::string* ad_component_str = ad_component.GetIfString(); |
| GURL ad_component_url; |
| if (ad_component_str) { |
| ad_component_url = GURL(*ad_component_str); |
| } |
| if (!ad_component_url.is_valid()) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid entry in adComponents."})); |
| } |
| |
| synth_interest_group.interest_group.ad_components->emplace_back( |
| ad_component_url, /*metadata=*/std::nullopt); |
| |
| InterestGroupAuction::Bid::ComponentAdInfo component_ad_info; |
| // TODO(http://crbug.com/40275797): What's the story with dimensions? |
| component_ad_info.ad_descriptor = |
| blink::AdDescriptor(std::move(ad_component_url)); |
| // The pointer to InterestGroup::Ad will be filled in below once the IG |
| // is in its final place. |
| component_ad_info.ad = nullptr; |
| ad_components.push_back(std::move(component_ad_info)); |
| } |
| } |
| |
| AdditionalBidDecodeResult result; |
| |
| const base::Value* single_negative_ig = |
| result_dict->Find("negativeInterestGroup"); |
| const base::Value* multiple_negative_ig = |
| result_dict->Find("negativeInterestGroups"); |
| |
| if (single_negative_ig && multiple_negative_ig) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to specifying both 'negativeInterestGroup' and " |
| "'negativeInterestGroups'."})); |
| } |
| |
| if (single_negative_ig) { |
| const std::string* negative_ig_name = single_negative_ig->GetIfString(); |
| if (!negative_ig_name) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to non-string 'negativeInterestGroup'."})); |
| } |
| result.negative_target_interest_group_names.push_back(*negative_ig_name); |
| } |
| |
| if (multiple_negative_ig) { |
| const base::Value::Dict* multiple_negative_ig_dict = |
| multiple_negative_ig->GetIfDict(); |
| if (!multiple_negative_ig_dict) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to non-dictionary 'negativeInterestGroups'."})); |
| } |
| const std::string* joining_origin_str = |
| multiple_negative_ig_dict->FindString("joiningOrigin"); |
| GURL joining_origin_url; |
| if (joining_origin_str) { |
| joining_origin_url = GURL(*joining_origin_str); |
| } |
| if (!joining_origin_url.is_valid() || |
| !joining_origin_url.SchemeIs("https")) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to invalid or missing 'joiningOrigin'."})); |
| } |
| result.negative_target_joining_origin = |
| url::Origin::Create(joining_origin_url); |
| |
| const base::Value::List* interest_group_names = |
| multiple_negative_ig_dict->FindList("interestGroupNames"); |
| if (!interest_group_names) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to missing or invalid 'interestGroupNames' within " |
| "'negativeInterestGroups'."})); |
| } |
| for (const base::Value& negative_ig : *interest_group_names) { |
| const std::string* negative_ig_str = negative_ig.GetIfString(); |
| if (!negative_ig_str) { |
| return base::unexpected(base::StrCat( |
| {"Additional bid on auction with seller '", seller.Serialize(), |
| "' rejected due to non-string 'interestGroupNames' entry."})); |
| } |
| result.negative_target_interest_group_names.push_back(*negative_ig_str); |
| } |
| } |
| SingleStorageInterestGroup storage_interest_group( |
| std::move(synth_interest_group)); |
| result.bid_state = std::make_unique<InterestGroupAuction::BidState>( |
| std::move(storage_interest_group)); |
| result.bid_state->additional_bid_buyer = |
| result.bid_state->bidder->interest_group.owner; |
| result.bid_state->made_bid = true; |
| result.bid_state->BeginTracing(); |
| |
| const blink::InterestGroup::Ad* bid_ad = |
| &result.bid_state->bidder->interest_group.ads.value()[0]; |
| |
| for (size_t i = 0; i < ad_components.size(); ++i) { |
| ad_components[i].ad = |
| &result.bid_state->bidder->interest_group.ad_components.value()[i]; |
| } |
| |
| result.bid = std::make_unique<InterestGroupAuction::Bid>( |
| auction_worklet::mojom::BidRole::kBothKAnonModes, ad_metadata, *bid_val, |
| /*bid_currency=*/bid_currency, |
| /*ad_cost=*/ad_cost, |
| /*ad_descriptor=*/blink::AdDescriptor(GURL(bid_ad->render_url())), |
| /*ad_component_descriptors=*/std::move(ad_components), |
| /*modeling_signals=*/ |
| static_cast<std::optional<uint16_t>>(modeling_signals), |
| /*aggregate_wins_signals=*/std::move(aggregate_win_signals), |
| /*bid_duration=*/base::TimeDelta(), |
| /*bidding_signals_data_version=*/std::nullopt, bid_ad, |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt, |
| result.bid_state.get(), auction); |
| |
| // TODO(http://crbug.com/1464874): Do we need to fill in any k-anon info? |
| |
| return result; |
| } |
| |
| SignedAdditionalBid::SignedAdditionalBid() = default; |
| SignedAdditionalBid::SignedAdditionalBid(SignedAdditionalBid&& other) = default; |
| SignedAdditionalBid::~SignedAdditionalBid() = default; |
| |
| SignedAdditionalBid& SignedAdditionalBid::operator=(SignedAdditionalBid&&) = |
| default; |
| |
| std::vector<size_t> SignedAdditionalBid::VerifySignatures() { |
| std::vector<size_t> verified; |
| for (size_t i = 0; i < signatures.size(); ++i) { |
| if (ED25519_verify( |
| reinterpret_cast<const uint8_t*>(additional_bid_json.data()), |
| additional_bid_json.size(), signatures[i].signature.data(), |
| signatures[i].key.data())) { |
| verified.push_back(i); |
| } |
| } |
| return verified; |
| } |
| |
| base::expected<SignedAdditionalBid, std::string> DecodeSignedAdditionalBid( |
| base::Value signed_additional_bid_in) { |
| base::Value::Dict* in_dict = signed_additional_bid_in.GetIfDict(); |
| if (!in_dict) { |
| return base::unexpected("Signed additional bid not a dictionary."); |
| } |
| |
| SignedAdditionalBid result; |
| |
| std::string* bid_json = in_dict->FindString("bid"); |
| if (!bid_json) { |
| return base::unexpected( |
| "Signed additional bid missing string 'bid' field."); |
| } |
| result.additional_bid_json = std::move(*bid_json); |
| |
| const base::Value::List* signature_list = in_dict->FindList("signatures"); |
| if (!signature_list) { |
| return base::unexpected( |
| "Signed additional bid missing list 'signatures' field."); |
| } |
| |
| for (const base::Value& sig_entry : *signature_list) { |
| SignedAdditionalBidSignature decoded_signature; |
| |
| const base::Value::Dict* sig_entry_dict = sig_entry.GetIfDict(); |
| if (!sig_entry_dict) { |
| return base::unexpected( |
| "Signed additional bid 'signatures' list entry not a dictionary."); |
| } |
| const std::string* key = sig_entry_dict->FindString("key"); |
| if (!key) { |
| return base::unexpected( |
| "Signed additional bid 'signatures' list entry missing 'key' " |
| "string."); |
| } |
| |
| std::optional<std::string> maybe_key_error = |
| DecodeBase64Fixed("key", *key, decoded_signature.key); |
| if (maybe_key_error.has_value()) { |
| return base::unexpected(maybe_key_error.value()); |
| } |
| |
| const std::string* signature = sig_entry_dict->FindString("signature"); |
| if (!signature) { |
| return base::unexpected( |
| "Signed additional bid 'signatures' list entry missing 'signature' " |
| "string."); |
| } |
| |
| std::optional<std::string> maybe_signature_error = |
| DecodeBase64Fixed("signature", *signature, decoded_signature.signature); |
| if (maybe_signature_error.has_value()) { |
| return base::unexpected(maybe_signature_error.value()); |
| } |
| result.signatures.push_back(std::move(decoded_signature)); |
| } |
| |
| return result; |
| } |
| |
| AdAuctionNegativeTargeter::AdAuctionNegativeTargeter() = default; |
| AdAuctionNegativeTargeter::~AdAuctionNegativeTargeter() = default; |
| |
| void AdAuctionNegativeTargeter::AddInterestGroupInfo( |
| const url::Origin& buyer, |
| const std::string& name, |
| const url::Origin& joining_origin, |
| const blink::InterestGroup::AdditionalBidKey& key) { |
| // Should not have any duplicates since (buyer, name) ought to be the DB |
| // primary key. |
| DCHECK(!negative_interest_groups_.contains(std::make_pair(buyer, name))); |
| auto& spot = negative_interest_groups_[std::make_pair(buyer, name)]; |
| spot.joining_origin = joining_origin; |
| spot.key = key; |
| } |
| |
| size_t AdAuctionNegativeTargeter::GetNumNegativeInterestGroups() { |
| return negative_interest_groups_.size(); |
| } |
| |
| bool AdAuctionNegativeTargeter::ShouldDropDueToNegativeTargeting( |
| const url::Origin& buyer, |
| const std::optional<url::Origin>& negative_target_joining_origin, |
| const std::vector<std::string>& negative_target_interest_group_names, |
| const std::vector<SignedAdditionalBidSignature>& signatures, |
| const std::vector<size_t>& valid_signatures, |
| const url::Origin& seller, |
| AuctionMetricsRecorder& auction_metrics_recorder, |
| std::vector<std::string>& errors_out) { |
| if (valid_signatures.size() != signatures.size()) { |
| errors_out.push_back( |
| base::StrCat({"Warning: Some signatures on an additional bid from '", |
| buyer.Serialize(), "' on auction with seller '", |
| seller.Serialize(), "' failed to verify."})); |
| } |
| |
| for (const std::string& ig_name : negative_target_interest_group_names) { |
| auto negative_info_it = |
| negative_interest_groups_.find(std::make_pair(buyer, ig_name)); |
| |
| // Negative group not there, no reason to reject thus far. |
| if (negative_info_it == negative_interest_groups_.end()) { |
| continue; |
| } |
| |
| const NegativeInfo& negative_info = negative_info_it->second; |
| |
| // Negative group there, but we may need to ignore it if it doesn't have |
| // a matching signature. |
| if (!AdditionalBidKeyHasMatchingValidSignature(signatures, valid_signatures, |
| negative_info.key)) { |
| auction_metrics_recorder |
| .RecordNegativeInterestGroupIgnoredDueToInvalidSignature(); |
| errors_out.push_back(base::StrCat( |
| {"Warning: Ignoring negative targeting group '", ig_name, |
| "' on an additional bid from '", buyer.Serialize(), |
| "' on auction with seller '", seller.Serialize(), |
| "' since its key does not correspond to a valid signature."})); |
| continue; |
| } |
| |
| // Must also have proper joining origin, if applicable. |
| if (negative_target_joining_origin.has_value()) { |
| if (*negative_target_joining_origin != negative_info.joining_origin) { |
| auction_metrics_recorder |
| .RecordNegativeInterestGroupIgnoredDueToJoiningOriginMismatch(); |
| errors_out.push_back(base::StrCat( |
| {"Warning: Ignoring negative targeting group '", ig_name, |
| "' on an additional bid from '", buyer.Serialize(), |
| "' on auction with seller '", seller.Serialize(), |
| "' since it does not have the expected joining origin."})); |
| continue; |
| } |
| } |
| |
| // Found a negative group that meets all requirements. |
| return true; |
| } |
| |
| // No validated negative groups found. |
| return false; |
| } |
| |
| AdAuctionNegativeTargeter::NegativeInfo::NegativeInfo() = default; |
| AdAuctionNegativeTargeter::NegativeInfo::~NegativeInfo() = default; |
| |
| } // namespace content |