diff --git a/api.bs b/api.bs index cafd900b..36bb4aab 100644 --- a/api.bs +++ b/api.bs @@ -1075,13 +1075,11 @@ 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=]. @@ -1091,15 +1089,25 @@ 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 [=privacy budget store=], [=global privacy budget store=], + and [=impression site quota store=] are updated by [=deduct privacy and safety budgets=]. + +
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. @@ -1128,35 +1136,30 @@ A privacy budget key is a [=tuple=] consisting of the following items + +
The addition of 1000 to this value - ensures that the rounding errors added by this algorithm do not cause - the budget to be exceeded unnecessarily after multiple invocations. - The privacy loss from the additional one-thousandth of an epsilon is trivial. +1. Let |l1NormSensitivity| be |l1Norm| if |isSingleEpoch|, 2 * |value| otherwise. -1. Let |currentValue| be the result of [=map/get|getting the value=] of |key| - in the [=privacy budget store=]. - -1. Let |sensitivity| be |l1Norm| if |l1Norm| is non-null, 2 * |value| otherwise. +1. Let |valueSensitivity| be 2 * |value|. 1. Let |noiseScale| be 2 * |maxValue| / |epsilon|. -1. Let |deductionFp| be |sensitivity| / |noiseScale|. +1. Let |l1NormDeductionFp| be |l1NormSensitivity| / |noiseScale|. + +1. Let |valueDeductionFp| be |valueSensitivity| / |noiseScale|. -
Single epoch attributions — - the only case that |l1Norm| is non-null — +
Single epoch attributions do not cause any cascading effects across epochs. Attribution that involves multiple epochs consumes double the budget because of the potential for one change to affect attribution across epochs. @@ -1164,22 +1167,198 @@ nullable integer |l1Norm|: proportional to |maxValue| / |epsilon| is added to the aggregated histogram. -1. If |deductionFp| is negative or greater than [=maximum epsilon=], - [=map/set|set=] the value of |key| in the [=privacy budget store=] to 0 - and return false. +1. Let |l1Normdeduction| be |l1NormDeductionFp| * 1000000, rounded towards positive Infinity. + +1. Let |valueDeduction| be |valueDeductionFp| * 1000000, rounded towards positive Infinity. + +
One [=epoch=] (which is the epoch of the conversion site) can overlap with multiple [=safety limit epochs=] used by the [=global privacy budget store|global budget=] + and [=impression site quota store|impression site quotas=]. We will look at each impression individually and map it to the [=safety limit epoch=] it is in to deduct from the global budget + and impression site quota if those have not already deducted from in this call to [=measure a conversion=]. + +
TODO: Additional work to specify +how locking is performed to ensure atomicity across checks and deductions, but lock should start about here. + +1. If the result of invoking [=check for available privacy budget|checking for available privacy budget=] + with |key|, |l1Normdeduction|, |valueDeduction|, + |impressions|, and |isSingleEpoch|, + is false, return false. + +1. All budget checks passed, so perform the deductions atomically: + + 1. If the [=privacy budget store=] does not [=map/contain=] |key|, + [=map/set=] its value to the [=per-site privacy budget=]. + + 1. Let |currentValue| be the result of [=map/get|getting the value=] of |key| + in the [=privacy budget store=]. + + 1. If |isSingleEpoch| let |deduction| be |l1Normdeduction| else let |deduction| be |valueDeduction| + + 1. [=map/set|Set=] the value of |key| in the [=privacy budget store=] + to |currentValue| − |deduction|. + + 1. [=list/iterate|For each=] |impression| in |impressions|: + + 1. Let |impressionTime| be |impression|'s [=impression/timestamp=]. + + 1. Let |impressionSite| be |impression|'s [=impression/impression site=]. + + 1. Let |safetyLimitEpoch| be the result of [=get the current safety limit epoch=] + with |impressionTime|. + + 1. Let |impressionQuotaKey| be an [=impression site quota key=] + whose items are |safetyLimitEpoch| and |impressionSite|. + + 1. If the [=impression site quota store=] does not [=map/contain=] |impressionQuotaKey|, + [=map/set=] its value to the [=impression site quota per epoch=]. + + 1. If |deductedImpressionQuotas| does not [=map/contain=] |impressionQuotaKey|, + [=set/extend=] |deductedImpressionQuotas| with |impressionQuotaKey| + and decrement [=impression site quota store=]\[|impressionQuotaKey|] by |valueDeduction|. + + 1. If |deductedGlobalBudgets| does not [=map/contain=] |safetyLimitEpoch|, + [=set/extend=] |deductedGlobalBudgets| with |safetyLimitEpoch| + and decrement [=global privacy budget store=]\[|safetyLimitEpoch|] by |valueDeduction|. + +1. Return true. + +
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: + +
This approach allows a single user action +to enable multiple API invocations within the same session, +while only making the API available to one site +per activation. + +
TODO (issue https://github.com/w3c/attribution/issues/367): Define how to clear [=safety limits=] stores: + [=global privacy budget store=] and [=impression site quota store=]. + 1. If |sites| [=set/is empty|is not empty=]: 1. [=set/iterate|For each=] |impression| in the [=impression store=], @@ -1417,6 +1632,9 @@ and [=implicit API inputs=] |implicitInputs|: 1. If |document| is not [=allowed to use=] the [=policy-controlled feature=] named "{{PermissionPolicy/save-impression}}", return [=a promise rejected with=] a {{"NotAllowedError"}} {{DOMException}} in |realm|. +1. Let |window| be |document|'s [=relevant global object=]. +1. [=check attribution API activation|Check attribution API activation=] + given |window|, returning [=a promise rejected with=] any thrown reason. 1. Validate the page-supplied API inputs: 1. If |options|.{{AttributionImpressionOptions/histogramIndex}} is greater than or equal to the [=implementation-defined=] [=maximum histogram size=], @@ -1466,7 +1684,8 @@ and [=implicit API inputs=] |implicitInputs|: : [=impression/Priority=] :: |options|.{{AttributionImpressionOptions/priority}} 1. If the Attribution API is [[#opt-out|enabled]], - save |impression| to the [=impression store=]. + save |impression| to the [=impression store=]. + 1. Let |result| be a new {{AttributionImpressionResult}}. 1. Return [=a promise resolved with=] |result| in |realm|. @@ -1543,6 +1762,9 @@ and [=implicit API inputs=] |implicitInputs|: 1. If |document| is not [=allowed to use=] the [=policy-controlled feature=] named "{{PermissionPolicy/measure-conversion}}", return [=a promise rejected with=] a {{"NotAllowedError"}} {{DOMException}} in |realm|. +1. Let |window| be |document|'s [=relevant global object=]. +1. [=check attribution API activation|Check attribution API activation=] + given |window|, returning [=a promise rejected with=] any thrown reason. 1. Let |validatedOptions| be the result of [=validate AttributionConversionOptions|validating=] |options|, returning [=a promise rejected with=] any thrown reason. @@ -1550,8 +1772,9 @@ and [=implicit API inputs=] |implicitInputs|: 1. Run the following steps [=in parallel=]: 1. Let |report| be the result of invoking [=create an all-zero histogram=] with |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|, + 1. If the Attribution API is [[#opt-out|enabled]], + set |report| to the result of [=do attribution and fill a histogram=] with + |validatedOptions|, |implicitInputs|' [=implicit API inputs/top-level site=], |implicitInputs|' [=implicit API inputs/intermediary site=], and |implicitInputs|' [=implicit API inputs/timestamp=]. @@ -1683,8 +1906,8 @@ To validate {{AttributionConversionOptions}} |options|: To do attribution and fill a histogram, given [=validated conversion options=] |options|, [=site=] |topLevelSite|, - [=site=] or `undefined` |intermediarySite|, - and [=moment=] |now|: + [=site=] or `undefined` |intermediarySite|, and + [=moment=] |now|: 1. Let |matchedImpressions| be an [=set/is empty|empty=] [=set=]. @@ -1697,30 +1920,55 @@ To do attribution and fill a histogram, given 1. Let |earliestEpoch| be the result of calling [=get the current epoch=], passing |topLevelSite| and (|now| − |options|' [=validated conversion options/lookback=]). -1. Let |singleEpoch| be true if |currentEpoch| is equal to |earliestEpoch|, false otherwise. +1. Let |isSingleEpoch| be true if |currentEpoch| is equal to |earliestEpoch|, false otherwise. -1. If |singleEpoch| is true: +1. If |isSingleEpoch| is true: 1. Set |matchedImpressions| to the result of invoking [=common matching logic=] with |options|, |topLevelSite|, |intermediarySite|, |currentEpoch|, and |now|. -1. If |singleEpoch| is false: + 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 + |options|' [=validated conversion options/credit=]. + + 1. Let |l1Norm| be the sum of the [=list/items=] in |histogram|. + + 1. [=Assert=]: |l1Norm| is less than or equal to |options|' [=validated conversion options/value=]. + +1. Let |deductedImpressionQuotas| be a new [=set=]. + +1. Let |deductedGlobalBudgets| be a new [=set=]. + +
The |deductedImpressionQuotas| and |deductedGlobalBudgets| sets are initialized outside the +loop over epochs so that [=safety limit epochs=] which overlap two per-site [=epochs=] will not be have +their safety limits deducted from more than once. - 1. For each |epoch| from |startEpoch| to |currentEpoch|, inclusive: +1. For each |epoch| from |currentEpoch| to |startEpoch|, inclusive: - 1. Let |impressions| be the result of invoking [=common matching logic=] - with |options|, |topLevelSite|, |intermediarySite|, |epoch|, and |now|. +
Epochs are processed sequentially starting from the most recent epoch + and going back in time as safety limit deductions can depleate from same safety limits as + the next epoch will need to check. - 1. If |impressions| [=set/is empty|is not empty=]: + 1. Let |impressions| be the result of invoking [=common matching logic=] + with |options|, |topLevelSite|, |intermediarySite|, |epoch|, and |now|. - 1. Let |key| be a [=privacy budget key=] whose items are |epoch| and |topLevelSite|. + 1. If |impressions| [=set/is empty|is not empty=]: - 1. Let |budgetOk| be the result of invoking [=deduct privacy budget=] - with |key|, |options|' [=validated conversion options/epsilon=], - |options|' [=validated conversion options/value=], - |options|'s [=validated conversion options/max value=], - and null. + 1. Let |key| be a [=privacy budget key=] whose items are |epoch| and |topLevelSite|. - 1. If |budgetOk| is true, [=set/extend=] |matchedImpressions| with |impressions|. + 1. Let |budgetAndSafetyOk| be the result of invoking [=deduct privacy and safety budgets=] + with |key|, |impressions|, + |options|' [=validated conversion options/epsilon=], + |options|' [=validated conversion options/value=], + |options|'s [=validated conversion options/max value=], + |isSingleEpoch|, |l1Norm|, |deductedImpressionQuotas| and |deductedGlobalBudgets|. + + 1. If |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 @@ -1731,22 +1979,6 @@ To do attribution and fill a histogram, given |options|' [=validated conversion options/value=], and |options|' [=validated conversion options/credit=]. -1. If |singleEpoch| is true: - 1. Let |l1Norm| be the sum of the [=list/items=] in |histogram|. - - 1. [=Assert=]: |l1Norm| is less than or equal to |options|' [=validated conversion options/value=]. - - 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=], - |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 - [=create an all-zero histogram=] with |options|' [=validated conversion options/histogram size=]. - 1. Return |histogram|. @@ -1939,9 +2171,34 @@ will be a fixed value that is determined by the choice of technology used by the [=aggregation service=]; it is not [=implementation-defined=]. -Deciding on a value for differential privacy parameters +### Privacy and Safety Limit Parameter Configuration ### {#safety-limits-configuration} + +
Deciding on a value for differential privacy parameters is hard and therefore TBD. +[=User agents=] configure the [=privacy budget=] and [=safety limits=] by defining values +for the following: + +* The per-site privacy budget (εsite) within the range of: TBD + +* The global privacy budget per epoch (εglobal) which is + the maximum privacy budget available across all [=sites=] per [=epoch=], + specified in [=microepsilons=]. + Implementations [=must=] set this value as a multiple of + the per-[=site=] [=privacy budget=] per [=epoch=]. + +* The impression site quota per epoch (εimp-quota) which is + the maximum privacy budget that a single [=impression site=] + can enable to be consumed from the [=global privacy budget store|global privacy budget=] per [=epoch=], + specified in [=microepsilons=]. + Implementations [=must=] set this value as a multiple of + the per-[=site=] [=privacy budget=] per [=epoch=]. + +
Setting [=safety limits=] as multiples of the per-[=site=] budget +ensures that exploiting shared limits requires coordination by many sites, +making them primarily useful as a means of protecting against abuse +rather than as a primary privacy mechanism. + ## User Control and Visibility ## {#user-control} @@ -2715,11 +2972,12 @@ The first [[PPA-DP]] establishes the theory for on-device Individual DP accounting. The second [[PPA-DP-2]] expands the analysis to the mathematical privacy guarantees afforded by per-site budgets and by the global budget. -The per-site budgets should be seen as the primary privacy protection. Per-site budgets should be configured to provide a meaningful DP guarantee. However, the analysis in [[PPA-DP-2]] identified two assumptions that limit these guarantees: +The per-site budgets should be seen as the primary privacy protection. Per-site budgets should be configured to provide a meaningful DP guarantee. +However, the analysis in [[PPA-DP-2]] identified two assumptions that limit these guarantees: 1. *No cross-site adaptivity in data generation.* A site's queryable data stream (impressions - and conversions) must be generated independently of past DP [=attribution results=] from other sites. -1. *No leakage through cross-site shared limits.* Queries from one site must not affect which + and conversions) must be generated independently of past DP [=attribution results=] from other sites. +1. *No leakage through cross-site shared limits.* Queries from one site must not affect which reports are emitted to others. In short, neither assumption can hold in practice. @@ -2859,7 +3117,7 @@ If the [=privacy budget=] for that [=epoch=] is not sufficient, the impressions from that [=epoch=] are not used. Each time a [=conversion site=] invokes measureConversion() -the [=deduct privacy budget|privacy budget is deducted=] +the [=deduct privacy and safety budgets|privacy budget is deducted=] for [=epochs=] from which the [=attribution logic=] selected [=impressions=].