| // 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. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/342213636): Remove this and spanify to fix the errors. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include "content/browser/interest_group/additional_bids_util.h" |
| |
| #include <stdint.h> |
| |
| #include <array> |
| #include <limits> |
| #include <optional> |
| #include <string> |
| |
| #include "base/base64.h" |
| #include "base/containers/flat_set.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/task_environment.h" |
| #include "base/types/expected.h" |
| #include "base/types/optional_ref.h" |
| #include "base/uuid.h" |
| #include "base/values.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/interest_group/auction_metrics_recorder.h" |
| #include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom-forward.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "testing/gmock/include/gmock/gmock-matchers.h" |
| #include "testing/gtest/include/gtest/gtest.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/gurl.h" |
| #include "url/origin.h" |
| |
| using testing::UnorderedElementsAre; |
| |
| namespace content { |
| namespace { |
| |
| // In case one wants to generate some keys for test use, the following may be |
| // useful: |
| // template <int N> |
| // std::string SerializeKey(uint8_t key[N]) { |
| // std::string out; |
| // for (int row = 0; row < (N / 8); ++row) { |
| // for (int col = 0; col < 8; ++col) { |
| // base::StrAppend( |
| // &out, {base::StringPrintf("0x%02x", |
| // static_cast<unsigned>( |
| // key[row * 8 + col])), |
| // ", "}); |
| // } |
| // base::StrAppend(&out, {"\n"}); |
| // } |
| // return out; |
| // } |
| // |
| // TEST_F(AdditionalBidsUtilTest, GenerateKeyPair) { |
| // uint8_t public_key[32]; |
| // uint8_t private_key[64]; |
| // ED25519_keypair(public_key, private_key); |
| // std::cout << "public_key:\n"; |
| // std::cout << SerializeKey<32>(public_key) << "\n"; |
| // std::cout << base::Base64Encode( |
| // base::make_span(public_key, sizeof(public_key))); |
| // std::cout << "\n\n"; |
| // |
| // std::cout << "private_key:\n"; |
| // std::cout << SerializeKey<64>(private_key) << "\n"; |
| // std::cout << base::Base64Encode( |
| // base::make_span(private_key, sizeof(private_key))); |
| // std::cout << "\n\n"; |
| // } |
| |
| // Some test data for key/signature fields. These are just random sequences |
| // of bytes of the right length, not proper cryptographic ones. |
| const uint8_t kKey1[] = |
| "\xF5\x30\x88\xE9\x9B\xC7\xB0\x2A\x8C\xBE\x11\x8D\xD3\xEC\xEF\xEB\xB5\x71" |
| "\xDF\xF9\x7D\x67\xEF\xFF\x9A\xAD\xE1\x63\x86\xAD\x57\x5E"; |
| const char kKey1Base64[] = "9TCI6ZvHsCqMvhGN0+zv67Vx3/l9Z+//mq3hY4atV14="; |
| |
| const uint8_t kKey2[] = |
| "\x79\x34\x0E\x99\xF6\x02\x98\xB2\xF6\x82\xAA\xDA\x3C\x95\xFA\x62\x3A\xF2" |
| "\x53\xA8\x56\xEB\x21\xC4\xC2\x67\x6C\x5D\xE3\x4B\xDA\xA0"; |
| const char kKey2Base64[] = "eTQOmfYCmLL2gqraPJX6YjryU6hW6yHEwmdsXeNL2qA="; |
| |
| const uint8_t kSig1[] = |
| "\x49\xD1\x27\x01\x29\x9E\xC8\x34\xE3\x12\x46\xA0\xFA\x17\x33\x1E\xD2\x7B" |
| "\xC0\x63\x7D\x7F\x63\xF6\x12\x49\x39\x40\x80\x2F\x31\x93\x99\xD7\x93\x16" |
| "\x58\x4D\x3B\xEC\x0F\x46\x07\x29\xE4\xE6\x13\x0D\xD7\xEA\x6D\x35\x60\xB8" |
| "\x27\x9E\x86\xC7\xE0\x10\x63\xEA\x44\xE6"; |
| const char kSig1Base64[] = |
| "SdEnASmeyDTjEkag+hczHtJ7wGN9f2P2Ekk5QIAvMZOZ15MWWE077A9GBynk5hMN1+" |
| "ptNWC4J56Gx+AQY+pE5g=="; |
| |
| const uint8_t kSig2[] = |
| "\x91\x2C\xF4\x82\x8F\x62\x6B\x1F\x4A\x34\x1B\x8C\x4C\xB8\xD6\xA1\x41\xD0" |
| "\xBD\xCC\x67\xBA\xCF\x08\xE4\x32\x09\x5D\x97\x06\x09\x41\xFA\xEA\x12\x8E" |
| "\x49\x05\x73\xE2\xA4\x57\x7B\xA5\x3B\x00\xAE\x23\xAF\x61\xE9\x5F\xA4\x39" |
| "\xBD\x07\x9B\xB7\x49\x31\x52\xDD\x69\xDD"; |
| const char kSig2Base64[] = |
| "kSz0go9iax9KNBuMTLjWoUHQvcxnus8I5DIJXZcGCUH66hKOSQVz4qRXe6U7AK4jr2HpX6Q5vQ" |
| "ebt0kxUt1p3Q=="; |
| |
| // Version that requires forgiving decoder to accept. |
| const char kSig2Base64Sloppy[] = |
| " kSz0go9iax9KNBuMTLjWoUHQvcxnus8I5DIJXZcGCUH66hKOSQVz4qRXe6U7AK4jr2HpX6Q5v" |
| "Qebt0kxUt1p3Q"; |
| |
| const char kPretendBid[] = "Hi, I am a JSON bid."; |
| |
| class AdditionalBidsUtilTest : public testing::Test { |
| protected: |
| base::Value::Dict MakeMinimalValid() { |
| base::Value::Dict ig_dict; |
| ig_dict.Set("name", "trainfans"); |
| ig_dict.Set("biddingLogicURL", "https://rollingstock.test/logic.js"); |
| ig_dict.Set("owner", "https://rollingstock.test/"); |
| |
| base::Value::Dict bid_dict; |
| bid_dict.Set("bid", 10.0); |
| bid_dict.Set("render", "https://en.wikipedia.test/wiki/Train"); |
| |
| base::Value::Dict additional_bid_dict; |
| additional_bid_dict.Set("auctionNonce", kAuctionNonce.AsLowercaseString()); |
| additional_bid_dict.Set("seller", "https://seller.test"); |
| additional_bid_dict.Set("topLevelSeller", "https://top-organizer.test"); |
| additional_bid_dict.Set("interestGroup", std::move(ig_dict)); |
| additional_bid_dict.Set("bid", std::move(bid_dict)); |
| return additional_bid_dict; |
| } |
| |
| base::Value::Dict MakeValidWithMultipleNegativeIGs() { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value::Dict negative_igs_dict; |
| negative_igs_dict.Set("joiningOrigin", "https://depot.test"); |
| base::Value::List negative_ig_names_list; |
| negative_ig_names_list.Append("negative_group"); |
| negative_ig_names_list.Append("another negative group"); |
| negative_igs_dict.Set("interestGroupNames", |
| std::move(negative_ig_names_list)); |
| additional_bid_dict.Set("negativeInterestGroups", |
| std::move(negative_igs_dict)); |
| return additional_bid_dict; |
| } |
| |
| static base::Value::Dict MakeValidSignedBid() { |
| base::Value::Dict signed_dict; |
| signed_dict.Set("bid", kPretendBid); |
| |
| base::Value::List sigs_list; |
| base::Value::Dict sig1; |
| sig1.Set("key", kKey1Base64); |
| sig1.Set("signature", kSig1Base64); |
| sigs_list.Append(std::move(sig1)); |
| |
| base::Value::Dict sig2; |
| sig2.Set("key", kKey2Base64); |
| sig2.Set("signature", kSig2Base64); |
| sigs_list.Append(std::move(sig2)); |
| |
| signed_dict.Set("signatures", std::move(sigs_list)); |
| return signed_dict; |
| } |
| |
| blink::InterestGroup::AdditionalBidKey KeyFromLiteral( |
| const uint8_t* literal) { |
| blink::InterestGroup::AdditionalBidKey key; |
| memcpy(key.data(), literal, key.size()); |
| return key; |
| } |
| |
| // Fills in the key only, we don't actually need the signature part any more. |
| SignedAdditionalBidSignature SignatureWithLiteralKey(const uint8_t* literal) { |
| SignedAdditionalBidSignature result; |
| result.key = KeyFromLiteral(literal); |
| return result; |
| } |
| |
| const base::Uuid kAuctionNonce{base::Uuid::GenerateRandomV4()}; |
| const base::flat_set<url::Origin> kInterestGroupBuyers{ |
| url::Origin::Create(GURL("https://buyer.test")), |
| url::Origin::Create(GURL("https://rollingstock.test")), |
| url::Origin::Create(GURL("https://trainstuff.test"))}; |
| const url::Origin kSeller = url::Origin::Create(GURL("https://seller.test")); |
| const url::Origin kTopSeller = |
| url::Origin::Create(GURL("https://top-organizer.test")); |
| }; |
| |
| TEST_F(AdditionalBidsUtilTest, FailNotDict) { |
| base::Value input(5); |
| |
| auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce, |
| kInterestGroupBuyers, kSeller, |
| /*top_level_seller=*/std::nullopt); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' is not a " |
| "dictionary.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailNoNonce) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Remove("auctionNonce"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce, |
| kInterestGroupBuyers, kSeller, |
| /*top_level_seller=*/std::nullopt); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or incorrect nonce.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailInvalidNonce) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Set("auctionNonce", "not-a-nonce"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce, |
| kInterestGroupBuyers, kSeller, |
| /*top_level_seller=*/std::nullopt); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or incorrect nonce.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingSeller) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Remove("seller"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce, |
| kInterestGroupBuyers, kSeller, |
| /*top_level_seller=*/std::nullopt); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or incorrect seller.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailInvalidSeller) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Set("seller", "http://notseller.test"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce, |
| kInterestGroupBuyers, kSeller, |
| /*top_level_seller=*/std::nullopt); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or incorrect seller.", |
| result.error()); |
| } |
| |
| // Specifying topLevelSeller in a bid in a non-component auction is a problem. |
| TEST_F(AdditionalBidsUtilTest, FailInvalidTopLevelSeller) { |
| base::Value input(MakeMinimalValid()); |
| auto result = DecodeAdditionalBid(/*auction=*/nullptr, input, kAuctionNonce, |
| kInterestGroupBuyers, kSeller, |
| /*top_level_seller=*/std::nullopt); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to specifying topLevelSeller in a non-component auction.", |
| result.error()); |
| } |
| |
| // Not specifying topLevelSeller in component auction bid is also a problem. |
| TEST_F(AdditionalBidsUtilTest, FailInvalidTopLevelSeller2) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Remove("topLevelSeller"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or incorrect topLevelSeller.", |
| result.error()); |
| } |
| |
| // An incorrect topLevelSeller in a bid in a component auction is also a |
| // problem. |
| TEST_F(AdditionalBidsUtilTest, FailInvalidTopLevelSeller3) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Set("topLevelSeller", "https://wrong-organizer.test"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or incorrect topLevelSeller.", |
| result.error()); |
| } |
| |
| // Missing IG dictionary. |
| TEST_F(AdditionalBidsUtilTest, FailNoIGDictionary) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Remove("interestGroup"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing interest group name.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingInterestGroupName) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.RemoveByDottedPath("interestGroup.name"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing interest group name.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingInterestGroupBiddingScript) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.RemoveByDottedPath("interestGroup.biddingLogicURL"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing interest group bidding URL.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingInterestGroupOwner) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.RemoveByDottedPath("interestGroup.owner"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing interest group owner.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailNonHttpsInterestGroupOwner) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("interestGroup.owner", |
| "http://rollingstock.test/"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to non-https interest group owner URL.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailDomainMismatchBetweenOwnerAndBiddingScript) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("interestGroup.owner", |
| "https://trainstuff.test/"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid origin of biddingLogicURL.", |
| result.error()); |
| } |
| |
| // The additional bid owner is missing from interestGroupBuyers. |
| TEST_F(AdditionalBidsUtilTest, AdditionalBidOwnerNotInInterestGroupBuyers) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| const base::flat_set<url::Origin> wrong_interest_group_buyers{ |
| url::Origin::Create(GURL("https://wrongbuyer.test"))}; |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, wrong_interest_group_buyers, |
| kSeller, base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "because the additional bid's owner, 'https://rollingstock.test', " |
| "is not in interestGroupBuyers.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingBid) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Remove("bid"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing bid info.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingBidCreative) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.RemoveByDottedPath("bid.render"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or invalid creative URL.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailMissingBidValue) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.RemoveByDottedPath("bid.bid"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or invalid bid value.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, FailInvalidBidValue) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.bid", 0.0); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or invalid bid value.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, MinimalValid) { |
| base::Value input(MakeMinimalValid()); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid_state); |
| ASSERT_TRUE(result->bid); |
| const InterestGroupAuction::BidState* bid_state = result->bid_state.get(); |
| const InterestGroupAuction::Bid* bid = result->bid.get(); |
| |
| EXPECT_TRUE(bid_state->made_bid); |
| EXPECT_EQ("trainfans", bid_state->bidder->interest_group.name); |
| ASSERT_TRUE(bid_state->additional_bid_buyer.has_value()); |
| EXPECT_EQ(bid_state->bidder->interest_group.owner, |
| bid_state->additional_bid_buyer); |
| EXPECT_EQ("https://rollingstock.test", |
| bid_state->bidder->interest_group.owner.Serialize()); |
| ASSERT_TRUE(bid_state->bidder->interest_group.bidding_url.has_value()); |
| EXPECT_EQ("https://rollingstock.test/logic.js", |
| bid_state->bidder->interest_group.bidding_url->spec()); |
| |
| ASSERT_TRUE(bid_state->bidder->interest_group.ads.has_value()); |
| ASSERT_EQ(1u, bid_state->bidder->interest_group.ads->size()); |
| EXPECT_EQ("https://en.wikipedia.test/wiki/Train", |
| bid_state->bidder->interest_group.ads.value()[0].render_url()); |
| |
| EXPECT_EQ(auction_worklet::mojom::BidRole::kBothKAnonModes, bid->bid_role); |
| EXPECT_EQ("null", bid->ad_metadata); |
| EXPECT_EQ(10.0, bid->bid); |
| EXPECT_EQ(std::nullopt, bid->bid_currency); |
| EXPECT_EQ(std::nullopt, bid->ad_cost); |
| EXPECT_EQ(blink::AdDescriptor(GURL("https://en.wikipedia.test/wiki/Train")), |
| bid->ad_descriptor); |
| EXPECT_EQ(0u, bid->ad_component_descriptors.size()); |
| EXPECT_EQ(std::nullopt, bid->modeling_signals); |
| EXPECT_EQ(&bid_state->bidder->interest_group, bid->interest_group); |
| EXPECT_EQ(&bid_state->bidder->interest_group.ads.value()[0], bid->bid_ad); |
| EXPECT_EQ(bid_state, bid->bid_state); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidBidCurrencyType) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.bidCurrency", 5); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid bidCurrency.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidBidCurrencySyntax) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.bidCurrency", "Dollars"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid bidCurrency.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidBidCurrency) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.bidCurrency", "USD"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_TRUE(result.has_value()); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid->bid_currency); |
| EXPECT_EQ("USD", result->bid->bid_currency->currency_code()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidAdCost) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.adCost", "big"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid adCost.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidAdCost) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.adCost", 15.5); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid->ad_cost); |
| EXPECT_EQ(15.5, *result->bid->ad_cost); |
| } |
| |
| // We have a tradition of ignoring modeling signals if they're out of range, |
| // so this follows. |
| TEST_F(AdditionalBidsUtilTest, InvalidModelingSignals) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.modelingSignals", 4096); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| EXPECT_FALSE(result->bid->modeling_signals); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidModelingSignals2) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.modelingSignals", -0.001); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| EXPECT_FALSE(result->bid->modeling_signals); |
| } |
| |
| // Bad-type modeling signals still an error, however. |
| TEST_F(AdditionalBidsUtilTest, BadTypeModelingSignals) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.modelingSignals", "string"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to non-numeric modelingSignals.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidModelingSignals) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.modelingSignals", 0); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid->modeling_signals); |
| EXPECT_EQ(*result->bid->modeling_signals, 0); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidModelingSignals2) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.modelingSignals", 2.5); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid->modeling_signals); |
| EXPECT_EQ(*result->bid->modeling_signals, 2); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidModelingSignals3) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.modelingSignals", 4095.5); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid->modeling_signals); |
| EXPECT_EQ(*result->bid->modeling_signals, 4095); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidAdComponents) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.SetByDottedPath("bid.adComponents", "oops"); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid adComponents.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidAdComponentsEntry) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value::List ad_components_list; |
| ad_components_list.Append(10); |
| additional_bid_dict.SetByDottedPath("bid.adComponents", |
| std::move(ad_components_list)); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid entry in adComponents.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, TooManyAdComponents) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value::List ad_components_list; |
| const size_t kMaxAdAuctionAdComponents = blink::MaxAdAuctionAdComponents(); |
| for (size_t i = 0; i < kMaxAdAuctionAdComponents + 1; ++i) { |
| ad_components_list.Append("https://en.wikipedia.test/wiki/Locomotive"); |
| } |
| additional_bid_dict.SetByDottedPath("bid.adComponents", |
| std::move(ad_components_list)); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to too many ad component URLs.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidAdComponents) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value::List ad_components_list; |
| ad_components_list.Append("https://en.wikipedia.test/wiki/Locomotive"); |
| ad_components_list.Append("https://en.wikipedia.test/wiki/High-speed_rail"); |
| additional_bid_dict.SetByDottedPath("bid.adComponents", |
| std::move(ad_components_list)); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid_state); |
| |
| // Components should be both in the ad and the synthesized IG. |
| ASSERT_EQ(2u, result->bid->ad_component_descriptors.size()); |
| EXPECT_EQ( |
| blink::AdDescriptor(GURL("https://en.wikipedia.test/wiki/Locomotive")), |
| result->bid->ad_component_descriptors[0]); |
| EXPECT_EQ(blink::AdDescriptor( |
| GURL("https://en.wikipedia.test/wiki/High-speed_rail")), |
| result->bid->ad_component_descriptors[1]); |
| |
| ASSERT_TRUE( |
| result->bid_state->bidder->interest_group.ad_components.has_value()); |
| ASSERT_EQ(2u, |
| result->bid_state->bidder->interest_group.ad_components->size()); |
| EXPECT_EQ("https://en.wikipedia.test/wiki/Locomotive", |
| result->bid_state->bidder->interest_group.ad_components.value()[0] |
| .render_url()); |
| EXPECT_EQ("https://en.wikipedia.test/wiki/High-speed_rail", |
| result->bid_state->bidder->interest_group.ad_components.value()[1] |
| .render_url()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidAdComponentsEmpty) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value::List ad_components_list; |
| additional_bid_dict.SetByDottedPath("bid.adComponents", |
| std::move(ad_components_list)); |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->bid); |
| ASSERT_TRUE(result->bid_state); |
| |
| EXPECT_EQ(0u, result->bid->ad_component_descriptors.size()); |
| ASSERT_TRUE( |
| result->bid_state->bidder->interest_group.ad_components.has_value()); |
| EXPECT_EQ(0u, |
| result->bid_state->bidder->interest_group.ad_components->size()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidAdMetadata) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| base::Value::Dict metadata_dict; |
| metadata_dict.Set("a", "hello"); |
| metadata_dict.Set("b", 1.0); |
| additional_bid_dict.SetByDottedPath("bid.ad", std::move(metadata_dict)); |
| |
| base::Value input(std::move(additional_bid_dict)); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, input, kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| EXPECT_TRUE(result->bid); |
| EXPECT_EQ(R"({"a":"hello","b":1.0})", result->bid->ad_metadata); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidSingleNegativeIG) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Set("negativeInterestGroup", "not_if_here"); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| EXPECT_FALSE(result->negative_target_joining_origin.has_value()); |
| ASSERT_EQ(1u, result->negative_target_interest_group_names.size()); |
| EXPECT_EQ("not_if_here", result->negative_target_interest_group_names[0]); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidSingleNegativeIG) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Set("negativeInterestGroup", false); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to non-string 'negativeInterestGroup'.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, InvalidBothKindsOfNegativeIG) { |
| base::Value::Dict additional_bid_dict = MakeMinimalValid(); |
| additional_bid_dict.Set("negativeInterestGroup", "not_if_here"); |
| additional_bid_dict.Set("negativeInterestGroups", "boo"); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to specifying both 'negativeInterestGroup' and " |
| "'negativeInterestGroups'.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, ValidMultipleNegativeIG) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| ASSERT_TRUE(result->negative_target_joining_origin.has_value()); |
| EXPECT_EQ("https://depot.test", |
| result->negative_target_joining_origin->Serialize()); |
| ASSERT_EQ(2u, result->negative_target_interest_group_names.size()); |
| EXPECT_EQ("negative_group", result->negative_target_interest_group_names[0]); |
| EXPECT_EQ("another negative group", |
| result->negative_target_interest_group_names[1]); |
| } |
| |
| // Non-string joining origin. |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict.SetByDottedPath("negativeInterestGroups.joiningOrigin", |
| 10); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid or missing 'joiningOrigin'.", |
| result.error()); |
| } |
| |
| // Non-HTTPS joining origin. |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG2) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict.SetByDottedPath("negativeInterestGroups.joiningOrigin", |
| "http://example.org"); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid or missing 'joiningOrigin'.", |
| result.error()); |
| } |
| |
| // Missing joining origin. |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG3) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict.RemoveByDottedPath( |
| "negativeInterestGroups.joiningOrigin"); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to invalid or missing 'joiningOrigin'.", |
| result.error()); |
| } |
| |
| // Missing interestGroupNames. |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG4) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict.RemoveByDottedPath( |
| "negativeInterestGroups.interestGroupNames"); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or invalid 'interestGroupNames' within " |
| "'negativeInterestGroups'.", |
| result.error()); |
| } |
| |
| // interestGroupNames not a list. |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG5) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict.SetByDottedPath( |
| "negativeInterestGroups.interestGroupNames", "hi"); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to missing or invalid 'interestGroupNames' within " |
| "'negativeInterestGroups'.", |
| result.error()); |
| } |
| |
| // Non-string entry in interestGroupNames |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG6) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict |
| .FindListByDottedPath("negativeInterestGroups.interestGroupNames") |
| ->Append(50); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to non-string 'interestGroupNames' entry.", |
| result.error()); |
| } |
| |
| // Wrong type for negativeInterestGroups |
| TEST_F(AdditionalBidsUtilTest, InvalidMultipleNegativeIG7) { |
| base::Value::Dict additional_bid_dict = MakeValidWithMultipleNegativeIGs(); |
| additional_bid_dict.Set("negativeInterestGroups", "boo"); |
| auto result = DecodeAdditionalBid( |
| /*auction=*/nullptr, base::Value(std::move(additional_bid_dict)), |
| kAuctionNonce, kInterestGroupBuyers, kSeller, |
| base::optional_ref<const url::Origin>(kTopSeller)); |
| EXPECT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Additional bid on auction with seller 'https://seller.test' rejected " |
| "due to non-dictionary 'negativeInterestGroups'.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeBasicSignedBid) { |
| for (bool require_forgiving_base64 : {false, true}) { |
| SCOPED_TRACE(require_forgiving_base64); |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| if (require_forgiving_base64) { |
| *((*signed_bid_dict.FindList("signatures"))[1].GetDict().FindString( |
| "signature")) = kSig2Base64Sloppy; |
| } |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_TRUE(result.has_value()) << result.error(); |
| EXPECT_EQ(kPretendBid, result->additional_bid_json); |
| ASSERT_EQ(2u, result->signatures.size()); |
| ASSERT_EQ(sizeof(kKey1) - 1, result->signatures[0].key.size()); |
| ASSERT_EQ(sizeof(kSig1) - 1, result->signatures[0].signature.size()); |
| EXPECT_EQ( |
| 0, memcmp(kKey1, result->signatures[0].key.data(), sizeof(kKey1) - 1)); |
| EXPECT_EQ(0, memcmp(kSig1, result->signatures[0].signature.data(), |
| sizeof(kSig1) - 1)); |
| |
| ASSERT_EQ(sizeof(kKey2) - 1, result->signatures[1].key.size()); |
| ASSERT_EQ(sizeof(kSig2) - 1, result->signatures[1].signature.size()); |
| EXPECT_EQ( |
| 0, memcmp(kKey2, result->signatures[1].key.data(), sizeof(kKey2) - 1)); |
| EXPECT_EQ(0, memcmp(kSig2, result->signatures[1].signature.data(), |
| sizeof(kSig2) - 1)); |
| } |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, SignedNotDict) { |
| auto result = DecodeSignedAdditionalBid(base::Value(10)); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Signed additional bid not a dictionary.", result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingBid) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| signed_bid_dict.Remove("bid"); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Signed additional bid missing string 'bid' field.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingSignatures) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| signed_bid_dict.Remove("signatures"); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Signed additional bid missing list 'signatures' field.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatures) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| signed_bid_dict.FindList("signatures")->Append(40); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Signed additional bid 'signatures' list entry not a dictionary.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingSignatureKey) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| (*signed_bid_dict.FindList("signatures"))[0].GetDict().Remove("key"); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Signed additional bid 'signatures' list entry missing 'key' string.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureKey) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| (*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("key", "$$$"); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Field 'key' is not valid base64.", result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureKeyLength) { |
| const char kLength31[] = "r7J39NbxqA5AvGD57ENOYdOvxzHPwA6KoehNIFCjDw=="; |
| |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| (*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("key", kLength31); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Field 'key' has unexpected length.", result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedMissingSignatureSig) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| (*signed_bid_dict.FindList("signatures"))[0].GetDict().Remove("signature"); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| "Signed additional bid 'signatures' list entry missing 'signature' " |
| "string.", |
| result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureSig) { |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| (*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("signature", |
| "$$$"); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Field 'signature' is not valid base64.", result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, DecodeSignedInvalidSignatureSigLength) { |
| const char kLength65[] = |
| "rq9Nm5seElZB7vH9u8o6Cjt4v72LkPKGVKVl6k4uOlmV8Y7n023fmOk47R2bPRNYx/" |
| "EzpBSXdJainpItZwK5DTI="; |
| |
| base::Value::Dict signed_bid_dict = MakeValidSignedBid(); |
| (*signed_bid_dict.FindList("signatures"))[0].GetDict().Set("signature", |
| kLength65); |
| auto result = |
| DecodeSignedAdditionalBid(base::Value(std::move(signed_bid_dict))); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ("Field 'signature' has unexpected length.", result.error()); |
| } |
| |
| TEST_F(AdditionalBidsUtilTest, VerifySignature) { |
| const int kKeys = 4; |
| |
| struct { |
| uint8_t public_key[32]; |
| uint8_t private_key[64]; |
| } key_pairs[kKeys]; |
| |
| SignedAdditionalBid data; |
| data.additional_bid_json = "Greetings. I am JSON!"; |
| for (int i = 0; i < kKeys; ++i) { |
| ED25519_keypair(key_pairs[i].public_key, key_pairs[i].private_key); |
| |
| data.signatures.emplace_back(); |
| memcpy(data.signatures[i].key.data(), key_pairs[i].public_key, 32); |
| |
| bool ok = ED25519_sign( |
| data.signatures[i].signature.data(), |
| reinterpret_cast<const uint8_t*>(data.additional_bid_json.data()), |
| data.additional_bid_json.size(), key_pairs[i].private_key); |
| CHECK(ok); |
| } |
| |
| EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre(0, 1, 2, 3)); |
| |
| // Flip a bit in the [1] signature. |
| data.signatures[1].signature[3] ^= 0x02; |
| EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre(0, 2, 3)); |
| |
| // Flip a couple of bits in the [2] key. |
| data.signatures[2].key[7] ^= 0x41; |
| EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre(0, 3)); |
| |
| // Change the payload. |
| data.additional_bid_json += "Boo. Bad unverified data appended!"; |
| EXPECT_THAT(data.VerifySignatures(), UnorderedElementsAre()); |
| } |
| |
| class AdditionalBidsUtilNegativeTargetingTest : public AdditionalBidsUtilTest { |
| public: |
| const url::Origin kBuyer = url::Origin::Create(GURL("https://buyer.test")); |
| const url::Origin kOtherBuyer = |
| url::Origin::Create(GURL("https://other.test")); |
| |
| const url::Origin kJoin = url::Origin::Create(GURL("https://engines.test")); |
| const url::Origin kOtherJoin = |
| url::Origin::Create(GURL("https://wagons.test")); |
| |
| static constexpr size_t kNumNegativeInterestGroups = 4; |
| |
| AdditionalBidsUtilNegativeTargetingTest() |
| : source_id_(ukm::AssignNewSourceId()), recorder_(source_id_) { |
| negative_targeter_.AddInterestGroupInfo(kBuyer, "a", kJoin, |
| KeyFromLiteral(kKey1)); |
| negative_targeter_.AddInterestGroupInfo(kBuyer, "b", kJoin, |
| KeyFromLiteral(kKey2)); |
| negative_targeter_.AddInterestGroupInfo(kOtherBuyer, "c", kJoin, |
| KeyFromLiteral(kKey1)); |
| negative_targeter_.AddInterestGroupInfo(kBuyer, "z", kOtherJoin, |
| KeyFromLiteral(kKey1)); |
| } |
| |
| void VerifyMetricValue(std::string metric_name, int64_t expected_value) { |
| std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry> entries = |
| ukm_recorder_.GetEntries( |
| ukm::builders::AdsInterestGroup_AuctionLatency_V2::kEntryName, |
| {metric_name}); |
| ASSERT_THAT(entries, testing::SizeIs(1)); |
| ASSERT_EQ(entries.at(0).source_id, source_id_); |
| ASSERT_TRUE(entries.at(0).metrics.contains(metric_name)) |
| << "Missing expected metric, " << metric_name; |
| EXPECT_EQ(entries.at(0).metrics[metric_name], expected_value) |
| << "Unexpected value for " << metric_name << " metric"; |
| } |
| |
| void VerifyMetrics(int64_t invalid_signatures, |
| int64_t joining_origin_mismatches) { |
| recorder_.OnAuctionEnd(AuctionResult::kSuccess); |
| VerifyMetricValue( |
| ukm::builders::AdsInterestGroup_AuctionLatency_V2:: |
| kNumNegativeInterestGroupsIgnoredDueToInvalidSignatureName, |
| invalid_signatures); |
| VerifyMetricValue( |
| ukm::builders::AdsInterestGroup_AuctionLatency_V2:: |
| kNumNegativeInterestGroupsIgnoredDueToJoiningOriginMismatchName, |
| joining_origin_mismatches); |
| } |
| |
| protected: |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| ukm::TestAutoSetUkmRecorder ukm_recorder_; |
| ukm::SourceId source_id_; |
| AuctionMetricsRecorder recorder_; |
| AdAuctionNegativeTargeter negative_targeter_; |
| }; |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, GetNumNegativeInterestGroups) { |
| EXPECT_EQ(negative_targeter_.GetNumNegativeInterestGroups(), |
| kNumNegativeInterestGroups); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, SuccessfullyNegativeTargets) { |
| // Negative targets a, key1 matches that. |
| std::vector<std::string> errors_out; |
| EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/std::nullopt, |
| /*negative_target_interest_group_names=*/{"a"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1), SignatureWithLiteralKey(kKey2)}, |
| /*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, UnorderedElementsAre()); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, WrongBuyer) { |
| // Negative targets c, which isn't under kBuyer. |
| std::vector<std::string> errors_out; |
| EXPECT_FALSE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/std::nullopt, |
| /*negative_target_interest_group_names=*/{"c"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1)}, |
| /*valid_signatures=*/{0}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, UnorderedElementsAre()); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, |
| SuccessfulDespiteUnusedBadSignature) { |
| // Negative targets a, key1 matches that, but one of the keys is wrong. |
| std::vector<std::string> errors_out; |
| EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/std::nullopt, |
| /*negative_target_interest_group_names=*/{"a"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1), SignatureWithLiteralKey(kKey2)}, |
| /*valid_signatures=*/{0}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT( |
| errors_out, |
| UnorderedElementsAre("Warning: Some signatures on an additional bid " |
| "from 'https://buyer.test' on auction with seller " |
| "'https://seller.test' failed to verify.")); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, NoMatchingKey) { |
| // Negative targets b, key does not match that. |
| std::vector<std::string> errors_out; |
| EXPECT_FALSE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/std::nullopt, |
| /*negative_target_interest_group_names=*/{"b"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1)}, |
| /*valid_signatures=*/{0}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, |
| UnorderedElementsAre( |
| "Warning: Ignoring negative targeting group 'b' on an " |
| "additional bid from 'https://buyer.test' on auction with " |
| "seller 'https://seller.test' since its key does not " |
| "correspond to a valid signature.")); |
| VerifyMetrics( |
| /*invalid_signatures=*/1, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, SuccessfulDespiteMissingKey) { |
| // Negative targets a with invalid key, non-existent c and d, |
| // then b with valid key. |
| std::vector<std::string> errors_out; |
| EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/std::nullopt, |
| /*negative_target_interest_group_names=*/{"a", "c", "d", "b"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey2)}, |
| /*valid_signatures=*/{0}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, |
| UnorderedElementsAre( |
| "Warning: Ignoring negative targeting group 'a' on an " |
| "additional bid from 'https://buyer.test' on auction with " |
| "seller 'https://seller.test' since its key does not " |
| "correspond to a valid signature.")); |
| VerifyMetrics( |
| /*invalid_signatures=*/1, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, |
| SuccessfulSoDoesNotEvenSeeMissingKey) { |
| // Negative targets a with valid key, non-existent c and d, |
| // then b with invalid key. We don't get far enough to warn about b. |
| std::vector<std::string> errors_out; |
| EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/std::nullopt, |
| /*negative_target_interest_group_names=*/{"a", "c", "d", "b"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1)}, |
| /*valid_signatures=*/{0}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, UnorderedElementsAre()); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, JoiningOriginMismatch) { |
| // Negative targets a, b with valid keys, but none of the joins match. |
| std::vector<std::string> errors_out; |
| EXPECT_FALSE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/kOtherJoin, |
| /*negative_target_interest_group_names=*/{"a", "b"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1), SignatureWithLiteralKey(kKey2)}, |
| /*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, |
| UnorderedElementsAre( |
| "Warning: Ignoring negative targeting group 'a' on an " |
| "additional bid from 'https://buyer.test' on auction with " |
| "seller 'https://seller.test' since it does not have the " |
| "expected joining origin.", |
| "Warning: Ignoring negative targeting group 'b' on an " |
| "additional bid from 'https://buyer.test' on auction with " |
| "seller 'https://seller.test' since it does not have the " |
| "expected joining origin.")); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/2); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, |
| SuccessfulWithMultipleNegativeInterestGroups) { |
| // Negative targets a, b with valid keys; all of the joins match. |
| std::vector<std::string> errors_out; |
| EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/kJoin, |
| /*negative_target_interest_group_names=*/{"a", "b"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1), SignatureWithLiteralKey(kKey2)}, |
| /*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, UnorderedElementsAre()); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/0); |
| } |
| |
| TEST_F(AdditionalBidsUtilNegativeTargetingTest, |
| SuccessfulDespiteTwoJoiningOriginMismatches) { |
| // Negative targets a, b,z with valid keys; only the join on z matches. |
| std::vector<std::string> errors_out; |
| EXPECT_TRUE(negative_targeter_.ShouldDropDueToNegativeTargeting( |
| kBuyer, |
| /*negative_target_joining_origin=*/kOtherJoin, |
| /*negative_target_interest_group_names=*/{"a", "b", "z"}, |
| /*signatures=*/ |
| {SignatureWithLiteralKey(kKey1), SignatureWithLiteralKey(kKey2)}, |
| /*valid_signatures=*/{0, 1}, kSeller, recorder_, errors_out)); |
| EXPECT_THAT(errors_out, |
| UnorderedElementsAre( |
| "Warning: Ignoring negative targeting group 'a' on an " |
| "additional bid from 'https://buyer.test' on auction with " |
| "seller 'https://seller.test' since it does not have the " |
| "expected joining origin.", |
| "Warning: Ignoring negative targeting group 'b' on an " |
| "additional bid from 'https://buyer.test' on auction with " |
| "seller 'https://seller.test' since it does not have the " |
| "expected joining origin.")); |
| VerifyMetrics( |
| /*invalid_signatures=*/0, |
| /*joining_origin_mismatches=*/2); |
| } |
| |
| } // namespace |
| } // namespace content |