WICG / turtledove

TURTLEDOVE
https://wicg.github.io/turtledove/
Other
533 stars 236 forks source link

Support for Negative Interest Group Targeting #319

Open stguav opened 2 years ago

stguav commented 2 years ago

The current FLEDGE API only supports positive Interest Group targeting: allowing advertisers to show ads to users that have interacted with their site. Advertisers are not just interested in re-engaging existing customers; advertisers are also interested in attracting new customers. Within FLEDGE, targeting this group of users requires an API change to support negative IG targeting: showing ads only if a user does not belong to an IG. Here we are specifically proposing a mechanism for negative IG targeting for non-FLEDGE ads.

This can be made to work in a way that is compatible with the privacy sandbox principles. We propose something like the following extension of FLEDGE.

In the auctionConfig, we add a new optional field negativeTargetingAds:

const myAuctionConfig = {
// ...
  'negativeTargetingAds': {
       'buyer-1': [
         { 
           'ig-name-1': externalAd1,
           // For reportWin only
           'reportingLogicUrl': 'https://www.someurl...'
           'bid': bidValue
           'desirability': desirabilityScore
           'render': ...
         }, 
         ...
       ],
       'buyer-2': [{ 'ig-name-2': externalAd2, ... }, ...],
       // ...
    }
}

where each externalAd is only eligible if the user does not have the corresponding IG. The externalAd object has a desirability field, a 'bid' field for reporting purposes, and a render field. Note that server-side the SSP can vet the DSPs' ads, apply any sellside specific logic, and select the top one per negative IG. Then, the externally supplied ads that are eligible can compete with the positive IG ads' desirability output from scoreAd.

To maintain privacy, if one of the negative IG ads wins, then it goes through the FLEDGE rendering and reporting flow; that is, it will be rendered via the FLEDGE auctionResultPromise being passed into a Fenced Frame. A null response from runAdAuction will indicate neither a positive nor negative interest group ad won the browser auction. The seller reporting would be unchanged, except perhaps from some 'browserSignals' field indicating that it was negative targeting. The externalAd provides a 'reportingLogicUrl', which provides the buyer reporting function reportWin. The reportWin function should follow the same spec as when a regular IG ad wins in FLEDGE.

Furthermore, the ad would be subject to the same k-anonymity requirements.

JensenPaul commented 2 years ago

This is tricky. There does not seem to be any great solution to this problem. I have a few suggestions below, but none is really satisfying.

