diff options
Diffstat (limited to 'testing/web-platform/tests/fledge')
19 files changed, 1198 insertions, 104 deletions
diff --git a/testing/web-platform/tests/fledge/tentative/TODO b/testing/web-platform/tests/fledge/tentative/TODO index 0f68a7c914..6fd378c035 100644 --- a/testing/web-platform/tests/fledge/tentative/TODO +++ b/testing/web-platform/tests/fledge/tentative/TODO @@ -47,6 +47,7 @@ Need tests for (likely not a complete list): * adAuctionConfig passed to reportResult(). * Component auctions. * Including cross-origin sellers. + * Timeouts (seller timeout, buyer timeout, reporting timeout). * browserSignals fields in scoring/bidding methods. * In reporting methods, browserSignals fields: topLevelSeller, componentSeller, modifiedBid, adCost, madeHighestScoringOtherBid @@ -88,3 +89,4 @@ If possible: * Signals request batching. This is an optional feature, so can't require it, but maybe a test where batching could be used, and make sure things work, whether batching is used or not? +* reporting timeout being 0. diff --git a/testing/web-platform/tests/fledge/tentative/additional-bids.https.window.js b/testing/web-platform/tests/fledge/tentative/additional-bids.https.window.js index 0e1d22c261..965f9a60c7 100644 --- a/testing/web-platform/tests/fledge/tentative/additional-bids.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/additional-bids.https.window.js @@ -10,15 +10,8 @@ // This file contains tests for additional bids and negative targeting. // // TODO: -// - test that an negatively targeted additional bid is suppressed. -// - test that an incorrectly signed additional bid is not negative targeted. -// - test that an missing-signature additional bid is not negative targeted. -// - test that an additional bid with some correct signatures can be negative. -// negative targeted for those negative interest groups whose signatures -// match. -// - test an additional bid with multiple negative interest groups. -// - test that multiple negative interest groups with mismatched joining origins -// is not negative targeted. +// - test that an additional bid with some correct signatures can be negative +// targeted for those negative interest groups whose signatures match. // - test that additional bids can be fetched using an iframe navigation. // - test that additional bids are not fetched using an iframe navigation for // which the `adAuctionHeaders=true` attribute is not specified. @@ -26,12 +19,19 @@ // `adAuctionHeaders: true` is not specified. // - test that an additional bid with an incorrect auction nonce is not used // included in an auction. Same for seller and top-level seller. +// - lots of tests for different types of malformed additional bids, e.g. +// missing fields, malformed signature, invalid currency code, +// missing joining origin for multiple negative interest groups, etc. // - test that correctly formatted additional bids are included in an auction // when fetched alongside malformed additional bid headers by a Fetch -// request. +// request (both invalid headers and invalid additional bids) +// - test that an additional bid is rejected if its from a buyer who is not +// allowed to participate in the auction. +// - test that an additional bid is rejected if its currency doesn't match the +// buyer's associated per-buyer currency from the auction config. // - test that correctly formatted additional bids are included in an auction // when fetched alongside malformed additional bid headers by an iframe -// navigation. +// navigation (both invalid headers and invalid additional bids). // - test that reportWin is not used for reporting an additional bid win. // - test that additional bids can *not* be fetched from iframe subresource // requests. @@ -80,6 +80,9 @@ // for ad auction headers interception to associate it with this auction. const SINGLE_SELLER_AUCTION_SELLER = window.location.origin; +const ADDITIONAL_BID_SECRET_KEY = 'nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A='; +const ADDITIONAL_BID_PUBLIC_KEY = '11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo='; + // Single-seller auction with a single buyer who places a single additional // bid. As the only bid, this wins. subsetTest(promise_test, async test => { @@ -88,12 +91,12 @@ subsetTest(promise_test, async test => { const seller = SINGLE_SELLER_AUCTION_SELLER; const buyer = OTHER_ORIGIN1; - const additionalBid = createAdditionalBid( + const additionalBid = additionalBidHelper.createAdditionalBid( uuid, auctionNonce, seller, buyer, 'horses', 1.99); await runAdditionalBidTest( test, uuid, [buyer], auctionNonce, - fetchAdditionalBids(seller, [additionalBid]), + additionalBidHelper.fetchAdditionalBids(seller, [additionalBid]), /*highestScoringOtherBid=*/0, /*winningAdditionalBidId=*/'horses'); }, 'single valid additional bid'); @@ -105,16 +108,17 @@ subsetTest(promise_test, async test => { const seller = SINGLE_SELLER_AUCTION_SELLER; const buyer1 = OTHER_ORIGIN1; - const additionalBid1 = createAdditionalBid( + const additionalBid1 = additionalBidHelper.createAdditionalBid( uuid, auctionNonce, seller, buyer1, 'horses', 1.99); const buyer2 = OTHER_ORIGIN2; - const additionalBid2 = createAdditionalBid( + const additionalBid2 = additionalBidHelper.createAdditionalBid( uuid, auctionNonce, seller, buyer2, 'planes', 2.99); await runAdditionalBidTest( test, uuid, [buyer1, buyer2], auctionNonce, - fetchAdditionalBids(seller, [additionalBid1, additionalBid2]), + additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid1, additionalBid2]), /*highestScoringOtherBid=*/1.99, /*winningAdditionalBidId=*/'planes'); }, 'two valid additional bids'); @@ -127,20 +131,241 @@ subsetTest(promise_test, async test => { const seller = SINGLE_SELLER_AUCTION_SELLER; const buyer1 = OTHER_ORIGIN1; - const additionalBid1 = createAdditionalBid( + const additionalBid1 = additionalBidHelper.createAdditionalBid( uuid, auctionNonce, seller, buyer1, 'horses', 1.99); const buyer2 = OTHER_ORIGIN2; - const additionalBid2 = createAdditionalBid( + const additionalBid2 = additionalBidHelper.createAdditionalBid( uuid, auctionNonce, seller, buyer2, 'planes', 2.99); - await runAdditionalBidTest( test, uuid, [buyer1, buyer2], auctionNonce, Promise.all([ - fetchAdditionalBids(seller, [additionalBid1]), - fetchAdditionalBids(seller, [additionalBid2]) + additionalBidHelper.fetchAdditionalBids(seller, [additionalBid1]), + additionalBidHelper.fetchAdditionalBids(seller, [additionalBid2]) ]), /*highestScoringOtherBid=*/1.99, /*winningAdditionalBidId=*/'planes'); }, 'two valid additional bids from two distinct Fetch requests'); + +// Single-seller auction with a single additional bid. Because this additional +// bid is filtered by negative targeting, this auction has no winner. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const negativeInterestGroupName = 'already-owns-a-plane'; + + const buyer = OTHER_ORIGIN1; + const additionalBid = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer, 'planes', 2.99); + additionalBidHelper.addNegativeInterestGroup( + additionalBid, negativeInterestGroupName); + additionalBidHelper.signWithSecretKeys( + additionalBid, [ADDITIONAL_BID_SECRET_KEY]); + + await joinNegativeInterestGroup( + test, buyer, negativeInterestGroupName, ADDITIONAL_BID_PUBLIC_KEY); + + await runBasicFledgeTestExpectingNoWinner( + test, uuid, + { interestGroupBuyers: [buyer], + auctionNonce: auctionNonce, + additionalBids: additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid])}); +}, 'one additional bid filtered by negative targeting, so auction has no ' + + 'winner'); + +// Single-seller auction with a two buyers competing with additional bids. +// The higher of these has a negative interest group specified, and that +// negative interest group has been joined, so the lower bid wins. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const negativeInterestGroupName = 'already-owns-a-plane'; + + const buyer1 = OTHER_ORIGIN1; + const additionalBid1 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer1, 'horses', 1.99); + + const buyer2 = OTHER_ORIGIN2; + const additionalBid2 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer2, 'planes', 2.99); + additionalBidHelper.addNegativeInterestGroup( + additionalBid2, negativeInterestGroupName); + additionalBidHelper.signWithSecretKeys( + additionalBid2, [ADDITIONAL_BID_SECRET_KEY]); + + await joinNegativeInterestGroup( + test, buyer2, negativeInterestGroupName, ADDITIONAL_BID_PUBLIC_KEY); + + await runAdditionalBidTest( + test, uuid, [buyer1, buyer2], auctionNonce, + additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid1, additionalBid2]), + /*highestScoringOtherBid=*/0, + /*winningAdditionalBidId=*/'horses'); +}, 'higher additional bid is filtered by negative targeting, so ' + + 'lower additional bid win'); + +// Same as above, except that the bid is missing a signature, so that the +// negative targeting interest group is ignored, and the higher bid, which +// would have otherwise been filtered by negative targeting, wins. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const negativeInterestGroupName = 'already-owns-a-plane'; + + const buyer1 = OTHER_ORIGIN1; + const additionalBid1 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer1, 'horses', 1.99); + + const buyer2 = OTHER_ORIGIN2; + const additionalBid2 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer2, 'planes', 2.99); + additionalBidHelper.addNegativeInterestGroup( + additionalBid2, negativeInterestGroupName); + + await joinNegativeInterestGroup( + test, buyer2, negativeInterestGroupName, ADDITIONAL_BID_PUBLIC_KEY); + + await runAdditionalBidTest( + test, uuid, [buyer1, buyer2], auctionNonce, + additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid1, additionalBid2]), + /*highestScoringOtherBid=*/1.99, + /*winningAdditionalBidId=*/'planes'); +}, 'higher additional bid is filtered by negative targeting, but it is ' + + 'missing a signature, so it still wins'); + +// Same as above, except that the bid is signed incorrectly, so that the +// negative targeting interest group is ignored, and the higher bid, which +// would have otherwise been filtered by negative targeting, wins. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const negativeInterestGroupName = 'already-owns-a-plane'; + + const buyer1 = OTHER_ORIGIN1; + const additionalBid1 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer1, 'horses', 1.99); + + const buyer2 = OTHER_ORIGIN2; + const additionalBid2 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer2, 'planes', 2.99); + additionalBidHelper.addNegativeInterestGroup( + additionalBid2, negativeInterestGroupName); + additionalBidHelper.incorrectlySignWithSecretKeys( + additionalBid2, [ADDITIONAL_BID_SECRET_KEY]); + + await joinNegativeInterestGroup( + test, buyer2, negativeInterestGroupName, ADDITIONAL_BID_PUBLIC_KEY); + + await runAdditionalBidTest( + test, uuid, [buyer1, buyer2], auctionNonce, + additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid1, additionalBid2]), + /*highestScoringOtherBid=*/1.99, + /*winningAdditionalBidId=*/'planes'); +}, 'higher additional bid is filtered by negative targeting, but it has an ' + + 'invalid signature, so it still wins'); + +// A test of an additional bid with multiple negative interest groups. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const negativeInterestGroupName1 = 'already-owns-a-plane'; + const negativeInterestGroupName2 = 'another-negative-interest-group'; + + const buyer1 = OTHER_ORIGIN1; + const additionalBid1 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer1, 'horses', 1.99); + + const buyer2 = OTHER_ORIGIN2; + const additionalBid2 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer2, 'planes', 2.99); + additionalBidHelper.addNegativeInterestGroups( + additionalBid2, [negativeInterestGroupName1, negativeInterestGroupName2], + /*joiningOrigin=*/window.location.origin); + additionalBidHelper.signWithSecretKeys( + additionalBid2, [ADDITIONAL_BID_SECRET_KEY]); + + await joinNegativeInterestGroup( + test, buyer2, negativeInterestGroupName1, ADDITIONAL_BID_PUBLIC_KEY); + + await runAdditionalBidTest( + test, uuid, [buyer1, buyer2], auctionNonce, + additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid1, additionalBid2]), + /*highestScoringOtherBid=*/0, + /*winningAdditionalBidId=*/'horses'); +}, 'higher additional bid is filtered by negative targeting by two negative ' + + 'interest groups, and since one is on the device, the lower bid wins'); + +// Same as above, but with a mismatched joining origin. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const negativeInterestGroupName1 = 'already-owns-a-plane'; + const negativeInterestGroupName2 = 'another-negative-interest-group'; + + const buyer1 = OTHER_ORIGIN1; + const additionalBid1 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer1, 'horses', 1.99); + + const buyer2 = OTHER_ORIGIN2; + const additionalBid2 = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer2, 'planes', 2.99); + additionalBidHelper.addNegativeInterestGroups( + additionalBid2, [negativeInterestGroupName1, negativeInterestGroupName2], + /*joiningOrigin=*/OTHER_ORIGIN1); + additionalBidHelper.signWithSecretKeys( + additionalBid2, [ADDITIONAL_BID_SECRET_KEY]); + + await joinNegativeInterestGroup( + test, buyer2, negativeInterestGroupName1, ADDITIONAL_BID_PUBLIC_KEY); + + await runAdditionalBidTest( + test, uuid, [buyer1, buyer2], auctionNonce, + additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid1, additionalBid2]), + /*highestScoringOtherBid=*/1.99, + /*winningAdditionalBidId=*/'planes'); +}, 'higher additional bid is filtered by negative targeting by two negative ' + + 'interest groups, but because of a joining origin mismatch, it still wins'); + +// Ensure that trusted seller signals are retrieved for additional bids. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const auctionNonce = await navigator.createAuctionNonce(); + const seller = SINGLE_SELLER_AUCTION_SELLER; + + const buyer = OTHER_ORIGIN1; + const additionalBid = additionalBidHelper.createAdditionalBid( + uuid, auctionNonce, seller, buyer, 'horses', 1.99); + + let renderURL = createRenderURL(uuid); + await runBasicFledgeTestExpectingWinner( + test, uuid, + { interestGroupBuyers: [buyer], + auctionNonce: auctionNonce, + additionalBids: additionalBidHelper.fetchAdditionalBids( + seller, [additionalBid]), + decisionLogicURL: createDecisionScriptURL( + uuid, + { scoreAd: + `if(!"${renderURL}" in trustedScoringSignals.renderURL) ` + + 'throw "missing trusted signals";'}), + trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL}); +}, 'trusted seller signals retrieved for additional bids'); diff --git a/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js b/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js index c78a27bb87..9b12d077ba 100644 --- a/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js @@ -6,7 +6,8 @@ // META: variant=?1-5 // META: variant=?6-10 // META: variant=?11-15 -// META: variant=?16-last +// META: variant=?16-20 +// META: variant=?21-last "use strict;" @@ -206,3 +207,23 @@ makeTest({ [{width: ' 100', height: '200.50px '}, {width: ' 70.00sh ', height: '80.50sw'}]} }); + +makeTest({ + name: 'AuctionConfig.reportingTimeout with positive within-cap value.', + fieldName: 'reportingTimeout', + fieldValue: 100, +}); + +makeTest({ + name: 'AuctionConfig.reportingTimeout above the cap value.', + fieldName: 'reportingTimeout', + fieldValue: 5000, + auctionConfigOverrides: {fieldValue: 1234567890} +}); + +makeTest({ + name: 'AuctionConfig.reportingTimeout not provided', + fieldName: 'reportingTimeout', + fieldValue: 50, + auctionConfigOverrides: {fieldValue: undefined} +});
\ No newline at end of file diff --git a/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js b/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js index 8fbdc95dfc..5fa4fa252f 100644 --- a/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js @@ -10,7 +10,9 @@ // META: variant=?21-25 // META: variant=?26-30 // META: variant=?31-35 -// META: variant=?36-last +// META: variant=?36-40 +// META: variant=?40-45 +// META: variant=?46-last "use strict;" @@ -86,6 +88,14 @@ const EXPECT_NO_WINNER = auctionResult => { assert_equals(auctionResult, null, 'Auction unexpected had a winner'); }; +// Expect a winner (FencedFrameConfig). +const EXPECT_WINNER = + auctionResult => { + assert_true( + auctionResult instanceof FencedFrameConfig, + 'Auction did not return expected FencedFrameConfig'); + } + // Expect an exception of the given type. const EXPECT_EXCEPTION = exceptionType => auctionResult => { assert_not_equals(auctionResult, null, "got null instead of expected error"); @@ -130,6 +140,50 @@ makeTest({ }); makeTest({ + name: 'valid trustedScoringSignalsURL', + expect: EXPECT_WINNER, + auctionConfigOverrides: + {trustedScoringSignalsURL: window.location.origin + '/resource.json'} +}); + +makeTest({ + name: 'trustedScoringSignalsURL should not have a fragment', + expect: EXPECT_EXCEPTION(TypeError), + auctionConfigOverrides: + {trustedScoringSignalsURL: window.location.origin + '/resource.json#foo'} +}); + +makeTest({ + name: 'trustedScoringSignalsURL with an empty fragment is not OK', + expect: EXPECT_EXCEPTION(TypeError), + auctionConfigOverrides: + {trustedScoringSignalsURL: window.location.origin + '/resource.json#'} +}); + +makeTest({ + name: 'trustedScoringSignalsURL should not have a query', + expect: EXPECT_EXCEPTION(TypeError), + auctionConfigOverrides: + {trustedScoringSignalsURL: window.location.origin + '/resource.json?foo'} +}); + +makeTest({ + name: 'trustedScoringSignalsURL with an empty query is not OK', + expect: EXPECT_EXCEPTION(TypeError), + auctionConfigOverrides: + {trustedScoringSignalsURL: window.location.origin + '/resource.json?'} +}); + +makeTest({ + name: 'trustedScoringSignalsURL should not have embedded credentials', + expect: EXPECT_EXCEPTION(TypeError), + auctionConfigOverrides: { + trustedScoringSignalsURL: (window.location.origin + '/resource.json') + .replace('https://', 'https://user:pass@') + } +}); + +makeTest({ name: 'trustedScoringSignalsURL is cross-origin with seller', expect: EXPECT_EXCEPTION(TypeError), auctionConfigOverrides: { trustedScoringSignalsURL: "https://example.com" }, @@ -420,9 +474,9 @@ subsetTest(promise_test, async test => { let bid = browserSignals.forDebuggingOnlyInCooldownOrLockout ? 1 : 2; return {bid: bid, render: '${renderURL}'};`, reportWin: ` - if (browserSignals.bid == 1) + if (browserSignals.bid === 1) sendReportTo('${bidderReportURL1}'); - if (browserSignals.bid == 2) + if (browserSignals.bid === 2) sendReportTo('${bidderReportURL2}');` }) @@ -443,9 +497,9 @@ subsetTest(promise_test, async test => { browserSignals.forDebuggingOnlyInCooldownOrLockout ? 1 : 2; return {desirability: desirability};`, reportResult: ` - if (browserSignals.desirability == 1) + if (browserSignals.desirability === 1) sendReportTo('${sellerReportURL1}'); - if (browserSignals.desirability == 2) + if (browserSignals.desirability === 2) sendReportTo('${sellerReportURL2}');` }) }; diff --git a/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js b/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js index 7e98570b9e..6b22585d57 100644 --- a/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js @@ -61,7 +61,7 @@ async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGr `// "status" is passed to the beacon URL, to be verified by waitForObservedRequests(). let status = "ok"; const componentAds = window.fence.getNestedConfigs() - if (componentAds.length != 40) + if (componentAds.length !== 40) status = "unexpected getNestedConfigs() length"; for (let i of ${JSON.stringify(componentAdsToLoad)}) { let fencedFrame = document.createElement("fencedframe"); @@ -144,7 +144,7 @@ subsetTest(promise_test, async test => { const nestedConfigsLength = window.fence.getNestedConfigs().length // "getNestedConfigs()" should return a list of 40 configs, to avoid leaking // whether there were any component URLs to the page. - if (nestedConfigsLength != 40) + if (nestedConfigsLength !== 40) status = "unexpected getNestedConfigs() length: " + nestedConfigsLength; window.fence.reportEvent({eventType: "beacon", eventData: status, diff --git a/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js b/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js index c70532024c..015c20a5c2 100644 --- a/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js @@ -685,7 +685,7 @@ subsetTest(promise_test, async test => { auctionConfig.componentAuctions[0].decisionLogicURL = createDecisionScriptURL( uuid, - { scoreAd: `if (browserSignals.renderURL != '${renderURL1}') + { scoreAd: `if (browserSignals.renderURL !== '${renderURL1}') throw 'Wrong ad';`, reportResult: `sendReportTo('${seller1ReportURL}');`} ); @@ -696,7 +696,7 @@ subsetTest(promise_test, async test => { decisionLogicURL: createDecisionScriptURL( uuid, { origin: OTHER_ORIGIN1, - scoreAd: `if (browserSignals.renderURL != '${renderURL2}') + scoreAd: `if (browserSignals.renderURL !== '${renderURL2}') throw 'Wrong ad';`, reportResult: `sendReportTo('${seller2ReportURL}');`} ) diff --git a/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js b/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js index a8cf93049f..eed74c522f 100644 --- a/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js @@ -356,7 +356,7 @@ subsetTest(promise_test, async test => { throw "Wrong origin: " + interestGroup.owner; if (!interestGroup.biddingLogicURL.startsWith("${bidderOrigin}")) throw "Wrong origin: " + interestGroup.biddingLogicURL; - if (interestGroup.ads[0].renderURL != "${renderURL}") + if (interestGroup.ads[0].renderURL !== "${renderURL}") throw "Wrong renderURL: " + interestGroup.ads[0].renderURL; if (browserSignals.seller !== "${sellerOrigin}") throw "Wrong origin: " + browserSignals.seller;`, diff --git a/testing/web-platform/tests/fledge/tentative/currency.https.window.js b/testing/web-platform/tests/fledge/tentative/currency.https.window.js index 9a33d12148..99943cecbf 100644 --- a/testing/web-platform/tests/fledge/tentative/currency.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/currency.https.window.js @@ -501,7 +501,7 @@ subsetTest(promise_test, async test => { topLevelSellerScriptParamsOverride: { scoreAd: ` // scoreAd sees what's actually passed in. - if (bid != 9) + if (bid !== 9) throw 'Wrong bid'; if (browserSignals.bidCurrency !== '???') throw 'Wrong currency';` diff --git a/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js index 8687e3f296..c7078ae08a 100644 --- a/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js @@ -939,7 +939,7 @@ subsetTest(promise_test, async test => { if (!deepEquals(Object.keys(instance.exports), ["increment"])) throw "Unexpected exports: " + JSON.stringify(instance.exports); - if (instance.exports.increment(1) != 2) + if (instance.exports.increment(1) !== 2) throw "Unexpected increment result: " + instance.exports.increment(1);` }) } }); diff --git a/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js b/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js index c7c7120240..db6ef2d35a 100644 --- a/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js @@ -259,7 +259,7 @@ subsetTest(promise_test, async test => { // reportResultSuccessCondition: `browserSignals.interestGroupName === undefined`, // reportWinSuccessCondition: - `browserSignals.interestGroupName === ''` + `browserSignals.interestGroupName === 'default name'` ); }, 'browserSignals.interestGroupName test.'); @@ -303,3 +303,53 @@ await runReportArgumentValidationTest( uuid ); }, 'browserSignals.madeHighestScoringOtherBid with other bid.'); + +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + await runReportTest( + test, uuid, + { reportResultSuccessCondition: + `browserSignals.reportingTimeout === undefined`, + reportResult: + `sendReportTo('${createSellerReportURL(uuid)}');`, + reportWinSuccessCondition: + 'browserSignals.reportingTimeout === 100', + reportWin: + `sendReportTo('${createBidderReportURL(uuid)}');` }, + // expectedReportURLs: + [createSellerReportURL(uuid), createBidderReportURL(uuid)], + // renderURLOverride + null, + // auctionConfigOverrides + {reportingTimeout: 100}); +}, 'browserSignals.reportingTimeout with custom value from auction config.'); + +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + await runReportTest( + test, uuid, + { reportResultSuccessCondition: + `browserSignals.reportingTimeout === undefined`, + reportResult: + `sendReportTo('${createSellerReportURL(uuid)}');`, + reportWinSuccessCondition: + 'browserSignals.reportingTimeout === 5000', + reportWin: + `sendReportTo('${createBidderReportURL(uuid)}');` }, + // expectedReportURLs: + [createSellerReportURL(uuid), createBidderReportURL(uuid)], + // renderURLOverride + null, + // auctionConfigOverrides + {reportingTimeout: 1234567890}); +}, 'browserSignals.reportingTimeout above the cap value.'); + +subsetTest(promise_test, async test => { + await runReportArgumentValidationTest( + test, + // reportResultSuccessCondition: + `browserSignals.reportingTimeout === undefined`, + // reportWinSuccessCondition: + `browserSignals.reportingTimeout === 50` + ); +}, 'browserSignals.reportingTimeout default value.'); diff --git a/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py b/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py index 060606b41d..721909a045 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py +++ b/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py @@ -13,6 +13,7 @@ with a value of b"?1"; this entrypoint otherwise returns a 400 response. import json import base64 +import fledge.tentative.resources.ed25519 as ed25519 import fledge.tentative.resources.fledge_http_server_util as fledge_http_server_util @@ -20,6 +21,57 @@ class BadRequestError(Exception): pass +def _generate_signature(message, base64_encoded_secret_key): + """Returns a signature entry for a signed additional bid. + + Args: + base64_encoded_secret_key: base64-encoded Ed25519 key with which to sign + the message. From this secret key, the public key can be deduced, which + becomes part of the signature entry. + message: The additional bid text (or other text if generating an invalid + signature) to sign. + """ + secret_key = base64.b64decode(base64_encoded_secret_key.encode("utf-8")) + public_key = ed25519.publickey_unsafe(secret_key) + signature = ed25519.signature_unsafe( + message.encode("utf-8"), secret_key, public_key) + return { + "key": base64.b64encode(public_key).decode("utf-8"), + "signature": base64.b64encode(signature).decode("utf-8") + } + + +def _sign_additional_bid(additional_bid_string, + secret_keys_for_valid_signatures, + secret_keys_for_invalid_signatures): + """Returns a signed additional bid given an additional bid and secret keys. + + Args: + additional_bid_string: string representation of the additional bid + secret_keys_for_valid_signatures: a list of strings, each a base64-encoded + Ed25519 secret key with which to sign the additional bid + secret_keys_for_invalid_signatures: a list of strings, each a base64-encoded + Ed25519 secret key with which to incorrectly sign the additional bid + """ + signatures = [] + signatures.extend( + _generate_signature(additional_bid_string, secret_key) + for secret_key in secret_keys_for_valid_signatures) + + # For invalid signatures, we use the correct secret key to sign a different + # message - the additional bid prepended by 'invalid' - so that the signature + # is a structually valid signature but can't be used to verify the additional + # bid. + signatures.extend( + _generate_signature("invalid" + additional_bid_string, secret_key) + for secret_key in secret_keys_for_invalid_signatures) + + return json.dumps({ + "bid": additional_bid_string, + "signatures": signatures + }) + + def main(request, response): try: if fledge_http_server_util.handle_cors_headers_and_preflight(request, response): @@ -34,14 +86,16 @@ def main(request, response): if not additional_bids: raise BadRequestError("Missing 'additionalBids' parameter") for additional_bid in json.loads(additional_bids): - additional_bid_string = json.dumps(additional_bid) + # Each additional bid may have associated testMetadata. Remove this from + # the additional bid and use it to adjust the behavior of this handler. + test_metadata = additional_bid.pop("testMetadata", {}) auction_nonce = additional_bid.get("auctionNonce", None) if not auction_nonce: raise BadRequestError("Additional bid missing required 'auctionNonce' field") - signed_additional_bid = json.dumps({ - "bid": additional_bid_string, - "signatures": [] - }) + signed_additional_bid = _sign_additional_bid( + json.dumps(additional_bid), + test_metadata.get("secretKeysForValidSignatures", []), + test_metadata.get("secretKeysForInvalidSignatures", [])) additional_bid_header_value = (auction_nonce.encode("utf-8") + b":" + base64.b64encode(signed_additional_bid.encode("utf-8"))) response.headers.append(b"Ad-Auction-Additional-Bid", additional_bid_header_value) diff --git a/testing/web-platform/tests/fledge/tentative/resources/ed25519.py b/testing/web-platform/tests/fledge/tentative/resources/ed25519.py new file mode 100644 index 0000000000..53e548ab8e --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/ed25519.py @@ -0,0 +1,289 @@ +# ed25519.py - Optimized version of the reference implementation of Ed25519 +# +# Written in 2011? by Daniel J. Bernstein <djb@cr.yp.to> +# 2013 by Donald Stufft <donald@stufft.io> +# 2013 by Alex Gaynor <alex.gaynor@gmail.com> +# 2013 by Greg Price <price@mit.edu> +# +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see +# <http://creativecommons.org/publicdomain/zero/1.0/>. +# +# Downloaded from https://raw.githubusercontent.com/pyca/ed25519/main/ed25519.py +# on April 1, 2024. + +""" +NB: This code is not safe for use with secret keys or secret data. +The only safe use of this code is for verifying signatures on public messages. + +Functions for computing the public key of a secret key and for signing +a message are included, namely publickey_unsafe and signature_unsafe, +for testing purposes only. + +The root of the problem is that Python's long-integer arithmetic is +not designed for use in cryptography. Specifically, it may take more +or less time to execute an operation depending on the values of the +inputs, and its memory access patterns may also depend on the inputs. +This opens it to timing and cache side-channel attacks which can +disclose data to an attacker. We rely on Python's long-integer +arithmetic, so we cannot handle secrets without risking their disclosure. +""" + +import hashlib + + +__version__ = "1.0.dev0" + + +b = 256 +q = 2**255 - 19 +l = 2**252 + 27742317777372353535851937790883648493 + + +def H(m): + return hashlib.sha512(m).digest() + + +def pow2(x, p): + """== pow(x, 2**p, q)""" + while p > 0: + x = x * x % q + p -= 1 + return x + + +def inv(z): + r"""$= z^{-1} \mod q$, for z != 0""" + # Adapted from curve25519_athlon.c in djb's Curve25519. + z2 = z * z % q # 2 + z9 = pow2(z2, 2) * z % q # 9 + z11 = z9 * z2 % q # 11 + z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0 + z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0 + z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ... + z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q + z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q + z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q + z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q + z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0 + return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2 + + +d = -121665 * inv(121666) % q +I = pow(2, (q - 1) // 4, q) + + +def xrecover(y): + xx = (y * y - 1) * inv(d * y * y + 1) + x = pow(xx, (q + 3) // 8, q) + + if (x * x - xx) % q != 0: + x = (x * I) % q + + if x % 2 != 0: + x = q - x + + return x + + +By = 4 * inv(5) +Bx = xrecover(By) +B = (Bx % q, By % q, 1, (Bx * By) % q) +ident = (0, 1, 1, 0) + + +def edwards_add(P, Q): + # This is formula sequence 'addition-add-2008-hwcd-3' from + # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + (x1, y1, z1, t1) = P + (x2, y2, z2, t2) = Q + + a = (y1 - x1) * (y2 - x2) % q + b = (y1 + x1) * (y2 + x2) % q + c = t1 * 2 * d * t2 % q + dd = z1 * 2 * z2 % q + e = b - a + f = dd - c + g = dd + c + h = b + a + x3 = e * f + y3 = g * h + t3 = e * h + z3 = f * g + + return (x3 % q, y3 % q, z3 % q, t3 % q) + + +def edwards_double(P): + # This is formula sequence 'dbl-2008-hwcd' from + # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + (x1, y1, z1, t1) = P + + a = x1 * x1 % q + b = y1 * y1 % q + c = 2 * z1 * z1 % q + # dd = -a + e = ((x1 + y1) * (x1 + y1) - a - b) % q + g = -a + b # dd + b + f = g - c + h = -a - b # dd - b + x3 = e * f + y3 = g * h + t3 = e * h + z3 = f * g + + return (x3 % q, y3 % q, z3 % q, t3 % q) + + +def scalarmult(P, e): + if e == 0: + return ident + Q = scalarmult(P, e // 2) + Q = edwards_double(Q) + if e & 1: + Q = edwards_add(Q, P) + return Q + + +# Bpow[i] == scalarmult(B, 2**i) +Bpow = [] + + +def make_Bpow(): + P = B + for i in range(253): + Bpow.append(P) + P = edwards_double(P) + + +make_Bpow() + + +def scalarmult_B(e): + """ + Implements scalarmult(B, e) more efficiently. + """ + # scalarmult(B, l) is the identity + e = e % l + P = ident + for i in range(253): + if e & 1: + P = edwards_add(P, Bpow[i]) + e = e // 2 + assert e == 0, e + return P + + +def encodeint(y): + bits = [(y >> i) & 1 for i in range(b)] + return bytes( + [sum([bits[i * 8 + j] << j for j in range(8)]) for i in range(b // 8)] + ) + + +def encodepoint(P): + (x, y, z, t) = P + zi = inv(z) + x = (x * zi) % q + y = (y * zi) % q + bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] + return bytes( + [sum([bits[i * 8 + j] << j for j in range(8)]) for i in range(b // 8)] + ) + + +def bit(h, i): + return (h[i // 8] >> (i % 8)) & 1 + + +def publickey_unsafe(sk): + """ + Not safe to use with secret keys or secret data. + + See module docstring. This function should be used for testing only. + """ + h = H(sk) + a = 2 ** (b - 2) + sum(2**i * bit(h, i) for i in range(3, b - 2)) + A = scalarmult_B(a) + return encodepoint(A) + + +def Hint(m): + h = H(m) + return sum(2**i * bit(h, i) for i in range(2 * b)) + + +def signature_unsafe(m, sk, pk): + """ + Not safe to use with secret keys or secret data. + + See module docstring. This function should be used for testing only. + """ + h = H(sk) + a = 2 ** (b - 2) + sum(2**i * bit(h, i) for i in range(3, b - 2)) + r = Hint(bytes([h[j] for j in range(b // 8, b // 4)]) + m) + R = scalarmult_B(r) + S = (r + Hint(encodepoint(R) + pk + m) * a) % l + return encodepoint(R) + encodeint(S) + + +def isoncurve(P): + (x, y, z, t) = P + return ( + z % q != 0 + and x * y % q == z * t % q + and (y * y - x * x - z * z - d * t * t) % q == 0 + ) + + +def decodeint(s): + return sum(2**i * bit(s, i) for i in range(0, b)) + + +def decodepoint(s): + y = sum(2**i * bit(s, i) for i in range(0, b - 1)) + x = xrecover(y) + if x & 1 != bit(s, b - 1): + x = q - x + P = (x, y, 1, (x * y) % q) + if not isoncurve(P): + raise ValueError("decoding point that is not on curve") + return P + + +class SignatureMismatch(Exception): + pass + + +def checkvalid(s, m, pk): + """ + Not safe to use when any argument is secret. + + See module docstring. This function should be used only for + verifying public signatures of public messages. + """ + if len(s) != b // 4: + raise ValueError("signature length is wrong") + + if len(pk) != b // 8: + raise ValueError("public-key length is wrong") + + R = decodepoint(s[: b // 8]) + A = decodepoint(pk) + S = decodeint(s[b // 8 : b // 4]) + h = Hint(encodepoint(R) + pk + m) + + (x1, y1, z1, t1) = P = scalarmult_B(S) + (x2, y2, z2, t2) = Q = edwards_add(R, scalarmult(A, h)) + + if ( + not isoncurve(P) + or not isoncurve(Q) + or (x1 * z2 - x2 * z1) % q != 0 + or (y1 * z2 - y2 * z1) % q != 0 + ): + raise SignatureMismatch("signature does not pass verification") diff --git a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js index 5819357e29..7be02e34ff 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js +++ b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js @@ -177,7 +177,7 @@ async function waitForObservedRequestsIgnoreDebugOnlyReports( function createBiddingScriptURL(params = {}) { let origin = params.origin ? params.origin : new URL(BASE_URL).origin; let url = new URL(`${origin}${RESOURCE_PATH}bidding-logic.sub.py`); - // These checks use "==" to ignore null and not provided arguments, while + // These checks use "!=" to ignore null and not provided arguments, while // treating '' as a valid argument. if (params.generateBid != null) url.searchParams.append('generateBid', params.generateBid); @@ -213,7 +213,7 @@ function createDecisionScriptURL(uuid, params = {}) { let origin = params.origin ? params.origin : new URL(BASE_URL).origin; let url = new URL(`${origin}${RESOURCE_PATH}decision-logic.sub.py`); url.searchParams.append('uuid', uuid); - // These checks use "==" to ignore null and not provided arguments, while + // These checks use "!=" to ignore null and not provided arguments, while // treating '' as a valid argument. if (params.scoreAd != null) url.searchParams.append('scoreAd', params.scoreAd); @@ -230,8 +230,8 @@ function createDecisionScriptURL(uuid, params = {}) { // be last. "signalsParams" also has no effect, but is used by // trusted-scoring-signals.py to affect the response. function createRenderURL(uuid, script, signalsParams, origin) { - // These checks use "==" to ignore null and not provided arguments, while - // treating '' as a valid argument. + // These checks use "==" and "!=" to ignore null and not provided + // arguments, while treating '' as a valid argument. if (origin == null) origin = new URL(BASE_URL).origin; let url = new URL(`${origin}${RESOURCE_PATH}fenced-frame.sub.py`); @@ -260,6 +260,15 @@ function createInterestGroupForOrigin(uuid, origin, }; } +// Waits for the join command to complete. Adds cleanup command to `test` to +// leave the interest group when the test completes. +async function joinInterestGroupWithoutDefaults(test, interestGroup, + durationSeconds = 60) { + await navigator.joinAdInterestGroup(interestGroup, durationSeconds); + test.add_cleanup( + async () => { await navigator.leaveAdInterestGroup(interestGroup); }); +} + // Joins an interest group that, by default, is owned by the current frame's // origin, is named DEFAULT_INTEREST_GROUP_NAME, has a bidding script that // issues a bid of 9 with a renderURL of "https://not.checked.test/${uuid}", @@ -271,12 +280,33 @@ function createInterestGroupForOrigin(uuid, origin, // interest group. async function joinInterestGroup(test, uuid, interestGroupOverrides = {}, durationSeconds = 60) { - let interestGroup = createInterestGroupForOrigin(uuid, window.location.origin, - interestGroupOverrides); - - await navigator.joinAdInterestGroup(interestGroup, durationSeconds); - test.add_cleanup( - async () => { await navigator.leaveAdInterestGroup(interestGroup) }); + await joinInterestGroupWithoutDefaults( + test, createInterestGroupForOrigin( + uuid, window.location.origin, interestGroupOverrides), + durationSeconds); +} + +// Joins a negative interest group with the specified owner, name, and +// additionalBidKey. Because these are the only valid fields for a negative +// interest groups, this function doesn't expose an 'overrides' parameter. +// Adds cleanup command to `test` to leave the interest group when the test +// completes. +async function joinNegativeInterestGroup( + test, owner, name, additionalBidKey) { + let interestGroup = { + owner: owner, + name: name, + additionalBidKey: additionalBidKey + }; + if (owner !== window.location.origin) { + let iframe = await createIframe(test, owner, 'join-ad-interest-group'); + await runInFrame( + test, iframe, + `await joinInterestGroupWithoutDefaults(` + + `test_instance, ${JSON.stringify(interestGroup)})`); + } else { + await joinInterestGroupWithoutDefaults(test_instance, interestGroup); + } } // Similar to joinInterestGroup, but leaves the interest group instead. @@ -487,6 +517,17 @@ async function runReportTest(test, uuid, codeToInsert, expectedReportURLs, await waitForObservedRequests(uuid, expectedReportURLs); } +// Helper function for running a standard test of the additional bid and +// negative targeting features. This helper verifies that the auction produces a +// winner. It takes the following arguments: +// - test/uuid: the test object and uuid from the test case (see generateUuid) +// - buyers: array of strings, each a domain for a buyer participating in this +// auction +// - actionNonce: string, the auction nonce for this auction, typically +// retrieved from a prior call to navigator.createAuctionNonce +// - highestScoringOtherBid: the amount of the second-highest bid, +// or zero if there's no second-highest bid +// - winningAdditionalBidId: the label of the winning bid async function runAdditionalBidTest(test, uuid, buyers, auctionNonce, additionalBidsPromise, highestScoringOtherBid, @@ -516,7 +557,7 @@ async function runInFrame(test, child_window, script, param) { let promise = new Promise(function(resolve, reject) { function WaitForMessage(event) { - if (event.data.messageUuid != messageUuid) + if (event.data.messageUuid !== messageUuid) return; receivedResponse = event.data; if (event.data.result === 'success') { @@ -548,7 +589,7 @@ async function createFrame(test, origin, is_iframe = true, permissions = null) { `${origin}${RESOURCE_PATH}subordinate-frame.sub.html?uuid=${frameUuid}`; let promise = new Promise(function(resolve, reject) { function WaitForMessage(event) { - if (event.data.messageUuid != frameUuid) + if (event.data.messageUuid !== frameUuid) return; if (event.data.result === 'load complete') { resolve(); @@ -662,79 +703,130 @@ function directFromSellerSignalsValidatorCode(uuid, expectedSellerSignals, return { // Seller worklets scoreAd: - `if (directFromSellerSignals === null || + `if (directFromSellerSignals == null || directFromSellerSignals.sellerSignals !== ${expectedSellerSignals} || directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} || - Object.keys(directFromSellerSignals).length != 2) { + Object.keys(directFromSellerSignals).length !== 2) { throw 'Failed to get expected directFromSellerSignals in scoreAd(): ' + JSON.stringify(directFromSellerSignals); }`, reportResultSuccessCondition: - `directFromSellerSignals !== null && + `directFromSellerSignals != null && directFromSellerSignals.sellerSignals === ${expectedSellerSignals} && directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} && - Object.keys(directFromSellerSignals).length == 2`, + Object.keys(directFromSellerSignals).length === 2`, reportResult: `sendReportTo("${createSellerReportURL(uuid)}");`, // Bidder worklets generateBid: - `if (directFromSellerSignals === null || + `if (directFromSellerSignals == null || directFromSellerSignals.perBuyerSignals !== ${expectedPerBuyerSignals} || directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} || - Object.keys(directFromSellerSignals).length != 2) { + Object.keys(directFromSellerSignals).length !== 2) { throw 'Failed to get expected directFromSellerSignals in generateBid(): ' + JSON.stringify(directFromSellerSignals); }`, reportWinSuccessCondition: - `directFromSellerSignals !== null && + `directFromSellerSignals != null && directFromSellerSignals.perBuyerSignals === ${expectedPerBuyerSignals} && directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} && - Object.keys(directFromSellerSignals).length == 2`, + Object.keys(directFromSellerSignals).length === 2`, reportWin: `sendReportTo("${createBidderReportURL(uuid)}");`, }; } -// Creates an additional bid with the given parameters. This additional bid -// specifies a biddingLogicURL that provides an implementation of -// reportAdditionalBidWin that triggers a sendReportTo() to the bidder report -// URL of the winning additional bid. Additional bids are described in more -// detail at -// https://github.com/WICG/turtledove/blob/main/FLEDGE.md#6-additional-bids. -function createAdditionalBid(uuid, auctionNonce, seller, buyer, interestGroupName, bidAmount, - additionalBidOverrides = {}) { - return { - interestGroup: { - name: interestGroupName, - biddingLogicURL: createBiddingScriptURL( - { - origin: buyer, - reportAdditionalBidWin: `sendReportTo("${createBidderReportURL(uuid, interestGroupName)}");` - }), - owner: buyer - }, - bid: { - ad: ['metadata'], - bid: bidAmount, - render: createRenderURL(uuid) - }, - auctionNonce: auctionNonce, - seller: seller, - ...additionalBidOverrides +let additionalBidHelper = function() { + // Creates an additional bid with the given parameters. This additional bid + // specifies a biddingLogicURL that provides an implementation of + // reportAdditionalBidWin that triggers a sendReportTo() to the bidder report + // URL of the winning additional bid. Additional bids are described in more + // detail at + // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#6-additional-bids. + function createAdditionalBid(uuid, auctionNonce, seller, buyer, interestGroupName, bidAmount, + additionalBidOverrides = {}) { + return { + interestGroup: { + name: interestGroupName, + biddingLogicURL: createBiddingScriptURL( + { + origin: buyer, + reportAdditionalBidWin: `sendReportTo("${createBidderReportURL(uuid, interestGroupName)}");` + }), + owner: buyer + }, + bid: { + ad: ['metadata'], + bid: bidAmount, + render: createRenderURL(uuid) + }, + auctionNonce: auctionNonce, + seller: seller, + ...additionalBidOverrides + }; } -} -// Fetch some number of additional bid from a seller and verify that the -// 'Ad-Auction-Additional-Bid' header is not visible in this JavaScript context. -// The `additionalBids` parameter is a list of additional bids. -async function fetchAdditionalBids(seller, additionalBids) { - const url = new URL(`${seller}${RESOURCE_PATH}additional-bids.py`); - url.searchParams.append('additionalBids', JSON.stringify(additionalBids)); - const response = await fetch(url.href, {adAuctionHeaders: true}); + // Gets the testMetadata for an additional bid, initializing it if needed. + function getAndMaybeInitializeTestMetadata(additionalBid) { + if (additionalBid.testMetadata === undefined) { + additionalBid.testMetadata = {}; + } + return additionalBid.testMetadata; + } - assert_equals(response.status, 200, 'Failed to fetch additional bid: ' + await response.text()); - assert_false( - response.headers.has('Ad-Auction-Additional-Bid'), - 'Header "Ad-Auction-Additional-Bid" should not be available in JavaScript context.'); -} + // Tells the additional bid endpoint to correctly sign the additional bid with + // the given secret keys before returning that as a signed additional bid. + function signWithSecretKeys(additionalBid, secretKeys) { + getAndMaybeInitializeTestMetadata(additionalBid). + secretKeysForValidSignatures = secretKeys; + } + + // Tells the additional bid endpoint to incorrectly sign the additional bid with + // the given secret keys before returning that as a signed additional bid. This + // is used for testing the behavior when the auction encounters an invalid + // signature. + function incorrectlySignWithSecretKeys(additionalBid, secretKeys) { + getAndMaybeInitializeTestMetadata(additionalBid). + secretKeysForInvalidSignatures = secretKeys; + } + + // Adds a single negative interest group to an additional bid, as described at: + // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups + function addNegativeInterestGroup(additionalBid, negativeInterestGroup) { + additionalBid["negativeInterestGroup"] = negativeInterestGroup; + } + + // Adds multiple negative interest groups to an additional bid, as described at: + // https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups + function addNegativeInterestGroups(additionalBid, negativeInterestGroups, + joiningOrigin) { + additionalBid["negativeInterestGroups"] = { + joiningOrigin: joiningOrigin, + interestGroupNames: negativeInterestGroups + }; + } + + // Fetch some number of additional bid from a seller and verify that the + // 'Ad-Auction-Additional-Bid' header is not visible in this JavaScript context. + // The `additionalBids` parameter is a list of additional bids. + async function fetchAdditionalBids(seller, additionalBids) { + const url = new URL(`${seller}${RESOURCE_PATH}additional-bids.py`); + url.searchParams.append('additionalBids', JSON.stringify(additionalBids)); + const response = await fetch(url.href, {adAuctionHeaders: true}); + + assert_equals(response.status, 200, 'Failed to fetch additional bid: ' + await response.text()); + assert_false( + response.headers.has('Ad-Auction-Additional-Bid'), + 'Header "Ad-Auction-Additional-Bid" should not be available in JavaScript context.'); + } + + return { + createAdditionalBid: createAdditionalBid, + signWithSecretKeys: signWithSecretKeys, + incorrectlySignWithSecretKeys: incorrectlySignWithSecretKeys, + addNegativeInterestGroup: addNegativeInterestGroup, + addNegativeInterestGroups: addNegativeInterestGroups, + fetchAdditionalBids: fetchAdditionalBids + }; +}(); diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py index 45bede2c45..f9ca9031f1 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py +++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py @@ -110,6 +110,8 @@ def main(request, response): value = request.GET.first(b"slotSize", b"not-found").decode("ASCII") elif key == "allSlotsRequestedSizes": value = request.GET.first(b"allSlotsRequestedSizes", b"not-found").decode("ASCII") + elif key == "url": + value = request.url responseBody["keys"][key] = value if "data-version" in interestGroupNames: diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py index eccef5e762..ce53e76295 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py +++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py @@ -122,6 +122,8 @@ def main(request, response): value = request.GET.first(b"hostname", b"not-found").decode("ASCII") elif signalsParam == "headers": value = fledge_http_server_util.headers_to_ascii(request.headers) + elif signalsParam == "url": + value = request.url if addValue: if urlList["type"] not in responseBody: responseBody[urlList["type"]] = {} diff --git a/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js b/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js index 2147a026ae..0bac1b99a9 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js +++ b/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js @@ -11,10 +11,10 @@ function deepEquals(a, b) { return a === b; let aKeys = Object.keys(a); - if (aKeys.length != Object.keys(b).length) + if (aKeys.length !== Object.keys(b).length) return false; for (let key of aKeys) { - if (a.hasOwnProperty(key) != b.hasOwnProperty(key) || + if (a.hasOwnProperty(key) !== b.hasOwnProperty(key) || !deepEquals(a[key], b[key])) { return false; } diff --git a/testing/web-platform/tests/fledge/tentative/tie.https.window.js b/testing/web-platform/tests/fledge/tentative/tie.https.window.js index 48d6e95e5c..c87d10f201 100644 --- a/testing/web-platform/tests/fledge/tentative/tie.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/tie.https.window.js @@ -98,7 +98,7 @@ promise_test(async test => { auctionConfigOverrides.decisionLogicURL = createDecisionScriptURL( uuid, - {scoreAd: `if (browserSignals.renderURL == "${winningAdURL}") + {scoreAd: `if (browserSignals.renderURL === "${winningAdURL}") return 0;`}); // Add an abort controller, so can cancel extra auctions. diff --git a/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js index 9799af6ac1..d0b9a82086 100644 --- a/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js @@ -16,7 +16,8 @@ // META: variant=?51-55 // META: variant=?56-60 // META: variant=?61-65 -// META: variant=?66-last +// META: variant=?66-70 +// META: variant=?71-last "use strict"; @@ -785,3 +786,156 @@ subsetTest(promise_test, async test => { auctionConfigOverrides, uuid); }, 'all-slots-requested-sizes trustedBiddingSignalsSlotSizeMode in a component auction'); + +///////////////////////////////////////////////////////////////////////////// +// maxTrustedBiddingSignalsURLLength tests +///////////////////////////////////////////////////////////////////////////// + +// Trusted bidding signals can be retrieved when `maxTrustedBiddingSignalsURLLength` is set to 0, +// which means infinite length limit. +// In the following three tests, the generated request URL contains approximately 166 characters. +// The target of the tests is primarily to make sure all the signals are fetched with the full URL. +subsetTest(promise_test, async test => { + const name = 'group'; + await runTrustedBiddingSignalsTest( + test, + // Check the URL length is within an approximate range to ensure the URL is not truncated. + ` trustedBiddingSignals["interest-group-names"] === '["${name}"]' && + trustedBiddingSignals["url"].length > 150 && + trustedBiddingSignals["url"].length < 180 `, + { + name: name, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 0 + }); +}, 'Trusted bidding signals request works with a URL length limit set to 0.'); + +// Trusted bidding signals can be retrieved when `maxTrustedBiddingSignalsURLLength` is set to +// a non-zero value smaller than the length of the request URL. It also tests that multiple +// bidding keys from the same interest group will not be separated even the full URL length is +// larger than the limit. +subsetTest(promise_test, async test => { + const name = 'group'; + await runTrustedBiddingSignalsTest( + test, + ` trustedBiddingSignals["interest-group-names"] === '["${name}"]' && + trustedBiddingSignals["url"].length > 150 && + trustedBiddingSignals["url"].length < 180 `, + { + name: name, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 1 + }); +}, 'Trusted bidding signals request works with a URL length limit smaller than the URL length.'); + +// Trusted bidding signals can be retrieved when `maxTrustedBiddingSignalsURLLength` is set to +// a value larger than the length of the request URL. +subsetTest(promise_test, async test => { + const name = 'group'; + await runTrustedBiddingSignalsTest( + test, + ` trustedBiddingSignals["interest-group-names"] === '["${name}"]' && + trustedBiddingSignals["url"].length < 180 `, + { + name: name, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 1000 + }); +}, 'Trusted bidding signals request works with a URL length limit larger than the URL length.'); + +// Test whether an oversized trusted bidding signals request URL, generated from two interest +// groups, will be split into two parts when `maxTrustedBiddingSignalsURLLength` is set to a +// value larger than a single URL length and smaller than the combined URL length. A request +// URL from a single interest group contains about 188 characters, while a request URL from +// two interest groups contains about 216 characters. Note that this test can only verifies +// the fetch status of the winner's trusted bidding signal, which is the second interest +// group. We consider the request to be split if the URL length check passes for the second +// interest group. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const name1 = 'extraordinarilyLongNameGroup1'; + const name2 = 'extraordinarilyLongNameGroup2'; + + await Promise.all( + [ joinInterestGroup( + test, uuid, + { + name: name1, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 200, + biddingLogicURL: createBiddingScriptURL( + { + // Return 0 as bid to force the second interest group to win. This interest group + // is considered as fetching trusted bidding signals by itself if the winner's + // URL length passes the limit check. + generateBid: + `return { bid: 0, render: interestGroup.ads[0].renderURL };` + }) + }), + joinInterestGroup( + test, uuid, + { + name: name2, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 200, + biddingLogicURL: createBiddingScriptURL( + { + generateBid: + `if (trustedBiddingSignals["interest-group-names"] !== '["${name2}"]' || + trustedBiddingSignals["url"].length > 200) { + throw "unexpected trustedBiddingSignals"; + } + return { bid: 10, render: interestGroup.ads[0].renderURL };`}) + }) + ] + ); + runBasicFledgeTestExpectingWinner(test, uuid); +}, 'Trusted bidding signals splits the request if the combined URL length exceeds the limit of regular value.'); + +// Test whether an oversized trusted bidding signals request URL, generated from two interest +// groups, will be split into two parts when `maxTrustedBiddingSignalsURLLength` is set to a +// value smaller than a single URL length. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const name1 = 'extraordinaryLongNameGroup1'; + const name2 = 'extraordinaryLongNameGroup2'; + + await Promise.all( + [ joinInterestGroup( + test, uuid, + { + name: name1, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 1, + biddingLogicURL: createBiddingScriptURL( + { + generateBid: + `return { bid: 0, render: interestGroup.ads[0].renderURL };` + }) + }), + joinInterestGroup( + test, uuid, + { + name: name2, + trustedBiddingSignalsKeys: ['interest-group-names', 'url'], + trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL, + maxTrustedBiddingSignalsURLLength: 1, + biddingLogicURL: createBiddingScriptURL( + { + generateBid: + `if (trustedBiddingSignals["interest-group-names"] !== '["${name2}"]' || + trustedBiddingSignals["url"].length > 200) { + throw "unexpected trustedBiddingSignals"; + } + return { bid: 10, render: interestGroup.ads[0].renderURL };`}) + }) + ] + ); + runBasicFledgeTestExpectingWinner(test, uuid); +}, 'Trusted bidding signals splits the request if the combined URL length exceeds the limit of small value.');
\ No newline at end of file diff --git a/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js index 4de5cfc0f3..105b09fb3b 100644 --- a/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js @@ -11,7 +11,8 @@ // META: variant=?26-30 // META: variant=?31-35 // META: variant=?36-40 -// META: variant=?41-last +// META: variant=?41-45 +// META: variant=?45-last "use strict"; @@ -510,3 +511,151 @@ subsetTest(promise_test, async test => { }) }); }, 'Component ads trusted scoring signals.'); + +///////////////////////////////////////////////////////////////////////////// +// maxTrustedBiddingSignalsURLLength tests +///////////////////////////////////////////////////////////////////////////// + +// Trusted scoring signals can be retrieved when `maxTrustedScoringSignalsURLLength` is set to 0. +// In the following three tests, the generated request URL contains approximately 294 characters. +// The target of the tests is primarily to make sure the signals were fetched with the full URL. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const renderURL = createRenderURL(uuid, /*script=*/null, 'url'); + const interestGroupOverrides = { ads: [{ renderURL: renderURL }] }; + const auctionConfigOverrides = { + trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL, + maxTrustedScoringSignalsURLLength: 0, + decisionLogicURL: + createDecisionScriptURL(uuid, { + // Check the URL length is within an approximate range to ensure the URL is not truncated. + scoreAd: + `if (trustedScoringSignals.renderURL["${renderURL}"].length < 280 || + trustedScoringSignals.renderURL["${renderURL}"].length > 300) + throw "error";` + }) + }; + + await joinGroupAndRunBasicFledgeTestExpectingWinner( + test, + { + uuid: uuid, + interestGroupOverrides: interestGroupOverrides, + auctionConfigOverrides: auctionConfigOverrides + }); +}, 'Trusted scoring signals request works with a URL length limit set to 0.'); + +// Trusted scoring signals can be retrieved when `maxTrustedScoringSignalsURLLength` is set to +// a non-zero value smaller than the length of the request URL. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const renderURL = createRenderURL(uuid, /*script=*/null, 'url'); + const interestGroupOverrides = { ads: [{ renderURL: renderURL }] }; + const auctionConfigOverrides = { + trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL, + maxTrustedScoringSignalsURLLength: 1, + decisionLogicURL: + createDecisionScriptURL(uuid, { + // Check the URL length is within an approximate range to ensure the URL is not truncated. + scoreAd: + `if (trustedScoringSignals.renderURL["${renderURL}"].length < 280 || + trustedScoringSignals.renderURL["${renderURL}"].length > 300) + throw "error";` + }) + }; + + await joinGroupAndRunBasicFledgeTestExpectingWinner( + test, + { + uuid: uuid, + interestGroupOverrides: interestGroupOverrides, + auctionConfigOverrides: auctionConfigOverrides + }); +}, 'Trusted scoring signals request works with a URL length limit smaller than the URL length.'); + +// Trusted scoring signals can be retrieved when `maxTrustedScoringSignalsURLLength` is set to +// a value larger than the length of the request URL. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const renderURL = createRenderURL(uuid, /*script=*/null, 'url'); + const interestGroupOverrides = { ads: [{ renderURL: renderURL }] }; + const auctionConfigOverrides = { + trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL, + maxTrustedScoringSignalsURLLength: 1000, + decisionLogicURL: + createDecisionScriptURL(uuid, { + scoreAd: `if (trustedScoringSignals.renderURL["${renderURL}"].length > 300) throw "error";` + }) + }; + + await joinGroupAndRunBasicFledgeTestExpectingWinner( + test, + { + uuid: uuid, + interestGroupOverrides: interestGroupOverrides, + auctionConfigOverrides: auctionConfigOverrides + }); +}, 'Trusted scoring signals request works with a URL length limit larger than the URL length.'); + +// Test whether an oversized trusted scoring signals request URL, generated from two interest +// groups, will be split into two parts when `maxTrustedScoringSignalsURLLength` is set to a +// value larger than a single URL length and smaller than the combined URL length. A request +// URL from a single interest group contains about 294 characters, while a request URL from +// two interest groups contains about 466 characters. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const renderURL1 = createRenderURL(uuid, /*script=*/null, 'url,group1'); + const renderURL2 = createRenderURL(uuid, /*script=*/null, 'url,group2'); + const auctionConfigOverrides = { + trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL, + maxTrustedScoringSignalsURLLength: 300, + decisionLogicURL: + createDecisionScriptURL(uuid, { + // This will make the auction reject `renderURL2`, and if `renderURL1` passes the URL + // length check, we consider `renderURL2` is fetched by itself in the trusted scoring + // signals request. + scoreAd: + `if (!trustedScoringSignals.renderURL.has("${renderURL1}") || + trustedScoringSignals.renderURL.has("${renderURL2}") || + trustedScoringSignals.renderURL["${renderURL1}"].length > 300) { + throw "error"; + }` + }) + }; + + await Promise.all( + [ joinInterestGroup(test, uuid, { name: 'group 1', ads: [{ renderURL: renderURL1 }] }), + joinInterestGroup(test, uuid, { name: 'group 2', ads: [{ renderURL: renderURL2 }] }) ] + ); + + runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides); +}, 'Trusted scoring signals splits the request if the combined URL length exceeds the limit of regular value.'); + +// Test whether an oversized trusted scoring signals request URL, generated from two interest +// groups, will be split into two parts when `maxTrustedScoringSignalsURLLength` is set to a +// value smaller than a single URL length. +subsetTest(promise_test, async test => { + const uuid = generateUuid(test); + const renderURL1 = createRenderURL(uuid, /*script=*/null, 'url,group1'); + const renderURL2 = createRenderURL(uuid, /*script=*/null, 'url,group2'); + const auctionConfigOverrides = { + trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL, + maxTrustedScoringSignalsURLLength: 1, + decisionLogicURL: + createDecisionScriptURL(uuid, { + scoreAd: + `if (!trustedScoringSignals.renderURL.has("${renderURL1}") || + trustedScoringSignals.renderURL.has("${renderURL2}") || + trustedScoringSignals.renderURL["${renderURL1}"].length > 300) { + throw "error"; + }` + }) + }; + + await Promise.all( + [ joinInterestGroup(test, uuid, { name: 'group 1', ads: [{ renderURL: renderURL1 }] }), + joinInterestGroup(test, uuid, { name: 'group 2', ads: [{ renderURL: renderURL2 }] }) ] + ); + + runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides); +}, 'Trusted scoring signals splits the request if the combined URL length exceeds the limit of small value.');
\ No newline at end of file |