1. Introduction
This section is non-normative.
1.1. Motivation
Browsers are now working to prevent cross-site user tracking, including by partitioning storage and removing third-party cookies. There are a range of API proposals to continue supporting legitimate use cases in a way that respects user privacy. Many of these APIs, including the Shared Storage API and the Protected Audience API, isolate potentially identifying cross-site data in special contexts, which ensures that the data cannot escape the user agent.
Relative to cross-site data from an individual user, aggregate data about groups of users can be less sensitive and yet would be sufficient for a wide range of use cases. An aggregation service has been built to allow reporting noisy, aggregated cross-site data. This service was originally created for use by the Attribution Reporting API, but allowing more general aggregation supports additional use cases. In particular, the Protected Audience and Shared Storage APIs expect this functionality to be available.
1.2. Overview
This document outlines a general-purpose API that can be called from isolated contexts that have access to cross-site data (such as a Shared Storage worklet). Within these contexts, potentially identifying data can be encapsulated into "aggregatable reports". To prevent leakage, the cross-site data in these reports is encrypted to ensure it can only be processed by the aggregation service. During processing, this service adds noise and imposes limits on how many queries can be performed.
This API provides functions that allow the origin to construct an aggregatable report and specify the values to be embedded into its encrypted payload (for later computation via the aggregation service). These calls result in the aggregatable report being queued to be sent to the reporting endpoint of the script’s origin after a delay. After the endpoint receives the reports, it will batch the reports and send them to the aggregation service for processing. The output of that process is a summary report containing the (approximate) result, which is dispatched back to the script’s origin.
1.3. Alternative considered
Instead of the chosen API shape, we considered aligning with a design that is
much closer to fetch()
. However, there are a few
key differences which make this unfavorable:
-
This API is designed to be used in isolated contexts where
fetch()
is not available. -
It’s an anti-goal to give the developer control over when aggregatable reports are being sent or knowledge that they were sent (outside of the isolated context). Note, however, the exception when providing a context ID from outside the isolated context, see Protecting against leaks via the number of reports below.
-
The reports cannot be sent to arbitrary reporting endpoints, only a particular
.well-known
path on the script origin. -
The report’s input is very specific (an array of
PAHistogramContribution
s) and is not amenable tofetch()
's general purpose contents. -
There is no concept of a response.
So, we chose the more tailored API shape detailed below.
2. Exposed interface
[Exposed =(InterestGroupScriptRunnerGlobalScope ,SharedStorageWorklet ),SecureContext ]interface {
PrivateAggregation undefined contributeToHistogram (PAHistogramContribution );
contribution undefined enableDebugMode (optional PADebugModeOptions = {}); };
options dictionary {
PAHistogramContribution required bigint ;
bucket required long ;
value bigint = 0; };
filteringId dictionary {
PADebugModeOptions required bigint ; };
debugKey
Per the Web Platform Design Principles, we should consider switching long
to [EnforceRange] long long
.
enableDebugMode(options)
's argument should not
have a default value of {}
. Alternatively, debugKey
should not be required in PADebugModeOptions
.
Each PrivateAggregation
object has the following fields:
- scoping details (default null)
-
A scoping details or null
- allowed to use (default false)
-
A boolean
Note: See Exposing to global scopes below.
contributeToHistogram(PAHistogramContribution contribution)
method steps
are:
-
If contribution["
bucket
"] is not contained in the range 0 to 2128, exclusive, throw aRangeError
. -
If contribution["
value
"] is negative, throw aRangeError
. -
Let scopingDetails be this's scoping details.
-
Let batchingScope be the result of running scopingDetails’ get batching scope steps.
-
Let filteringIdMaxBytes be the default filtering ID max bytes.
-
If pre-specified report parameters map[batchingScope] exists:
-
Set filteringIdMaxBytes to pre-specified report parameters map[batchingScope]'s filtering ID max bytes.
-
-
If contribution["
filteringId
"] is not contained in the range 0 to 256filteringIdMaxBytes, exclusive, throw aRangeError
. -
Let entry be a new contribution cache entry with the items:
- contribution
-
contribution
- batching scope
-
batchingScope
- debug scope
-
The result of running scopingDetails’ get debug scope steps.
-
Append entry to the contribution cache.
Ensure errors are of an appropriate type, e.g. InvalidAccessError
is
deprecated.
Consider accepting an array of contributions. [Issue #44]
enableDebugMode(optional PADebugModeOptions options)
method steps are:
-
Let scopingDetails be this's scoping details.
-
Let debugScope be the result of running scopingDetails’ get debug scope steps.
-
If debug scope map[debugScope] exists, throw a "
DataError
"DOMException
.Note: This would occur if
enableDebugMode()
has already been run for this debug scope. -
Let debugKey be null.
-
If options was given:
-
Let debugDetails be a new debug details with the items:
-
Optionally, set debugDetails to a new debug details.
Note: This allows the user agent to make debug mode unavailable globally or just for certain callers.
-
Set debug scope map[debugScope] to debugDetails.
Ensure errors are of an appropriate type, e.g. InvalidAccessError
is
deprecated.
3. Exposing to global scopes
To expose this API to a global scope, a read only attribute privateAggregation
of type PrivateAggregation
should be exposed on the
global scope. Its getter steps should be set to the get the
privateAggregation steps given this.
Each global scope should set the allowed to use for the PrivateAggregation
object it exposes based on whether a relevant document is allowed to use the "private-aggregation
" policy-controlled feature.
Additionally, each global scope should set the scoping
details for the PrivateAggregation
object it exposes to a non-null value.
The global scope should wait to set the field until the API is intended to be
available.
For any batching scope returned by the get batching scope steps, the process contributions for a batching scope steps should later be performed given that same batching scope, the global scope’s relevant settings object's origin, some context type and a timeout (or null).
Note: This last requirement means that global scopes with different origins cannot share the same batching scope, see Same-origin policy discussion.
For any debug scope returned by the get debug scope steps, the mark a debug scope complete steps should later be performed given that same debug scope.
Note: A later algorithm asserts that, for any contribution cache entry in the contribution cache, the mark a debug scope complete steps were performed given the entry’s debug scope before the process contributions for a batching scope steps are performed given the entry’s batching scope.
3.1. APIs exposing Private Aggregation
This section is non-normative.
This API is currently exposed in global scopes defined in the specifications of two APIs:
-
Shared Storage and
-
Protected Audience (via the monkey patches below).
4. Structures
4.1. Batching scope
A batching scope is a unique internal value that identifies whichPAHistogramContribution
s should be sent in the same aggregatable
report unless their debug details differ.
Unique internal value is not an exported definition. See infra/583.
4.2. Debug scope
A debug scope is a unique internal value that identifies whichPAHistogramContribution
s should have their debug details affected by the
presence or absence of a call to enableDebugMode()
in the
same period of execution.
4.3. Scoping details
A scoping details is a struct with the following items:- get batching scope steps
-
An algorithm returning a batching scope
- get debug scope steps
-
An algorithm returning a debug scope
4.4. Debug details
A debug details is a struct with the following items:- enabled (default false)
-
A boolean
- key (default null)
-
An unsigned 64-bit integer or null. The key must be null if enabled is false.
4.5. Contribution cache entry
A contribution cache entry is a struct with the following items:- contribution
- batching scope
- debug scope
- debug details (default null)
-
A debug details or null
4.6. Aggregatable report
An aggregatable report is a struct with the following items:
- reporting origin
-
An origin
- original report time
-
A moment
- report time
-
A moment
- contributions
-
A list of
PAHistogramContribution
s - api
- report ID
-
A string
- debug details
- aggregation coordinator
- context ID
-
A string or null
- filtering ID max bytes
-
A positive integer
- queued
-
A boolean
4.7. Aggregation coordinator
An aggregation coordinator is an origin that the allowed aggregation coordinator set contains.
Consider switching to the suitable origin concept used by the Attribution Reporting API here and elsewhere.
Move other structures to be defined inline instead of via a header. Consider also removing all the subheadings.
4.8. Context type
A context type is a string indicating what kind of global scope thePrivateAggregation
object was exposed in. Each API exposing Private
Aggregation should pick a unique string (or multiple) for this.
4.9. Pre-specified report parameters
A pre-specified report parameters is a struct with the following items:
- context ID (default: null)
-
A string or null
- filtering ID max bytes (default: default filtering ID max bytes)
-
A positive integer
5. Storage
A user agent holds an aggregatable report cache, which is a list of aggregatable reports.
A user agent holds an aggregation coordinator map, which is a map from batching scopes to aggregation coordinators.
A user agent holds a pre-specified report parameters map, which is a map from batching scopes to pre-specified report parameters.
A user agent holds a contribution cache, which is a list of contribution cache entries.
A user agent holds a debug scope map, which is a map from debug scopes to debug details.
Elsewhere, link to definition when using user agent.
5.1. Clearing storage
The user agent must expose controls that allow the user to delete data from the aggregatable report cache as well as any contribution history data stored for the consume budget if permitted algorithm.
The user agent may expose controls that allow the user to delete data from the contribution cache, the debug scope map and the pre-specified report parameters map.
6. Constants
Default filtering ID max bytes is a positive integer controlling the max bytes used if none is explicitly chosen. Its value is 1.
Valid filtering ID max bytes range is a set of positive integers controlling the allowable values of max bytes. Its value is the range 1 to 8, inclusive.
Consider adding more constants.
7. Implementation-defined values
Allowed aggregation coordinator set is a set of origins that controls which origins are valid aggregation coordinators. Every item in this set must be a potentially trustworthy origin.
Default aggregation coordinator is an aggregation coordinator that controls which is used for a report if none is explicitly selected.
Maximum report contributions is a map from context type to positive integers. Semantically, it defines the maximum number of contributions that can be present in a single report for every kind of calling context, e.g. Shared Storage.
Minimum report delay is a non-negative duration that controls the minimum delay to deliver an aggregatable report.
Randomized report delay is a positive duration that controls the random delay to deliver an aggregatable report. This delay is additional to the minimum report delay.
8. Permissions Policy integration
This specification defines a policy-controlled feature identified by the
string "private-aggregation
". Its default allowlist is "*
".
Note: The allowed to use field is set by other specifications that integrate with this API according to this policy-controlled feature.
9. Algorithms
To serialize an integer, represent it as a string of the shortest possible decimal number.
This would ideally be replaced by a more descriptive algorithm in Infra. See infra/201.
9.1. Exported algorithms
Note: These algorithms allow other specifications to integrate with this API.
PrivateAggregation
this:
-
Let scopingDetails be this’s scoping details.
-
If scopingDetails is null, throw a "
NotAllowedError
"DOMException
.Note: This indicates the API is not yet available, for example, because the initial execution of the script after loading is not complete.
Consider improving developer ergonomics here (e.g. a way to detect this case).
-
If this’s allowed to use is false, throw an "
InvalidAccessError
"DOMException
. -
Return this.
Ensure errors are of an appropriate type, e.g. InvalidAccessError
is
deprecated.
-
Append entry to the contribution cache.
-
If debug scope map[debugScope] exists, return debug scope map[debugScope].
-
Otherwise, return a new debug details.
-
Let debugDetails be debugDetailsOverride.
-
If debug scope map[debugScope] exists:
-
Assert: debugDetailsOverride is null.
Note: The override can be provided if the debug details have not been set otherwise.
-
Set debugDetails to debug scope map[debugScope].
-
Remove debug scope map[debugScope].
-
If debugDetails’s key is not null, assert: debugDetails’s enabled is true.
-
-
If debugDetails is null, set debugDetails to a new debug details.
-
For each entry of the contribution cache:
-
If entry’s debug scope is debugScope, set entry’s debug details to debugDetails.
-
-
If preSpecifiedParams’ context ID is not null, return true.
-
If preSpecifiedParams’ filtering ID max bytes is not the default filtering ID max bytes, return true.
-
Return false.
Note: If a context ID or non-default filtering ID max bytes was specified, a report is sent, even if there are no contributions or there is insufficent budget for the requested contributions. See Protecting against leaks via the number of reports.
-
Let batchEntries be a new list.
-
For each entry of the contribution cache:
-
If entry’s batching scope is batchingScope:
-
Assert: entry’s debug details is not null.
Note: This asserts that the mark a debug scope complete steps were run before the process contributions for a batching scope steps.
-
Append entry to batchEntries.
-
-
-
Let aggregationCoordinator be the default aggregation coordinator.
-
If aggregation coordinator map[batchingScope] exists:
-
Set aggregationCoordinator to aggregation coordinator map[batchingScope].
-
Remove aggregation coordinator map[batchingScope].
-
-
Let preSpecifiedParams be a new pre-specified report parameters.
-
If pre-specified report parameters map[batchingScope] exists:
-
Set preSpecifiedParams to pre-specified report parameters map[batchingScope].
-
Remove pre-specified report parameters map[batchingScope].
-
-
Let isDeterministicReport be the result of determining if a report should be sent deterministically given preSpecifiedParams.
-
If isDeterministicReport is false, assert: timeout is null.
Note: Timeouts can only be used for deterministic reports.
-
If batchEntries is empty and isDeterministicReport is false, return.
-
Let batchedContributions be a new ordered map.
-
For each entry of batchEntries:
-
Remove entry from the contribution cache.
-
Let debugDetails be entry’s debug details.
-
If batchedContributions[debugDetails] does not exist:
-
Append entry’s contribution to batchedContributions[debugDetails].
-
-
If batchedContributions is empty:
-
Let debugDetails be a new debug details.
-
Set batchedContributions[debugDetails] to a new list.
-
-
For each debugDetails → contributions of batchedContributions:
-
Perform the report creation and scheduling steps with reportingOrigin, contextType, contributions, debugDetails, aggregationCoordinator, preSpecifiedParams and timeout.
-
Note: These steps break up the contributions based on their debug details as each report can only have one set of metadata.
-
Return whether origin is an aggregation coordinator.
-
Assert: origin is an aggregation coordinator.
-
Set aggregation coordinator map[batchingScope] to origin.
Elsewhere, surround algorithms in a <div algorithm>
block to match, and
add styling for all algorithms per bikeshed/1472.
-
Let contextId be params’ context ID.
-
Assert: contextId is null or contextId’s length is not larger than 64.
-
Let filteringIdMaxBytes be params’ filtering ID max bytes.
-
Assert: filteringIdMaxBytes is contained in the valid filtering ID max bytes range
-
Set pre-specified report parameters map[batchingScope] to params.
9.2. Scheduling reports
PAHistogramContribution
s contributions, a debug details debugDetails, an aggregation coordinator aggregationCoordinator, a pre-specified report parameters preSpecifiedParams and a moment or
null timeout:
-
Assert: reportingOrigin is a potentially trustworthy origin.
-
Optionally, return.
Note: This implementation-defined condition is intended to allow user agents to drop reports for a number of reasons, for example user opt-out or an origin not being enrolled.
-
Let mergedContributions be a new list.
-
For each contribution of contributions:
-
Let hasProcessedContribution be false.
-
For each mergedContribution of mergedContributions:
-
If contribution and mergedContribution have both the same
bucket
and the samefilteringId
:
-
-
If hasProcessedContribution is false, append contribution to mergedContributions.
-
-
Let truncatedContributions be a new list.
-
If mergedContributions has a size greater than maximum report contributions[api]:
-
For each n of the range 0 to maximum report contributions[api], exclusive:
-
Append mergedContributions[n] to truncatedContributions.
-
-
-
Otherwise, set truncatedContributions to mergedContributions.
-
Let contributionSum be 0.
-
For each contribution of truncatedContributions:
-
Let currentWallTime be the current wall time.
-
Let sufficientBudget be the result of consuming budget if permitted given contributionSum, reportingOrigin, api and currentWallTime.
Allow for some of the contributions to still be approved even if there isn’t enough budget for the entire report. Note that this change would require the merging step to be postponed until after budgeting and the truncation step modified to account for the later merging.
-
If sufficientBudget is false:
-
Let isDeterministicReport be the result of determining if a report should be sent deterministically given preSpecifiedParams.
-
If isDeterministicReport is false, return.
-
Empty truncatedContributions.
-
-
Let report be the result of obtaining an aggregatable report given reportingOrigin, api, truncatedContributions, debugDetails, aggregationCoordinator, preSpecifiedParams, timeout and currentWallTime.
-
Append report to the user agent’s aggregatable report cache.
PAHistogramContribution
s contributions, a debug details debugDetails, an aggregation coordinator aggregationCoordinator, a pre-specified report parameters preSpecifiedParams, a [=moment] or null timeout and a moment currentTime,
perform the following steps. They return an aggregatable report.
-
Assert: reportingOrigin is a potentially trustworthy origin.
-
Let reportTime be the result of running obtain a report delivery time given currentTime and timeout.
-
Let report be a new aggregatable report with the items:
- reporting origin
-
reportingOrigin
- original report time
-
reportTime
- report time
-
reportTime
- contributions
-
contributions
- api
-
api
- report ID
-
The result of generating a random UUID.
- debug details
-
debugDetails
- aggregation coordinator
-
aggregationCoordinator
- context ID
-
preSpecifiedParams’ context ID
- filtering ID max bytes
-
preSpecifiedParams’ filtering ID max bytes
- queued
-
false
-
Return report.
-
If timeout is not null:
-
Return timeout.
-
-
If automation local testing mode enabled is true, return currentTime.
-
Let r be a random double between 0 (inclusive) and 1 (exclusive) with uniform probability.
-
Return currentTime + minimum report delay + r * randomized report delay.
9.3. Sending reports
Note: This section is largely copied from the Attribution Reporting API spec, adapting as necessary.
Do we have to use the queue a task algorithm here?
The user agent must periodically attempt to queue reports for sending given its aggregatable report cache.
-
For each report of reports, run these steps in parallel:
-
Run these steps, but abort when the user agent shuts down:
-
If report’s queued value is true, return.
-
Set report’s queued value to true.
-
Let currentWallTime be the current wall time.
-
If report’s report time is before currentWallTime, set report’s report time to currentWallTime plus an implementation-defined random non-negative duration.
Note: On startup, it is possible the user agent will need to send many reports whose report times passed while the browser was closed. Adding random delay prevents temporal joining of reports.
-
Wait until the current wall time is equal to or after report’s report time.
-
Optionally, wait a further implementation-defined non-negative duration.
Note: This is intended to allow user agents to optimize device resource usage and wait for the user agent to be online.
-
Run attempt to deliver a report with report.
-
-
If aborted, set report’s queued value to false.
Note: It might be more practical to perform this step when the user agent next starts up.
-
-
Let url be the result of obtaining a reporting endpoint given report’s reporting origin and report’s api.
-
Let data be the result of serializing an aggregatable report given report.
-
If data is an error, remove report from the aggregatable report cache.
-
Let request be the result of creating a report request given url and data.
-
Queue a task to fetch request with processResponse being the following steps:
-
Let shouldRetry be an implementation-defined boolean. The value should be false if no error occurred.
-
If shouldRetry is true:
-
Set report’s report time to the current wall time plus an implementation-defined non-negative duration.
-
Set report’s queued value to false.
-
-
Otherwise, remove report from the aggregatable report cache.
-
-
Assert: reportingOrigin is a potentially trustworthy origin.
-
Let path be the concatenation of «"
.well-known/private-aggregation/report-
", api».Register this well-known directory. [Issue #67]
-
Let base be the result on running the URL parser on the serialization of reportingOrigin.
-
Assert: base is not failure.
-
Let result be the result of running the URL parser on path with base.
-
Assert: result is not failure.
-
Return result.
-
Let request be a new request with the following properties:
- method
-
"
POST
" - URL
-
url
- header list
-
«("
Content-Type
", "application/json
")» - unsafe-request flag
-
set
- body
-
body
- client
-
null
- window
-
"
no-window
" - service-workers mode
-
"
none
" - initiator
-
""
- referrer
-
"
no-referrer
" - mode
-
"
cors
" - credentials mode
-
"
omit
" - cache mode
-
"
no-store
"
-
Return request.
9.4. Serializing reports
Note: This section is largely copied from the Attribution Reporting API spec, adapting as necessary.
-
Let aggregationServicePayloads be the result of obtaining the aggregation service payloads given report.
-
If aggregationServicePayloads is an error, return aggregationServicePayloads.
-
Let data be an ordered map of the following key/value pairs:
- "
aggregation_coordinator_origin
" -
report’s aggregation coordinator, serialized.
- "
aggregation_service_payloads
" -
aggregationServicePayloads
- "
shared_info
" -
The result of obtaining a report’s shared info given report.
- "
-
Let debugKey be report’s debug details's key.
-
If debugKey is not null, set data["
debug_key
"] to debugKey. -
Let contextId be report’s context ID.
-
If contextId is not null, set data["
context_id
"] to contextId. -
Return the byte sequence resulting from executing serialize an infra value to JSON bytes on data.
-
Let publicKeyTuple be the result of obtaining the public key for encryption given report’s aggregation coordinator.
-
If publicKeyTuple is an error, return publicKeyTuple.
-
Let (pkR, keyId) be publicKeyTuple.
-
Let plaintextPayload be the result of obtaining the plaintext payload given report.
-
Let sharedInfo be the result of obtaining a report’s shared info given report.
-
Let encryptedPayload be the result of encrypting the payload given plaintextPayload, pkR and sharedInfo.
-
If encryptedPayload is an error, return encryptedPayload.
-
Let aggregationServicePayloads be a new list.
-
Let aggregationServicePayload be an ordered map of the following key/value pairs:
- "
key_id
" -
keyId
- "
payload
" -
encryptedPayload, base64 encoded
- "
-
If report’s debug details's enabled field is true:
-
Set aggregationServicePayload[
debug_cleartext_payload
] to plaintextPayload, base64 encoded.
-
-
Append aggregationServicePayload to aggregationServicePayloads.
-
Return aggregationServicePayloads.
-
Let url be a new URL record.
-
Set url’s path to «"
.well-known
", "aggregation-service
", "v1
", "public-keys
"». -
Return an implementation-defined tuple consisting of a public key from url and a string that should uniquely identify the public key or, in the event that the user agent failed to obtain the public key from url, an error. This step may be asynchronous.
Specify this in terms of fetch. Add details about which encryption standards to use, length requirements, etc.
Note: The user agent is encouraged to enforce regular key rotation. If there are multiple keys, the user agent can independently pick a key uniformly at random for every encryption operation.
-
Let payloadData be a new list.
-
Let contributions be report’s contributions.
-
Let maxContributions be maximum report contributions[api].
-
Assert: contributions’ size is not greater than maxContributions.
-
While contributions’ size is less than maxContributions:
-
Let nullContribution be a new
PAHistogramContribution
with the items:bucket
-
0
value
-
0
filteringId
-
0
-
Append nullContribution to contributions.
Note: This padding protects against the number of contributions being leaked through the encrypted payload size, see discussion below.
-
-
For each contribution of report’s contributions:
-
Let filteringIdMaxBytes be report’s filtering id max bytes.
-
Assert: contribution["
filteringId
"] is contained in the range 0 to 256filteringIdMaxBytes, exclusive. -
Let contributionData be an ordered map of the following key/value pairs:
- "
bucket
" -
The result of encoding an integer for the payload given contribution["
bucket
"] and 16. - "
value
" -
The result of encoding an integer for the payload given contribution["
value
"] and 4. - "
id
" -
The result of encoding an integer for the payload given contribution[="
filteringId
"] and filteringIdMaxBytes.
- "
-
Append contributionData to payloadData.
-
-
Let payload be an ordered map of the following key/value pairs:
- "
data
" -
payloadData
- "
operation
" -
"
histogram
"
- "
-
Return the byte sequence resulting from CBOR encoding payload.
-
Let info be the result of UTF-8 encoding the concatenation of « "
aggregation_service
", sharedInfo ». -
Let (kem_id, kdf_id, aead_id) be (0x0020, 0x0001, 0x0003).
Note: The ciphersuite triple above is composed of HPKE algorithm identifiers, specifying the KEM as DHKEM(X25519, HKDF-SHA256), the KDF function as HKDF-SHA256 and the AEAD function as ChaCha20Poly1305.
-
Let (enc, hpkeContext) be the result of setting up an HPKE sender’s context by calling
SetupBaseS()
with a public key pkR, application-supplied information info, KEM kem_id, KDF kdf_id, and AEAD aead_id. If this operation fails, return an error.Note: For clarity, we explicitly passed the KEM, KDF, and AEAD identifiers to
SetupBaseS()
above, even though RFC9180 omits the parameters from its pseudocode. -
Let aad be `` (an empty byte sequence).
-
Let ciphertext be the result of sealing the payload by calling
ContextS.Seal()
on the hpkeContext object with additional authenticated data aad and plaintext plaintextPayload. If this operation fails, return an error. -
Let encryptedPayload be the concatenation of the byte sequences « enc, ciphertext ».
Note: The length of the encapsulated symmetric key enc generated by our chosen KEM is exactly 32 bytes, as shown in RFC9180’s table of KEM IDs.
-
Return the byte sequence encryptedPayload.
-
Let scheduledReportTime be the duration from the UNIX epoch to report’s original report time.
-
Let sharedInfo be an ordered map of the following key/value pairs:
- "
api
" -
report’s api
- "
report_id
" -
report’s report ID
- "
reporting_origin
" -
The serialization of report’s reporting origin
- "
scheduled_report_time
" -
The number of seconds in scheduledReportTime, rounded down to the nearest number of whole seconds and serialized
- "
version
" -
"
1.0
"
- "
-
Return the result of serializing an infra value to a json string given sharedInfo.
10. User-agent automation
A user agent holds a boolean automation local testing mode enabled (default false).
For the purposes of user-agent automation and website testing, this document defines the below [WebDriver] extension commands to control the API configuration.
10.1. Set local testing mode
HTTP Method | URI Template |
---|---|
POST | /session/{session id}/private-aggregation/ localtestingmode
|
-
If parameters is not a JSON-formatted Object, return a WebDriver error with error code invalid argument.
-
Let enabled be the result of getting a property named
"enabled"
from parameters. -
If enabled is
undefined
or is not a boolean, return a WebDriver error with error code invalid argument. -
Set automation local testing mode enabled to enabled.
-
Return success with data
null
.
Note: Without this, aggregatable reports would be subject to delays, making testing difficult.
11. Protected Audience API monkey patches
This should be moved to the Protected Audience spec, along with any other Protected Audience-specific details. [Issue #43]
11.1. New WebIDL
partial interface InterestGroupScriptRunnerGlobalScope {readonly attribute PrivateAggregation ; };
privateAggregation dictionary {
PASignalValue required DOMString ;
baseValue double ; (
scale bigint or long ); };
offset dictionary {
PAExtendedHistogramContribution required (PASignalValue or bigint );
bucket required (PASignalValue or long );
value bigint = 0; }; [
filteringId Exposed =InterestGroupScriptRunnerGlobalScope ,SecureContext ]partial interface PrivateAggregation {undefined contributeToHistogramOnEvent (DOMString ,
event PAExtendedHistogramContribution ); };
contribution dictionary {
AuctionReportBuyersConfig required bigint ;
bucket required double ; };
scale dictionary {
AuctionReportBuyerDebugModeConfig boolean =
enabled false ; // Must only be provided if `enabled` is true.bigint ?; };
debugKey partial dictionary AuctionAdConfig {sequence <bigint >;
auctionReportBuyerKeys record <DOMString ,AuctionReportBuyersConfig >;
auctionReportBuyers AuctionReportBuyerDebugModeConfig ; };
auctionReportBuyerDebugModeConfig
Note: requiredSellerCapabilities
is defined in the Protected
Audience spec.
Do we want to align naming with implementation?
The privateAggregation
getter steps are to get the privateAggregation given this.
contributeToHistogramOnEvent(DOMString
event, PAExtendedHistogramContribution contribution)
method steps are:
-
Let scopingDetails be this's scoping details.
-
If event starts with "
reserved.
" and « "reserved.always
", "reserved.loss
", "reserved.win
" » does not contain event, return.Note: No error is thrown to allow forward compatibility if additional reserved event types are added later.
-
Let bucket be contribution["
bucket
"]. -
If bucket is a
PASignalValue
: -
Otherwise, if contribution["
bucket
"] is not contained in the range 0 to 2128, exclusive, throw aTypeError
.Make the error type consistent with
contributeToHistogram(contribution)
. -
Let value be contribution["
value
"]. -
If value is a
PASignalValue
: -
Otherwise, if contribution["
value
"] is negative, throw aTypeError
. -
If contribution["
filteringId
"] is not contained in the range 0 to 256default filtering ID max bytes, exclusive, throw aTypeError
.Make the error types on validation issues here and above consistent with
contributeToHistogram(contribution)
.Note: It is not currently possible to set a non-default filtering ID max bytes for Protected Audience.
-
Let batchingScope be null.
-
If event starts with "
reserved.
", set batchingScope to the result of running scopingDetails’ get batching scope steps.Note: Each non-reserved event will have a different batching scope that is created later.
-
Let entry be a new on event contribution cache entry with the items:
- contribution
-
contribution
- batching scope
-
batchingScope
- debug scope
-
The result of running scopingDetails’ get debug scope steps.
- worklet function
-
The worklet function that is currently being executed.
-
Let global be this's relevant global object.
-
Let auctionConfig be global’s auction config.
-
Let ig be the result of maybe obtaining an interest group given global.
-
Let cacheMap be auctionConfig’s per-bid or seller on event contribution cache.
-
If cacheMap[ig] does not exist, set cacheMap[ig] to a new on event contribution cache.
-
Let onEventContributionCache be cacheMap[ig].
-
If onEventContributionCache[event] does not exist, set onEventContributionCache[event] to a new list.
-
Append entry to onEventContributionCache[event].
Ensure errors are of an appropriate type, e.g. InvalidAccessError
is
deprecated.
Consider accepting an array of contributions. [Issue #44]
11.2. WebIDL modifications
The AuctionAdConfig
and AuctionAdInterestGroup
dictionaries are
modified to add a new field:
dictionary {
ProtectedAudiencePrivateAggregationConfig USVString ; };
aggregationCoordinatorOrigin partial dictionary AuctionAdConfig {ProtectedAudiencePrivateAggregationConfig ; };
privateAggregationConfig partial dictionary AuctionAdInterestGroup {ProtectedAudiencePrivateAggregationConfig ; };
privateAggregationConfig
Note: sellerCapabilities
is defined in the Protected
Audience spec
11.3. Structures
11.3.1. Extending auction config
Extend the auction config struct to add new fields:
- per-bid or seller on event contribution cache
-
A map from interest group or null to a on event contribution cache.
Note: a null key represents the seller.
- batching scope map
-
A map from a tuple consisting of an origin (an origin) and a coordinator (an aggregation coordinator) to a batching scope.
Note: Does not include batching scopes for contributions conditional on non-reserved events.
- permissions policy state
- seller Private Aggregation coordinator
-
An aggregation coordinator. Defaults to the default aggregation coordinator.
- auction report buyer keys
- auction report buyers
-
A map from strings to
AuctionReportBuyersConfig
s. - auction report buyer debug details
-
Consider replacing the strings above with specific enum types.
11.3.2. Extending interest group
Extend the interest group struct to add a new field:
- Private Aggregation coordinator
-
An aggregation coordinator or null.
Note: a null value specifies the default coordinator.
Add the following definitions in a new subsection at the end of Structures,
renumbered appropriately.
11.3.3. Permissions policy state
A permissions policy state is a struct with the following items:- private aggregation enabled
-
A boolean (default false)
11.3.4. Signal base value
A signal base value is one of the following:- "
winning-bid
" -
The numeric value is the bid value of the winning bid.
- "
highest-scoring-other-bid
" -
The numeric value is the bid value of the highest scoring bid that did not win.
- "
script-run-time
" -
The numeric value is the number of milliseconds of CPU time the calling function (e.g.
generateBid()
) took to run. - "
signals-fetch-time
" -
The numeric value is the number of milliseconds it took for the trusted bidding or scoring signals fetch to complete, when called from
generateBid()
orscoreAd()
, respectively. - "
bid-reject-reason
" -
The numeric value is an integer representing the reason a bid was rejected.
Note: this mapping to an integer is defined in determine a signal’s numeric value.
11.3.5. Worklet function
A worklet function is one of the following:- "
generate-bid
" -
The
generateBid()
function. - "
score-ad
" -
The
scoreAd()
function. - "
report-result
" -
The
reportResult()
function. - "
report-win
" -
The
reportWin()
function.
11.3.6. On event contribution cache entry
An on event contribution cache entry is a struct with the following items:- contribution
- batching scope
-
A batching scope or null
- debug scope
- debug details
-
A debug details or null (default null)
- worklet function
11.3.7. On event contribution cache
An on event contribution cache is a map from string to a list of on event contribution cache entries.11.3.8. Extending InterestGroupScriptRunnerGlobalScope
Extend the global scopes subsection to add:
Each InterestGroupScriptRunnerGlobalScope
has an:
- auction config
11.3.9. Extending InterestGroupReportingScriptRunnerGlobalScope
Extend the InterestGroupReportingScriptRunnerGlobalScope subsection to add an extra field to the end of the list beginning
"Each InterestGroupReportingScriptRunnerGlobalScope
has a":
- interest group
-
Null or an interest group. Null for seller reporting (i.e.
reportResult()
).
11.4. Algorithm modifications
The joinAdInterestGroup()
method steps are modified to add the
following steps at the end of the scope nested under step 5 ("Validate the given group and ..."):
-
If group["
privateAggregationConfig
"] exists:-
Let aggregationCoordinator be the result of obtaining the Private Aggregation coordinator given group["
privateAggregationConfig
"]. -
If aggregationCoordinator is a
DOMException
, then throw aggregationCoordinator. -
Set interestGroup’s Private Aggregation coordinator to aggregationCoordinator.
-
The runAdAuction()
method steps are modified to add the
following step just after step 5 ("If auctionConfig is a
failure, then..."), renumbering the later steps as appropriate:
-
Set auctionConfig’s permissions policy state to a new permissions policy state with the items:
- private aggregation enabled
-
The result of determining whether global’s associated Document is allowed to use the "
private-aggregation
" policy-controlled feature.
The validate and convert auction ad config steps are modified to add the following steps just before the last step ("Return auctionConfig"), renumbering the later step as appropriate:
-
If config["
auctionReportBuyerKeys
"] exists:-
Let interestGroupBuyers be auctionConfig’s interest group buyers.
-
If interestGroupBuyers is null, set interestGroupBuyers to a new list.
-
For each index of the range 0 to config["
auctionReportBuyerKeys
"]'s size, exclusive:-
Let key be config["
auctionReportBuyerKeys
"][index]. -
If key is not contained in the range 0 to 2128, exclusive, throw a
TypeError
. -
If index is equal to or greater than interestGroupBuyers’ size, continue.
Note: Continue is used (instead of break) to match validation logic for all given buyer keys.
-
Let origin be interestGroupBuyers[index].
-
Set auctionConfig’s auction report buyer keys[origin] to key.
Check behavior when an origin is repeated in
interestGroupBuyers
.
-
-
-
If config["
auctionReportBuyers
"] exists:-
For each reportType → reportBuyerConfig of config["
auctionReportBuyers
"]:-
If « "
interestGroupCount
", "bidCount
", "totalGenerateBidLatency
", "totalSignalsFetchLatency
" » does not contain reportType, continue.Note: No error is thrown to allow forward compatibility if additional report types are added later.
-
If reportBuyerConfig["
bucket
"] is not contained in the range 0 to 2128, exclusive, throw aTypeError
.Consider validating the case where the bucket used (after summing) is too large. Currently, the implementation appears to overflow. See protected-audience/1040.
-
Set auctionConfig’s auction report buyers[reportType] to reportBuyerConfig.
-
-
-
Set auctionConfig’s auction report buyer debug details to a new debug details.
-
If config["
auctionReportBuyerDebugModeConfig
"] exists:-
Let debugModeConfig be config["
auctionReportBuyerDebugModeConfig
"]. -
Let enabled be debugModeConfig["
enabled
"]. -
Let debugKey be debugModeConfig["
debugKey
"]. -
If debugKey is not null:
-
Set auctionConfig’s auction report buyer debug details to a new debug details with the items:
-
-
If config["
privateAggregationConfig
"] exists:-
Let aggregationCoordinator be the result of obtaining the Private Aggregation coordinator given config["
privateAggregationConfig
"]. -
If aggregationCoordinator is a
DOMException
, return failure. -
Set auctionConfig’s seller Private Aggregation coordinator to aggregationCoordinator.
-
Make all map indexing links (throughout the spec) where possible, i.e. matching this section.
The generate and score bids algorithm is modified by inserting the following step before each of the two "Return leadingBidInfo’s leading bid" steps (one in a nested scope), renumbering this and later steps as necessary.
-
Process the Private Aggregation contributions for an auction given auctionConfig and leadingBidInfo.
The evaluate a script steps are modified in two ways. First, we add the following steps after step 11 ("If evaluationStatus is an abrupt completion..."), renumbering later steps as appropriate:
-
Set global’s
privateAggregation
's allowed to use to auctionConfig’s permissions policy state's private aggregation enabled. -
Let debugScope be a new debug scope.
-
Set global’s
privateAggregation
's scoping details to a new scoping details with the items:- get batching scope steps
-
An algorithm that performs the following steps:
-
Let origin be realm’s settings object's origin.
-
Let ig be the result of maybe obtaining an interest group given realm’s global object.
-
Let aggregationCoordinator be null.
-
If ig is not null, set aggregationCoordinator to ig’s Private Aggregation coordinator.
-
Otherwise, set aggregationCoordinator to auctionConfig’s seller Private Aggregation coordinator.
-
If aggregationCoordinator is null, set aggregationCoordinator to the default aggregation coordinator.
-
Return the result of running get or create a batching scope given origin, aggregationCoordinator and auctionConfig.
-
- get debug scope steps
-
An algorithm that returns debugScope.
Once protected-audience/615 is resolved, align the above monkey patch with
how access to other functions is prevented in InterestGroupScriptRunnerGlobalScope
s until the script’s initial
execution is complete.
Second, in the nested scope of the last step, we insert a new step just after the step labelled "Clean up after script", renumbering the later step as appropriate:
-
Let debugDetails be the result of get a debug details given debugScope.
-
Let ig be the result of maybe obtaining an interest group given global.
-
Let onEventContributionCache be auctionConfig’s per-bid or seller on event contribution cache[ig].
-
For each event → entries of onEventContributionCache:
-
For each onEventEntry of entries:
-
If onEventEntry’s debug scope is debugScope, set onEventEntry’s debug details to debugDetails.
-
-
-
Mark a debug scope complete given debugScope.
The evaluate a bidding script steps are modified in the following two ways. First, we add a new parameter auction config auctionConfig.
Note: This algorithm already takes an interest group parameter ig.
Second, we add the following step after step 6 ("Set global’s interest group to ig"), renumbering later steps as appropriate:
-
Set global’s auction config to auctionConfig.
The evaluate a scoring script steps are modified in the following two ways. First, we add a new parameter auction config auctionConfig.
Second, we add the following step after step 1 ("Let global be a new InterestGroupScoringScriptRunnerGlobalScope
."),
renumbering the later step as appropriate:
-
Set global’s auction config to auctionConfig.
The evaluate a reporting script steps are modified in the following two ways. First, we add two new parameters: an auction config auctionConfig and an interest group or null ig.
Second, we add the following step after step 1 ("Let global be a new InterestGroupReportingScriptRunnerGlobalScope
."),
renumbering the later step as appropriate:
-
Set global’s auction config to auctionConfig.
-
Set global’s interest group to ig.
Then, we modify the invocations of the above algorithms to plumb the new parameters in:
The generate potentially multiple bids algorithm is modified to add a new auction config parameter auctionConfig. Additionally, its last step is modified by adding the argument auctionConfig to the invocation of evaluating a bidding script. Further, the generate and score bids algorithm is modified by adding the argument auctionConfig to both invocations of generate potentially multiple bids.
The score and rank a bid algorithm is modified by adding the argument auctionConfig to the invocation of evaluating a scoring script.
The report result algorithm is modified by passing in the arguments auctionConfig and null to the invocation of evaluate a reporting script.
The report win algorithm is modified by passing in the arguments auctionConfig and winner’s interest group to the invocation of evaluate a reporting script.
The estimated size of an interest group algorithm is modified to add the following line at the end of the sum:
-
The length of the serialization of ig’s Private Aggregation coordinator if the field is not null.
The update interest groups steps are modified to add the following case at the end of the "Switch on key" step.
- "
privateAggregationConfig
" -
-
If value is not a map whose keys are strings, jump to the step labeled Abort update.
-
If value["
aggregationCoordinatorOrigin
"] exists:-
If value["
aggregationCoordinatorOrigin
"] is not a string, jump to the step labeled Abort update. -
Let aggregationCoordinator be the result of obtaining the Private Aggregation coordinator given value["
aggregationCoordinatorOrigin
"]. -
If aggregationCoordinator is a
DOMException
, jump to the step labeled Abort update. -
Otherwise, set ig’s Private Aggregation coordinator to aggregationCoordinator.
-
-
11.5. New algorithms
Add the following definitions:
-
Let winnerOrigin be null.
-
If leadingBidInfo’s leading bid is not null, set winnerOrigin to leadingBidInfo’s leading bid’s interest group’s owner.
-
For each ig → onEventContributionCache of auctionConfig’s per-bid or seller on event contribution cache:
-
Let origin be null.
-
If ig is null, set origin to auctionConfig’s seller.
-
Otherwise, set origin to ig’s owner.
-
For each event → entries of onEventContributionCache:
-
If event is "
reserved.win
" or does not start with "reserved.
":-
If origin is not winnerOrigin, return.
-
-
If event is "
reserved.loss
" and origin is winnerOrigin, return. -
For each onEventEntry of entries:
-
Let filledInContribution be the result of filling in the contribution given onEventEntry’s contribution and leadingBidInfo.
Once protected-audience/627 is resolved, align 'filling in' logic with
forDebuggingOnly
. -
If event does not start with "
reserved.
":-
Store event, filledInContribution, onEventEntry’s debug details in the
FencedFrameConfig
as appropriate.Note: Each non-reserved event will have a different batching scope.
Once protected-audience/616 and any successors are landed, align integration and fill in fenced frame’s report a private aggregation event.
-
-
Let entry be a new contribution cache entry with the items:
- contribution
-
filledInContribution
- batching scope
-
onEventEntry’s batching scope
- debug scope
-
onEventEntry’s debug scope
- debug details
-
onEventEntry’s debug details
-
Append entry to the contribution cache.
-
-
-
-
Let sellerBatchingScope be the result of getting or creating a batching scope given auctionConfig’s seller, auctionConfig’s seller Private Aggregation coordinator, and auctionConfig.
-
Let auctionReportBuyersDebugScope be a new debug scope.
-
For each reportType → reportBuyerConfig of auctionConfig’s auction report buyers:
-
For each buyerOrigin → buyerOffset of auctionConfig’s auction report buyer keys:
-
Let bucket be the sum of buyerOffset and reportBuyerConfig’s
bucket
.Handle overflow here or in validation. See protected-audience/1040.
-
Let value be the result (a
double
) of switching on reportType:- "
interestGroupCount
" -
The number of interest groups in the user agent's interest group set whose owner is buyerOrigin.
- "
bidCount
" -
The number of valid bids generated by interest groups whose owner is buyerOrigin.
- "
totalGenerateBidLatency
" -
The sum of execution time in milliseconds for all
generateBid()
calls in the auction for interest groups whose owner is buyerOrigin. - "
totalSignalsFetchLatency
" -
The total time spent fetching trusted buyer signals in milliseconds, or 0 if the interest group didn’t fetch any trusted signals.
- None of the above values
-
Assert: false
Note: This enum value is validated in validate and convert auction ad config.
- "
-
Set value to the result of multiplying reportBuyerConfig’s
scale
with value. -
Set value to the maximum of 0.0 and value.
-
Set value to the result of converting value to an integer by truncating its fractional part.
-
Set value to the minimum of value and 231−1.
-
Let contribution be a new
PAHistogramContribution
with the items:bucket
-
bucket
value
-
value
filteringId
-
0
-
For each ig of the user agent's interest group set whose owner is buyerOrigin:
-
If seller capabilities don’t allow this reporting, continue.
Align behavior with seller capabilities handling once protected-audience/966 is resolved.
-
Let entry be a new contribution cache entry with the items:
- contribution
-
contribution
- batching scope
-
sellerBatchingScope
- debug scope
-
auctionReportBuyersDebugScope
-
Append entry to the contribution cache.
-
-
-
-
Mark a debug scope complete given auctionReportBuyersDebugScope and auctionConfig’s auction report buyer debug details.
-
For each (origin, aggregationCoordinator) → batchingScope of auctionConfig’s batching scope map:
-
Process contributions for a batching scope given batchingScope, origin, "
protected-audience
" and null.
-
-
Let batchingScopeMap be auctionConfig’s batching scope map.
-
Let tuple be (origin, aggregationCoordinator).
-
If batchingScopeMap[tuple] does not exist:
-
Set batchingScopeMap[tuple] to a new batching scope.
-
If aggregationCoordinator is not null, set the aggregation coordinator for a batching scope given aggregationCoordinator and batchingScopeMap[tuple].
-
-
Return batchingScopeMap[tuple].
PAExtendedHistogramContribution
contribution and a leading bid info leadingBidInfo, perform the
following steps. They return a PAHistogramContribution
.
-
Let bucket be contribution["
bucket
"]. -
If bucket is a
PASignalValue
, set bucket to the result of filling in the signal value given bucket, 2128−1 and leadingBidInfo. -
Let value be contribution["
value
"]. -
If value is a
PASignalValue
, set value to the result of filling in the signal value given value, 231−1 and leadingBidInfo. -
Let filledInContribution be a new
PAHistogramContribution
with the items:bucket
-
bucket
value
-
value
filteringId
-
contribution["
filteringId
"]
-
Return filledInContribution.
PASignalValue
value, an
integer maxAllowed and a leading bid info leadingBidInfo, perform the following steps. They return an integer.
-
Assert: value["
baseValue
"] is a valid signal base value. -
Let returnValue be the result of determining a signal’s numeric value given value["
baseValue
"] and leadingBidInfo. -
If value["
scale
"] exists, set returnValue to the result of multiplying value["scale
"] with returnValue. -
Set returnValue to the result of converting returnValue to an integer by truncating its fractional part.
-
If value["
offset
"] exists, set returnValue to the result of adding returnValue to value["offset
"]. -
Clamp returnValue to the range 0 to maxAllowed, inclusive, and return the result.
double
.
-
If signalBaseValue is "
winning-bid
":-
If leadingBidInfo’s leading bid is null, return 0.
-
Otherwise, return leadingBidInfo’s leading bid’s bid.
-
-
If signalBaseValue is "
highest-scoring-other-bid
":-
If leadingBidInfo’s highest scoring other bid is null, return 0.
-
Otherwise, return leadingBidInfo’s highest scoring other bid’s bid.
-
-
If signalBaseValue is "
script-run-time
":-
Return the number of milliseconds of CPU time that the calling function (e.g.
generateBid()
) took to run.
-
-
If signalBaseValue is "
signals-fetch-time
": Switch on the associated worklet function:generate-bid
-
Return the number of milliseconds it took for the trusted bidding signals fetch to complete, or 0 if no fetch was made.
score-ad
-
Return the number of milliseconds it took for the trusted scoring signals fetch to complete or 0 if no fetch was made.
report-result
report-win
-
Return 0.
Consider disallowing this in the latter two worklet functions.
-
If signalBaseValue is "
bid-reject-reason
":-
If the bid did not succeed purely because it didn’t meet the required k-anonymity threshold, return 8.
-
Let bidRejectReason be "
not-available
". -
If the seller provided a reject reason, set bidRejectReason to that value.
-
If bidRejectReason is:
- "
not-available
" -
Return 0.
- "
invalid-bid
" -
Return 1.
- "
bid-below-auction-floor
" -
Return 2.
- "
pending-approval-by-exchange
" -
Return 3.
- "
disapproved-by-exchange
" -
Return 4.
- "
blocked-by-publisher
" -
Return 5.
- "
language-exclusions
" -
Return 6.
- "
category-exclusions
" -
Return 7.
- None of the above values
-
Assert: false
Note: this enum value is validated in
scoreAd()
.Verify this once protected-audience/627 is resolved.
Once protected-audience/594 lands, update this mapping to align.
Consider disallowing this from reportWin() and reportResult().
- "
-
InterestGroupScriptRunnerGlobalScope
global, perform the following steps.
They return an interest group or null:
-
Switch on global’s type:
InterestGroupBiddingScriptRunnerGlobalScope
-
Return global’s interest group.
InterestGroupScoringScriptRunnerGlobalScope
-
Return null.
InterestGroupReportingScriptRunnerGlobalScope
-
Return global’s interest group.
ProtectedAudiencePrivateAggregationConfig
config, perform the following
steps. They return an aggregation coordinator, null or a DOMException
.
-
If config["
aggregationCoordinatorOrigin
"] does not exist, return null. -
Return the result of obtaining the Private Aggregation coordinator given config["
aggregationCoordinatorOrigin
"].
USVString
originString,
perform the following steps. They return an aggregation coordinator or a DOMException
.
-
Let url be the result of running the URL parser on originString.
-
If url is failure or null, return a new
DOMException
with name "SyntaxError
". -
Let origin be url’s origin.
-
If the result of determining if an origin is an aggregation coordinator given origin is false, return a new
DOMException
with name "DataError
". -
Return origin.
12. Privacy considerations
This section is non-normative.
12.1. Cross-site information disclosure
This API lets isolated contexts with access to cross-site data (i.e. Shared Storage worklets/Protected Audience script runners) send aggregatable reports over the network.
Aggregatable reports contain encrypted high entropy cross-site information, in the form of key-value pairs (i.e. contributions to a histogram). The information embedded in the contributions is arbitrary but can include things like browsing history and other cross-site activity. The API aims to protect this information from being passed from one site to another.
12.1.1. Restricted contribution processing
The histogram contributions are not exposed directly. Instead, they are encrypted so that they can only be processed by a trusted aggregation service. This trusted aggregation service sums the values across the reports for each key and adds noise to each of these values to produce ‘summary reports’.
The output of that processing will be an aggregated, noised histogram. The service ensures that any report can not be processed multiple times. Further, information exposure is limited by contribution budgets on the user agent. In principle, this framework can support specifying a noise parameter which satisfies differential privacy.
12.1.2. Unencrypted metadata
These reports also expose a limited amount of metadata, which is not based on cross-site data. The recipient of the report may also be able to observe side-channel information such as the time when the report was sent, or IP address of the sender.
12.1.3. Protecting against leaks via the number of reports
However, the number of reports with the given metadata could expose some cross-site information. To protect against this, the API delays sending reports by a randomized amount of time to make it difficult to determine whether a report was sent or not from any particular event. In the case that a context ID is supplied or a non-default filtering ID max bytes is specified, the API makes the number of reports sent deterministic (sending 'null reports' if necessary -- each containing only a contribution with a value of 0 in the payload). Additional mitigations may also be possible in the future, e.g. adding noise to the report count.
12.1.4. Protecting against leaks via payload size
The length of the payload could additionally expose some cross-site information, namely how many contributions are included. To protect against this, the payload is padded to a fixed number of contributions.
12.1.5. Temporary debugging mechanism
The enableDebugMode()
method allows for many of the
protections of this API to be bypassed to ease testing and integration.
Specifically, the contents of the payload, i.e. the histogram contributions, are
revealed in the clear when the debug mode is enabled. Optionally, a debug key
can also be set to associate the report with the calling context. In the future,
this mechanism will only be available for callers that are eligible to set
third-party cookies. In that case, the API caller already has the ability to
communicate information cross-site.
Tie enableDebugMode()
to third-party cookie
eligibility. [Issue #57]
12.1.6. Privacy parameters
The amount of information exposed by this API is a product of the privacy parameters used (e.g. contribution limits and the noise distribution used in the aggregation service). While we aim to minimize the amount of information exposed, we also aim to support a wide range of use cases. The privacy parameters are left implementation-defined to allow different and evolving choices in the tradeoffs between information exposure and utility.
12.2. Clearing site data
The aggregatable report cache as well as any contribution history data stored for the consume budget if permitted algorithm contain data about a user’s web activity. As such, user controls to delete this data are required, see clearing storage.
On the other hand, the contribution cache, the debug scope map and the pre-specified report parameters map only contain short-lived data tied to particular batching scopes and debug scopes, so controls are not required.
12.3. Reporting delay concerns
Delaying sending reports after API invocation can enable side-channel leakage in some situations.
12.3.1. Cross-network reporting origin leakage
A report may be stored while the browser is connected to one network but sent while the browser is connected to a different network, potentially enabling cross-network leakage of the reporting origin.
Example: A user runs the browser with a particular browsing profile on their home network. An aggregatable report with a particular reporting origin is stored with a report time in the future. After the report time is reached, the user runs the browser with the same browsing profile on their employer’s network, at which point the browser sends the report to the reporting origin. Although the report itself may be sent over HTTPS, the reporting origin may be visible to the network administrator via DNS or the TLS client hello (which can be mitigated with ECH). Some reporting origins may be known to operate only or primarily on sensitive sites, so this could leak information about the user’s browsing activity to the user’s employer without their knowledge or consent.
Possible mitigations include:
-
Only sending reports with a given reporting origin when the browser has already made a request to that origin on the same network: This prevents the network administrator from gaining additional information from the Private Aggregation API. However, it increases report loss and report delays, which reduces the utility of the API for the reporting origin. It might also increase the effectiveness of timing attacks, as the origin may be able to better link the report with the user’s request that allowed the report to be released.
-
Send reports immediately: This reduces the likelihood of a report being stored and sent on different networks. However, it increases the likelihood that the reporting origin can correlate the original API invocation to the report being sent, which weakens the privacy controls of the API, see Protecting against leaks via the number of reports.
-
Use a trusted proxy server to send reports: This effectively moves the reporting origin into the report body, so only the proxy server would be visible to the network administrator.
-
Require DNS over HTTPS: This effectively hides the reporting origin from the network administrator, but is likely impractical to enforce and is itself perhaps circumventable by the network administrator, e.g. by monitoring IP addresses instead.
12.3.2. User-presence tracking
The browser only tries to send reports while it is running and while it has internet connectivity (even without an explicit check for connectivity, naturally the report will fail to be sent if there is none), so receiving or not receiving a (serialized) aggregatable report at the original report time leaks information about the user’s presence. Additionally, because the report request inherently includes an IP address, this could reveal the user’s IP-derived location to the reporting origin, including at-home vs. at-work or approximate real-world geolocation, or reveal patterns in the user’s browsing activity.
Possible mitigations include:
-
Send reports immediately: This effectively eliminates the presence tracking, as the original request made to the reporting origin is in close temporal proximity to the report request. However, it increases the likelihood that the reporting origin can correlate the original API invocation to the report being sent, which weakens the privacy controls of the API, see Protecting against leaks via the number of reports.
-
Send reports immediately to a trusted proxy server, which would itself apply additional delay: This would effectively hide both the user’s IP address and their online-offline presence from the reporting origin.
13. Security considerations
This section is non-normative.
13.1. Same-origin policy
Writes to the aggregatable report cache, contribution cache, debug scope map and pre-specified report parameters map are attributed to the reporting origin and the data included in any report with a given reporting origin are generated with only data from that origin.
One notable exception is the consume budget if permitted algorithm which is implementation-defined and can consider contribution history from other origins. For example, the algorithm could consider all history from a particular site. This would be an explicit relaxation of the same-origin policy as multiple origins would be able to influence the API’s behavior. One particular risk of these kinds of shared limits is the introduction of denial of service attacks, where a group of origins could collude to intentionally consume all available budget, causing subsequent origins to be unable to access the API. This trades off security for privacy, in that the limits are there to reduce the efficacy of many origins colluding together to violate privacy. However, this security risk is lessened if the set of origins limited are all same site. User agents should consider these tradeoffs when choosing the consume budget if permitted algorithm.
13.2. Protecting the histogram contributions
As discussed above, the processing of histogram contributions is limited to protect privacy. This limitation relies on only the trusted aggregation service being able to access the unencrypted histogram contributions.
To ensure this, this API uses HPKE, a modern encryption specification. Additionally, each user agent is encouraged to require regular key rotation by the aggregation service. This limits the amount of data encrypted with the same key and thus the amount of vulnerable data in the case of a key being compromised.
While not specified here, each user agent is strongly encouraged to consider the security of any aggregation service design before allowing its public keys to be returned by obtain the public key for encryption.