Given that advertisement rendering and reporting in your proposal go through the same controlled channels that normal FLEDGE interest group advertising does (i.e. Fenced Frames and reporting worklets), and that the interest group and auction configuration information flow from the same places they do in normal FLEDGE operation, I think the privacy aspects of your proposal seem reasonable. I like that your proposal encourages contextual advertising to go through the FLEDGE auction as this potentially decreases the entropy leaked by the null versus non-null auction promise result (decreasing #211 concerns).

In FLEDGE today, the person using an interest group’s information to bid (represented by the origin of biddingLogicUrl) is required to match the interest group’s owner. By passing bids into the auction via negativeTargetingAds, the browser cannot enforce this restriction. Essentially the browser cannot answer the question “how do we know the person adding people to the interest group wants their interest group to be used when targeting the contextual ad?” I think there are a few potential ways to solve this problem but none is particularly simple or efficient:

Option 1: Use bidding scripts Instead of passing the bid value and ad URL in as part of negativeTargetingAds, pass in the URL of a generateBid() script that can return the bid value and ad URL. The bidding script’s origin can be required to match that of the interest group owner.

Option 2: Use a signed exchange (SXG) or signed WebBundle Instead of passing the bid value and ad URL in as part of negativeTargetingAds, pass in a SXG or signed WebBundle that specifies the bid value and ad URL, or contains a generateBid() script. The signature’s origin can be required to match that of the interest group owner.

Option 3: Use an iframe Instead of passing the bid value and ad URL in as part of negativeTargetingAds, provide an API for an iframe to pass in the bid value and ad URL into the auction. The iframe’s origin could be required to match that of the interest group owner.

Option 4: Limit to runAdAuction() caller We could require the caller of runAdAuction()’s origin match that of the interest group owner. This is likely to be prohibitively limiting.

It should be noted that several of these solutions are similar to solutions considered in #119.

The interest group could list other origins they are willing to allow using this interest group for negative targeting. The listed origins would have to match one of the origins from Options 1-4.

MattMenke2 commented 2 years ago

For Options 1 and 2 - normally only the IG owner can add a user to an IG, or a 3rd party explicitly delegated permission by the owner. Even forcing the bidder script to be loaded from a same-origin URL to the interest group, we're still potentially asking a bidder to bid unexpectedly. We'd probably need the bidding script to explicitly opt-in to this behavior, likely be returning an additional field in the return value to explicitly allow this use case.

There are a also a rather large number of design decisions that would need to be made (Do we support bidding signals? What's the priority of the pseudo-IG? Do we need to check for k-anonymity, and if so, how do we do it? Do we have an API to also set a WASM url? What IG would we pass in to generateBid()).

abrik0131 commented 1 year ago

We would like to propose the following option for supporting negative Interest Group targeting.

Disable negative targeting

If the ad owner and IG owner do not match, the ad will be shown regardless of whether the browser has joined IG or not. With this option, the matching of the owners needs to occur only if the browser has joined the negatively targeted IG.

Suppose that advertiser A wants to target users who have not joined G - an interest group belonging to advertiser B. The browsers that did not join G will unconditionally allow negative targeting of G. The browser that joined G, however, will not show the ad only if the owners match.

This will make A negatively targeting G fail, since the exclusion of browsers that joined G will be prevented.

Use of digital signatures

Instead of passing the bid value and ad URL in as part of negativeTargetingAds, pass in a SXG or signed WebBundle that specifies the bid value and the ad URL, or contains a generateBid() script, and signed with IG owner’s private key. The signature will be validated by Chrome with the IG owner’s public key on the client.

Seller - buyer interaction for negative IG targeting

It is SSP owned code that receives the negative IG targeting DSP data. This arrangement allows SSPs to filter and rank negative IG targeted ads before the FLEDGE auction.

DSPs need to provide two categories of negative IG targeting data to the SSP.

  1. The bid or other metadata needed by SSP to filter and perform other relevant processing.
  2. SXG or signed WebBundle which will be passed by SSP code to Chrome code for validation.

SSP code will be able to use category 1 data to perform filter or other processing. If SSP decides that negative IG targeting metadata can proceed to the auction, it will move category 2 data into the auction config prior to initiating FLEDGE auction.

DSPs could provide metadata about negative interest group targeting of ads to SSPs through the regular OpenRTB bid response (see https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf section 4) with custom Bid object extension fields, for example (negative_targeting_metadata):

{
  "id": "Vb8ttXOO",
  "seatbid": [{
    "seat": "buyer",
    "bid": [{
      "id": "L8QmgECv",
      "impid": "1",
      "price": 2.3,
      "nurle": "win_notice_url",
      "adm": "<img src=\"https://adnetwork.example/ads?id=9nLHIRIb&wprice=${AUCTION_PRICE}\">",
      "adomain": "shoes.advertiser.example",
      "cid": "running_shoes_campaign",
      "crid": "running_shoes_creative",
      "ext": {
        "negatively_targeted_interest_group": "running_shoes",
        "signed_metadata_for_chrome": "signed_metadata_for_running_shoes"
      }
      "w": "361",
      "h": "203"
    },
    {
      "id": "9nLHIRIb",
      "impid": "2",
      "price": 3.2,
      "nurle": "win_notice_url",
      "adm": "<img src=\"https://adnetwork.example/ads?id=5vAyHHZS&wprice=${AUCTION_PRICE}\">",
      "adomain": "watches.advertiser.example",
      "cid": "sports_watch_campaign",
      "crid": "sports_watch_creative",
      "ext": {
        "negatively_targeted_interest_group": "sports_watch",
        "signed_metadata_for_chrome": "signed_metadata_for_sports_watches"
      }
      "w": "320",
      "h": "320"
    }]
  }]
}

Chrome will validate signed_metadata_for_chrome. signed_metadata_for_chrome contains buyer domain or some other information that will help to verify that the buyer providing negative IG targeting metadata has permissions to negatively target the specified interest group.

If an SSP decides that a given ad can participate in the FLEDGE auction, it will add the following to the auction config:

const myAuctionConfig = {
  negativeTargetingAds: {
       negative_targeting_metadata.seatbid(0).seat(): [{
         buyer_ad_metadata: negative_targeting_metadata.seatbid(0).bid(0),
         desirability: desirability_score1,
         sellerReportingURL: seller_reporting_url,
         signedMetadata: negative_targeting_metadata.seatbid(0).bid(0).ext()
           .signed_metadata_for_chrome(),
       },
       ...
       ],
       negative_targeting_metadata.seatbid(1).seat(): [{
         buyer_ad_metadata: negative_targeting_metadata.seatbid(1).bid(2),
         desirability: desirability_score2,
         sellerReportingURL: seller_reporting_url,
         signedMetadata: negative_targeting_metadata.seatbid(1).bid(2).ext()
           .signed_metadata_for_chrome(),
       },
       ...
    }
}

Here, buyer_ad_metadata corresponds to the Bid object of the bid response and contains ad data needed by the buyer in order for the ad to enter the auction. Desirability is the seller’s desirability score for the ad that will be used to compare a negatively targeted ad against other FLEDGE bids (which are assigned their desirability score by the scoreAd function), and sellerReportingURL is the URL that the browser will request if the ad wins and gets rendered. These fields are to be provided by the SSP.

sbelov commented 1 year ago

contains ad data needed by the buyer in order for the ad to enter the auction

What does this mean? Can you clarify intent of buyer_ad_metadata above and how it could be used in the on-device auction? Who or what (some of on-device bidding or reporting functions? something else?) would be consuming this metadata?

abrik0131 commented 1 year ago

buyer_ad_metadata will be initially consumed by the seller on the serverside. It will contain the data required by the seller to decide whether the ad can enter the auction, and the data required by the seller to optionally log information about the ad. If the seller decides to pass the ad to the browser-side auction, it will add the metadata to the auction config, possibly modifying "price" (since "price" should be seller's desirability score).

On the browser, Chrome owned code will use the data to decide whether the ad wins the auction. If the ad wins, Chrome will use "nurl" to notify the buyer about the outcome of the auction. Currently, the rendering mechanism is underspecified. Nevertheless, Chrome code will also initiate the rendering if the ad wins.

MattMenke2 commented 1 year ago

Using a WebBundle from the DSP that owns the IG being negatively targeted to fully generate the bid, and identify the IG being negatively targeted (or at least confirm it) does sound like it could be viable to me, from privacy and security standpoints.

sbelov commented 1 year ago

@MattMenke2 can you clarify what you mean by 'WebBundle from the DSP' in this context - assuming for now it is an SSP that returns an ad response to the device?

MattMenke2 commented 1 year ago

I mean the IG owner. I don't think we can expose IGs to 3P scripts without buy-in from the IG owner. Using ads generated by the IG owner is the way FLEDGE currently accomplishes that.

orrb1 commented 1 year ago

Hi everyone. I've been thinking about this problem for a bit, and I have a design I'd like to propose, which is mostly aligned with the comments above. With this design, it's possible to pass additional bids into runAdAuction(), and for those additional bids to specify a negative InterestGroup that would behave as described above — a user's membership in that negative InterestGroup would indicate that the associated additional bid should not participate in the auction.

There are a few subtle differences worth noting between these additional bids and InterestGroup-based bids (those produced by calls to generateBid()).

Each additional bid would be represented by a JSON object containing all of the data we need for that bid to participate in the auction alongside InterestGroup-based bids. Each of the additional bids would need to be enclosed in a signed exchange (SXG, https://web.dev/signed-exchanges/) — a data structure that bundles together a URL, its associated response, and a signature for validation — so that the auction can verify that the additional bid came from the buyer, unmodified.

The response payload of each additional bid's corresponding signed exchange would contain the JSON object representing that bid. That object would include the following fields, split into three categories:

{
  "interestGroup": {
    // These would be passed to reportWin()
    "name": "campaign123",
    // This is used for its definition of reportWin()
    "biddingLogicURL": "https://example-dsp.com/bid_logic.js"
  },

  "bid": {
    // Fields analogous to those returned by generateBid()
    "ad": 'ad-metadata',
    "adCost": 2.99,
    "bid": 1.99,
    "bidCurrency": "USD",
    "render": "https://example-dsp.com/ad/123.jpg",
    "adComponents": [adComponent1, adComponent2, ...],
    "allowComponentAuction": true,
    "modelingSignals": 123,
  },

  // These fields are described in more detail below
  "negativeInterestGroup": "campaign123_negative_interest_group",
  "auctionNonce": "1234567890abcdeffedcba0987654321",
  "seller": "https://www.example-ssp2.com",
  "topLevelSeller": "https://www.another-ssp.com"
}

This design implements a series of protections to ensure that additional bids may be used only for the auction for which the buyer intended.

  1. Signed exchanges support an expiration date, after which the signed exchange is no longer considered valid. The signed exchange enclosing each additional bid should only be valid for a short period of time — provisionally 60 seconds. Any additional bid that has passed its expiration date will be ineligible from participating in the auction.
  2. Any auction that will use additional bids will need to first call a new navigator.createAuctionNonce() JavaScript method, setting the returned value in a new auctionNonce field on the auctionConfig, and ensuring that all additional bids have the same value set in their auctionNonce fields. Any additional bid for which the nonce is missing or different from that of the auction will be ineligible from participating in the auction.
  3. Each additional bid will also need to be annotated with the associated seller to ensure that only the seller for which it was intended may use that bid. For multi-seller auctions, this further ensures that the additional bid may only be used by the specific component auction for which it was intended. Any additional bid for which the seller is missing or different from that of the auction/component auction will be ineligible from participating in that auction/component auction.
  4. For multi-seller auctions, the additional bid will need to specify the topLevelSeller alongside the seller, the latter being associated with the seller of the component auction. Any additional bid in a multi-seller auction for which the topLevelSeller is missing or different from that of the auction will be ineligible from participating in that auction.

For each additional bid, the URL associated with the signed exchange must have the origin of the associated buyer, but may otherwise have any path or query string that uniquely identifies that bid. The owner of the additional bid is implied by the origin of the signed exchange, and it must be this owner whose certificate is used to sign the exchange. This URL is only used in the user's browser as an identifier, and no attempt will be made to fetch an additional bid from its origin. As is the case for InterestGroups, the biddingLogicURL field of an additional bid must be same-origin with the buyer of that additional bid, "and must point to URLs whose responses include the HTTP response header X-Allow-FLEDGE: true," as described in the explainer.

To get these additional bids into the auction, we'll add a new additionalBids field onto the auction config. The value of additionalBids will be a JavaScript Array of Uint8Array instances, each containing the encoded bytes of a single signed exchange representing a single additional bid. As with several other fields on the auction config, additionalBids can be passed in either as its actual value or as a Promise to that value. The buyer of each additional bid must be included in the auction's interestGroupBuyers if that additional bid specifies a negativeInterestGroup.

For each additional bid, the auction will first check to see if the user is a member of the specified negativeInterestGroup, if the additional bid provides one. Because only the name of the negative InterestGroup is specified, this implicitly enforces that the additional bid buyer can use only those InterestGroups for which it is the owner. Any InterestGroup may be used as a negative InterestGroup. An InterestGroup specifically intended for use as a negative InterestGroup only needs to provide two of the InterestGroup attributes - owner and name. If the user is a member of the specified negativeInterestGroup, the additional bid will not participate in the auction.

The auction will call scoreAd() for that additional bid that remains after validation and negative targeting have been applied. Each such additional bid can then compete against both InterestGroup-based bids and other additional bids in the auction. For additional bids that win the auction, event-level win reporting is supported via reportWin, just as it is for stored InterestGroups.

orrb1 commented 1 year ago

One clarification based on feedback received on the proposal above. This design is only intended to address the use case of negatively targeting contextual ads and other ads not triggered by the presence of a positive InterestGroup (non-remarketing). One thing that led to this confusion is that, among the data provided for each additional bid, a bid needs to specify an "InterestGroup name" via the interestGroup.name field. This InterestGroup name does not map to an existing InterestGroup, and it's only needed/used when reporting the win of an additional bid. Thanks for the feedback.

dmdabbs commented 1 year ago

@orrb1 thank you for the design proposal follow-up. Interesting stuff. Would be great to discuss in this week's Wednesday PAAPI call. I added it to end of tomorrow's agenda. Hope you or @morlovich are available to field questions & suggestions.

orrb1 commented 1 year ago

One piece of feedback received on the design above is that the option to specify only a single negativeInterestGroup is overly limiting. In order to improve the usability of this feature, we're expanding it to allow multiple negativeInterestGroups, with the restriction that, when multiple negativeInterestGroups are specified, all must have been joined from a single origin specified as part of the additional bid.

This restriction won't apply to additional bids that specify only a single negativeInterestGroup. When a single negativeInterestGroup is specified, that InterestGroup may have been joined from any origin, or even from multiple origins. To support this, we'll use a syntax that distinguishes the single negativeInterestGroup case from the multiple negativeInterestGroups case. For a single negativeInterestGroup, we use the same syntax expressed in the design above:

{
  "interestGroup": {
     // existing fields
   },
  "bid": {
     // existing fields
   },
  "negativeInterestGroup": "example_advertiser_negative_interest_group"
  // remaining fields
}

To specify multiple negativeInterestGroups, buyers would use the following syntax:

{
  "interestGroup": {
     // existing fields
   },
  "bid": {
     // existing fields
   },
  "negativeInterestGroups": {
    joiningOrigin: "https://example-advertiser.com",
    interestGroupNames: [
      "example_advertiser_negative_interest_group_a",
      "example_advertiser_negative_interest_group_b",
    ]
  },
  // remaining fields
}

Edit(8/23/23): Modified the joining origin to include the protocol.

orrb1 commented 1 year ago

As we worked to implement our previous design, we ran into some headwinds with our use of Signed Exchanges (SXGs). It’s unclear if we needed all the comprehensiveness of SXGs, so we’re considering a somewhat simpler approach that involves the following tweaks to the previous proposal:

When the browser considers whether to block an additional bid based on whether its negativeInterestGroup exists, the blocking only occurs if the interest group’s additionalBidKey matches one of the keys in the additional bid's signatures field, and it’s verified as having signed the additional bid's bid string into the signature. If the signature does not match, the bid is not blocked and continues as if the negativeInterestGroup didn’t exist.

It should be noted that the browser has less verification that the additionalBids originated from the named bidders, and thus bidders should be sure to verify that billing for rendered ads matches their logs for these additionalBids. This is made easier by the fact that additionalBids are contextual bids so they only contain single-site information, so they can report unique bid identifiers linked to particular bid prices and ads.

Because the browser is no longer verifying the additional bid's signature in cases where the negative interest group is present, it could be tampered with. This represents a difference with the interest group that is normally passed to reporting functions reportWin() and reportResult(), so to ensure that adtechs are aware of this difference in inputs to the reporting functions, we’re changing the names of the reporting worklets that are called for additionalBids to reportAdditionalBidWin() and reportAdditionalBidResult().

JensenPaul commented 1 year ago

I think @orrb1 meant "is not present" when he said "is present" in the last comment.

orrb1 commented 1 year ago

With the shift away from signed exchanges, we no longer can verify that the additional bids are untampered with by other JavaScript in the context that calls runAdAuction. One way to increase the security of the additional bids is to ensure that they arrive at the auction directly from ad tech servers without passing through the JavaScript context. To address this, we're applying the same response header mechanism to protect additional bids that's already in use for the Bidding & Auction response blob and directFromSellerSignals.

To use response headers to convey the additional bids, the request to fetch additional bids will first need to specify the adAuctionHeaders fetch flag.

fetch("https://...", {adAuctionHeaders: true});

The response will then need to include the additional bids not as part of the body of the HTTP response, but rather as response header values. Each instance of the Ad-Auction-Additional-Bid response header will correspond to a single additional bids. The response may include more than one additional bids by specifying multiple instances of the Ad-Auction-Additional-Bid response header. The structure of each instance of the Ad-Auction-Additional-Bid header must be as follows:

Ad-Auction-Additional-Bid:
    <auction nonce>:<base64-encoded additional bid JSON>

These HTTP response headers are intercepted by the browser, diverted to participate in the auction, but removed from the HTTP response headers seen in JavaScript. The additionalBids field on the auction config will be a Promise of type undefined, since it needs no value, but rather is only used to express that all additional bids for that auction have arrived. When all of the header-based additional bids for an auction have been received, the additionalBids Promise should be satisfied. The browser will use this to accept the bids provided by the Ad-Auction-Additional-Bid response headers into the auction.

dmdabbs commented 1 year ago

Owners providing addtional bids must also pass the enrollment gating, yes?

orrb1 commented 1 year ago

Owners providing addtional bids must also pass the enrollment gating, yes?

Yes, that's correct. An additional bid's owner has to satisfy the same requirements as the owner of a bid created from a generateBid() call, including the enrollment gating and inclusion in auction config's interestGroupBuyers field.

orrb1 commented 1 year ago

The negative targeting design described above has been implemented and documented in the Protected Audience explainer at https://github.com/WICG/turtledove/blob/main/FLEDGE.md#6-additional-bids.

There's one upcoming change to the implementation. The only field of a negative targeting interest group that can currently be updated is additionalBidKey. However, the additionalBidKey needs to be rotated for security only every 30 days. As such, key rotation can happen as part of the expiration of negative targeting interest groups. Therefore, negative targeting interest groups will not be updatable via the updateURL.

Supporting a 30 day key rotation, each additional bid should be signed with both the current and previous additionalBidKeys. (The exception is when a buyer is first using additional bids, when they don't yet have a previous additionalBidKey.) The process would work as follows:

In Chrome versions M119 and later, joinAdInterestGroup will return an error if called with an interest group that provides both an additionalBidKey - which makes it usable as a negative targeting interesting group - and updateURL. The explainer will be updated to reflect this, and to specify the procedure for rotating the additionalBidKey described above.

ryanluz commented 1 year ago

@orrb1 Does the seller pass the auctionNonce to buyer in contextual call request? It seems this is not documented in https://github.com/orrb1/turtledove/blob/main/FLEDGE.md#6-additional-bids. My understanding of the auctionNonce flow is:

  1. seller's auction script runs navigator.createAuctionNonce() to generate a new auctionNonce.
  2. seller's auction script makes contextual calls with auctionNonce in the request.
  3. buyer's contextual call response include with Ad-Auction-Additional-Bid:<auction nonce>:<base64-encoding of the signed additional bid> as header.
  4. seller's auction script runs auction with the same auctionNonce
    navigator.runAdAuction({
    // ...
    'auctionNonce':  auctionNonce,
    'additionalBids': ...,
    });
xxia2021 commented 1 year ago

https://github.com/WICG/turtledove/blob/main/FLEDGE.md#63-http-response-headers mentioned the need to send a fetch request with adAuctionHeaders specified. However, some of our ad tags uses iframe navigation. Changing the default rendering mode of it would be a significant product and engineering lift. Could Chrome support an iframe-based version of the adAuctionHeaders API, e.g., create an iframe attribute,