diff --git a/api.bs b/api.bs index 3e8b23f..cce078c 100644 --- a/api.bs +++ b/api.bs @@ -1068,14 +1068,13 @@ For example, "`extra.example.com`" is parsed as "`example.com`". ## State For Privacy Budget Management ## {#privacy-state} -[=User agents=] maintain three pieces of state +[=User agents=] maintain several pieces of state that are used to manage the expenditure of [=privacy budgets=]: * The [=privacy budget store=] records the state of the per-[=site=] and per-[=epoch=] [=privacy budgets=]. It is updated by [=deduct privacy budget=]. - * The [=epoch start store=] records when each [=epoch=] starts for [=conversion sites=]. This store is initialized as a side effect @@ -1084,15 +1083,27 @@ that are used to manage the expenditure of [=privacy budgets=]: * A singleton [=last browsing history clear=] value that tracks when the browsing activity for a [=site=] was last cleared. +* The [=global privacy budget store=] records the state + of the per-[=epoch=] global [=privacy budget=] + that applies across all [=sites=]. + +* The [=impression site quota store=] records the state + of per-[=impression site=] and per-[=epoch=] quota [=privacy budgets=]. + +* The [=conversion site quota store=] records the state + of per-[=conversion site=] and per-[=epoch=] quota [=privacy budgets=]. + +* The [=user action context store=] records which [=sites=] + have accessed quota [=privacy budgets=] within the current [=user action context=]. +

Like the [=impression store=], -the [=privacy budget store=] does not use a [=storage key=]. +the [=privacy budget store=] and related stores do not use a [=storage key=]. These stores have some additional constraints on how information is cleared; see [[#clear-budget-store]] for details.

-The [=safety limits=] need to be described in more detail. Some references to clearing the [=impression store=] may need to be updated to refer to the [=privacy budget store=] as well. @@ -1121,14 +1132,21 @@ A privacy budget key is a [=tuple=] consisting of the following items + +

-To deduct privacy budget +To deduct privacy and safety budgets given a [=privacy budget key=] |key|, +a [=map=] |impressionsByImpSite| from [=impression sites=] to [=sets=] of [=impressions=], [[WEBIDL#idl-double|double]] |epsilon|, integer |value|, integer |maxValue|, and nullable integer |l1Norm|: +1. Let |epoch| be the [=epoch index=] component of |key|. + +1. Let |conversionSite| be the [=site=] component of |key|. + 1. If the [=privacy budget store=] does not [=map/contain=] |key|, [=map/set=] its value of |key| to be a [=user agent=]-defined value, plus 1000. @@ -1163,16 +1181,262 @@ is added to the aggregated histogram. 1. Let |deduction| be |deductionFp| * 1000000, rounded towards positive Infinity. -1. If |deduction| is greater than |currentValue|, - [=map/set|set=] the value of |key| in the [=privacy budget store=] to 0 - and return false. +1. Let |impSiteDeductions| be the result of [=compute impression site deductions|computing impression site deductions=] + with |impressionsByImpSite|, |value|, |maxValue|, |epsilon| + +

TODO: The [=compute impression site deductions=] function needs to still be defined. + +1. Check that sufficient budget exists in all relevant stores + before deducting from any of them. + This ensures atomicity: either all deductions succeed, or none occur. + + 1. If |deduction| is greater than |currentValue|, return false. + + 1. If the [=global privacy budget store=] does not [=map/contain=] |epoch|, + [=map/set=] its value to the [=global budget per epoch=]. + + 1. If |deduction| is greater than [=global privacy budget store=]\[|epoch|], + return false. -1. [=map/set|Set=] the value of |key| in the [=privacy budget store=] - to |currentValue| − |deduction| - and return true. + 1. Let |convQuotaKey| be a [=conversion site quota key=] + whose items are |epoch| and |conversionSite|. + + 1. If the [=conversion site quota store=] does not [=map/contain=] |convQuotaKey|, + [=map/set=] its value to the [=conversion site quota per epoch=]. + + 1. If |deduction| is greater than [=conversion site quota store=]\[|convQuotaKey|], + return false. + + 1. [=map/iterate|For each=] |impSite| → |siteDeduction| in |impSiteDeductions|: + + 1. Let |impQuotaKey| be an [=impression site quota key=] + whose items are |epoch| and |impSite|. + + 1. If the [=impression site quota store=] does not [=map/contain=] |impQuotaKey|, + [=map/set=] its value to the [=impression site quota per epoch=]. + + 1. If |siteDeduction| is greater than [=impression site quota store=]\[|impQuotaKey|], + return false. + +1. All budget checks passed; perform the deductions atomically. + + 1. [=map/set|Set=] the value of |key| in the [=privacy budget store=] + to |currentValue| − |deduction|. + + 1. Decrement [=global privacy budget store=]\[|epoch|] by |deduction|. + + 1. Decrement [=conversion site quota store=]\[|convQuotaKey|] by |deduction|. + + 1. [=map/iterate|For each=] |impSite| → |siteDeduction| in |impSiteDeductions|: + + 1. Let |impQuotaKey| be an [=impression site quota key=] + whose items are |epoch| and |impSite|. + + 1. Decrement [=impression site quota store=]\[|impQuotaKey|] by |siteDeduction|. + +1. Return true.

+ + + +### Global Privacy Budget Store ### {#s-global-privacy-budget-store} + +The global privacy budget store is a [=map=] whose keys are +[=epoch indices=] and whose values are [=32-bit unsigned integers=] +in units of [=microepsilons=]. + +The [=global privacy budget store=] enforces a single [=privacy budget=] +per [=epoch=] that applies across all [=sites=]. +This provides a [=safety limit=] against adversaries +that can correlate activity for the same person across multiple [=sites=]. + +

Unlike the per-[=site=] [=privacy budget store=], +the [=global privacy budget store=] is keyed only by [=epoch index=], +not by [=site=]. + + +### Impression Site Quota Store ### {#s-impression-site-quota-store} + +The impression site quota store is a [=map=] whose keys are +[=impression site quota keys=] and whose values are [=32-bit unsigned integers=] +in units of [=microepsilons=]. + +An impression site quota key is a [=tuple=] consisting of the following items: + +

+: epoch index +:: An [=epoch index=] +: impression site +:: An [=impression site=] + +
+ +The [=impression site quota store=] limits the amount of "stock" +(privacy budget related to [=impressions=]) +that any single [=impression site=] can contribute in an [=epoch=]. +This prevents a single [=impression site=] +from enabling excessive budget +that could be maliciously triggered. + + +### Conversion Site Quota Store ### {#s-conversion-site-quota-store} + +The conversion site quota store is a [=map=] whose keys are +[=conversion site quota keys=] and whose values are [=32-bit unsigned integers=] +in units of [=microepsilons=]. + +A conversion site quota key is a [=tuple=] consisting of the following items: + +
+: epoch index +:: An [=epoch index=] +: conversion site +:: A [=conversion site=] + +
+ +The [=conversion site quota store=] limits the amount of "flow" +(privacy budget consumed by reports) +that any single [=conversion site=] can trigger in an [=epoch=]. +This constrains the budget that can be drawn by a [=conversion site=], +limiting its ability to rapidly deplete the [=global privacy budget=]. + + +### User Action Context Store ### {#s-user-action-context-store} + +The user action context store is a [=map=] keyed by [=user action contexts=] +and containing values that are [=user action context entries=]. + +A user action context is an identifier +for a sequence of API invocations +that are associated with a single intentional user action, +such as a navigation or click. + +A user action context entry is a [=struct=] with the following fields: + +
+: Allowed Impression Sites +:: A [=set=] of [=impression sites=] that have been permitted + to save [=impressions=] within this [=user action context=]. +: Impression Site Counter +:: A non-negative integer representing the remaining number of + new [=impression sites=] that may save [=impressions=] + within this [=user action context=]. + Initialized to an [=implementation-defined=] impression site count cap. +: Allowed Conversion Sites +:: A [=map=] keyed by [=epoch index=] and containing values that are [=sets=] + of [=conversion sites=] that have been permitted to measure [=conversions=] + within this [=user action context=] for that [=epoch=]. +: Conversion Site Counters +:: A [=map=] keyed by [=epoch index=] and containing values that are + non-negative integers representing the remaining number of + new [=conversion sites=] that may measure [=conversions=] + within this [=user action context=] for that [=epoch=]. + Each counter is initialized to an [=implementation-defined=] conversion site count cap. +
+ +The [=user action context store=] tracks which [=sites=] +have accessed quota [=privacy budgets=] +(either [=impression site quota store|impression site quotas=] +or [=conversion site quota store|conversion site quotas=]) +within the current [=user action context=]. +This enables enforcement of site count caps, +limiting the number of distinct sites that can participate +in attribution within a single user action. + +

A [=user action context=] typically corresponds to +a top-level navigation or other substantial user interaction. +[=User agents=] determine when a new [=user action context=] begins +based on their understanding of intentional user actions. + +

+To get the current user action context, +returning a [=user action context=]: + +1. If the [=user agent=] has an active [=user action context=] + associated with the current execution context, return it. + +1. Otherwise, create a new [=user action context=] identifier, + create a new [=user action context entry=] with: + * [=user action context entry/Allowed Impression Sites=] set to an empty [=set=], + * [=user action context entry/Impression Site Counter=] set to the [=impression site count cap=], + * [=user action context entry/Allowed Conversion Sites=] set to an empty [=map=], + * [=user action context entry/Conversion Site Counters=] set to an empty [=map=], + + add the identifier and entry to the [=user action context store=], + and return the identifier. + +

The [=user agent=] determines when [=user action contexts=] expire +and are removed from the [=user action context store=]. +Contexts typically expire after some period of inactivity +or when a new top-level navigation occurs. + +

+ +
+To check impression site allowance, +given an [=impression site=] |impSite| +and a [=user action context=] |uaContext|, +returning a [=boolean=]: + +1. Let |entry| be the result of [=map/get|getting=] |uaContext| + from the [=user action context store=]. + +1. If |entry|'s [=user action context entry/Allowed Impression Sites=] + [=set/contains=] |impSite|, return true. + +1. If |entry|'s [=user action context entry/Impression Site Counter=] is 0, + return false. + +1. [=set/Append=] |impSite| to |entry|'s + [=user action context entry/Allowed Impression Sites=]. + +1. Decrement |entry|'s [=user action context entry/Impression Site Counter=] by 1. + +1. Return true. + +
+ +
+To check conversion site allowance, +given a [=conversion site=] |convSite|, +an [=epoch index=] |epoch|, +and a [=user action context=] |uaContext|, +returning a [=boolean=]: + +1. Let |entry| be the result of [=map/get|getting=] |uaContext| + from the [=user action context store=]. + +1. If |entry|'s [=user action context entry/Conversion Site Counters=] + does not [=map/contain=] |epoch|: + + 1. [=map/Set=] |entry|'s [=user action context entry/Conversion Site Counters=]\[|epoch|] + to the [=conversion site count cap=]. + + 1. [=map/Set=] |entry|'s [=user action context entry/Allowed Conversion Sites=]\[|epoch|] + to an empty [=set=]. + +1. Let |allowedSites| be the result of [=map/get|getting=] |epoch| + from |entry|'s [=user action context entry/Allowed Conversion Sites=]. + +1. If |allowedSites| [=set/contains=] |convSite|, return true. + +1. Let |counter| be the result of [=map/get|getting=] |epoch| + from |entry|'s [=user action context entry/Conversion Site Counters=]. + +1. If |counter| is 0, return false. + +1. [=set/Append=] |convSite| to |allowedSites|. + +1. Decrement |entry|'s [=user action context entry/Conversion Site Counters=]\[|epoch|] by 1. + +1. Return true. + +
+ + ### Epoch Start Store ### {#s-epoch-start} An [=epoch=] starts at a randomly-selected time @@ -1248,6 +1512,33 @@ returning an [=epoch index=]: +### Safety Limits Configuration ### {#safety-limits-configuration} + +[=User agents=] configure [=safety limits=] by defining the following values: + +* Global budget per epochglobal): + The maximum privacy budget available across all [=sites=] per [=epoch=], + specified in [=microepsilons=]. + +* Impression site quota per epochimp-quota): + The maximum privacy budget that a single [=impression site=] + can enable to be consumed from the [=global privacy budget=] per [=epoch=], + specified in [=microepsilons=]. + +* Conversion site quota per epochconv-quota): + The maximum privacy budget that a single [=conversion site=] + can consume from the [=global privacy budget=] per [=epoch=], + specified in [=microepsilons=]. + +* Quota count cap (kquota-count): + The maximum number of distinct [=sites=] + that can create new quota budgets + within a single [=user action context=]. + +

Typical values might be: +TODO + + ### Last Browsing History Clear Time ### {#last-clear} The last browsing history clear is a [=moment=] @@ -1367,6 +1658,10 @@ and a [=moment=] |now|: 1. [=map/clear|Clear=] the [=epoch start store=]. +

TODO: Define how to clear [=safety limits=] stores: + [=global privacy budget store=], [=impression site quota store=], + [=conversion site quota store=], and [=user action context store=]. + 1. If |sites| [=set/is empty|is not empty=]: 1. [=set/iterate|For each=] |impression| in the [=impression store=], @@ -1411,6 +1706,7 @@ The saveImpression(|options|) method steps are 1. otherwise, the result of [=obtain a site|obtaining a site=] from |settings|' [=environment settings object/origin=]. + 1. Let |uaContext| be the [=current user action context=]. 1. Validate the page-supplied API inputs: 1. If |options|.{{AttributionImpressionOptions/histogramIndex}} is greater than or equal to the [=implementation-defined=] [=maximum histogram size=], @@ -1457,8 +1753,11 @@ The saveImpression(|options|) method steps are :: |options|.{{AttributionImpressionOptions/histogramIndex}} : [=impression/Priority=] :: |options|.{{AttributionImpressionOptions/priority}} - 1. If the Attribution API is [[#opt-out|enabled]], - save |impression| to the [=impression store=]. + 1. If the Attribution API is [[#opt-out|enabled]]: + 1. Let |allowed| be the result of [=check impression site allowance|checking impression site allowance=] + given |site| and |uaContext|. + 1. If |allowed| is true, save |impression| to the [=impression store=]. + 1. Let |result| be a new {{AttributionImpressionResult}}. 1. Return [=a promise resolved with=] |result| in |realm|. @@ -1496,6 +1795,10 @@ The measureConversion(|options|) method steps 1. otherwise, the result of [=obtain a site|obtaining a site=] from |settings|' [=environment settings object/origin=]. + 1. Let |uaContext| be the [=current user action context=]. + +

The [=user agent=] determines when a new [=user action context=] begins, + typically corresponding to a top-level navigation or other substantial user interaction. 1. Let |validatedOptions| be the result of [=validate AttributionConversionOptions|validating=] |options|, returning [=a promise rejected with=] any thrown reason. @@ -1505,7 +1808,7 @@ The measureConversion(|options|) method steps |validatedOptions|' [=validated conversion options/histogram size=]. 1. If the Attribution API is [[#opt-out|enabled]], set |report| to the result of [=do attribution and fill a histogram=] with |validatedOptions|, - |topLevelSite|, |intermediarySite|, and |now|. + |topLevelSite|, |intermediarySite|, |uaContext|, and |now|. 1. Let |aggregationService| be |validatedOptions|'s [=validated conversion options/aggregation service=]. 1. Switch on the value of |aggregationService|.{{AttributionAggregationService/protocol}}:

@@ -1628,6 +1931,7 @@ To do attribution and fill a histogram, given [=validated conversion options=] |options|, [=site=] |topLevelSite|, [=site=] or `undefined` |intermediarySite|, + [=user action context=] |uaContext|, and [=moment=] |now|: 1. Let |matchedImpressions| be an [=set/is empty|empty=] [=set=]. @@ -1648,7 +1952,6 @@ To do attribution and fill a histogram, given with |options|, |topLevelSite|, |intermediarySite|, |currentEpoch|, and |now|. 1. If |singleEpoch| is false: - 1. For each |epoch| from |startEpoch| to |currentEpoch|, inclusive: 1. Let |impressions| be the result of invoking [=common matching logic=] @@ -1656,20 +1959,39 @@ To do attribution and fill a histogram, given 1. If |impressions| [=set/is empty|is not empty=]: + 1. Let |quotaCountOk| be the result of invoking [=check conversion site allowance=] + with |topLevelSite|, |epoch|, and |uaContext|. + + 1. Let |impressionsByImpSite| be a new [=map=]. + + 1. [=set/iterate|For each=] |impression| in |impressions|: + + 1. Let |impSite| be |impression|'s [=impression/impression site=]. + + 1. If |impressionsByImpSite| does not [=map/contain=] |impSite|, + [=map/set=] |impressionsByImpSite|\[|impSite|] to an empty [=set=]. + + 1. [=set/Append=] |impression| to |impressionsByImpSite|\[|impSite|]. + 1. Let |key| be a [=privacy budget key=] whose items are |epoch| and |topLevelSite|. - 1. Let |budgetOk| be the result of invoking [=deduct privacy budget=] - with |key|, |options|' [=validated conversion options/epsilon=], + 1. Let |budgetAndSafetyOk| be the result of invoking [=deduct privacy and safety budgets=] + with |key|, |impressionsByImpSite|, + |options|' [=validated conversion options/epsilon=], |options|' [=validated conversion options/value=], |options|'s [=validated conversion options/max value=], and null. - 1. If |budgetOk| is true, [=set/extend=] |matchedImpressions| with |impressions|. + 1. If |quotaCountOk| is true and |budgetAndSafetyOk| is true, + [=set/extend=] |matchedImpressions| with |impressions|. + + 1. If |matchedImpressions| [=set/is empty=], return the result of invoking [=create an all-zero histogram=] with |options|' [=validated conversion options/histogram size=]. + 1. Set |histogram| to the result of [=fill a histogram with last-n-touch attribution=] with |matchedImpressions|, |options|' [=validated conversion options/histogram size=], |options|' [=validated conversion options/value=], and @@ -1680,15 +2002,27 @@ To do attribution and fill a histogram, given 1. [=Assert=]: |l1Norm| is less than or equal to |options|' [=validated conversion options/value=]. + 1. Let |impressionsByImpSite| be a new [=map=]. + + 1. [=set/iterate|For each=] |impression| in |matchedImpressions|: + + 1. Let |impSite| be |impression|'s [=impression/impression site=]. + + 1. If |impressionsByImpSite| does not [=map/contain=] |impSite|, + [=map/set=] |impressionsByImpSite|\[|impSite|] to an empty [=set=]. + + 1. [=set/Append=] |impression| to |impressionsByImpSite|\[|impSite|]. + 1. Let |key| be a [=privacy budget key=] whose items are |currentEpoch| and |topLevelSite|. - 1. Let |budgetOk| be the result of [=deduct privacy budget=] - with |key|, |options|' [=validated conversion options/epsilon=], + 1. Let |budgetAndSafetyOk| be the result of [=deduct privacy and safety budgets=] + with |key|, |impressionsByImpSite|, + |options|' [=validated conversion options/epsilon=], |options|' [=validated conversion options/value=] |options|'s [=validated conversion options/max value=], and |l1Norm|. - 1. If |budgetOk| is false, set |histogram| to the result of invoking + 1. If |budgetAndSafetyOk| is false, set |histogram| to the result of invoking [=create an all-zero histogram=] with |options|' [=validated conversion options/histogram size=]. 1. Return |histogram|.