Private Aggregation API

Unofficial Proposal Draft,

More details about this document
This version:
https://patcg-individual-drafts.github.io/private-aggregation-api
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google)

Abstract

A generic API for measuring aggregate, cross-site data in a privacy preserving manner. The potentially identifying cross-site data is encapsulated into aggregatable reports. To prevent leakage, this data is encrypted, ensuring it can only be processed by an aggregation service. During processing, this service will add noise and impose limits on how many queries can be performed.

Status of this document

This document is an individual draft proposal. It has not been adopted by the Private Advertising Technology Community Group, but it may be discussed in that CG’s meetings. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

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:

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 filteringId = 0;
};

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.

The contributeToHistogram(PAHistogramContribution contribution) method steps are:
  1. If contribution["bucket"] is not contained in the range 0 to 2128, exclusive, throw a RangeError.

  2. If contribution["value"] is negative, throw a RangeError.

  3. Let scopingDetails be this's scoping details.

  4. Let batchingScope be the result of running scopingDetailsget batching scope steps.

  5. Let filteringIdMaxBytes be the default filtering ID max bytes.

  6. If pre-specified report parameters map[batchingScope] exists:

    1. Set filteringIdMaxBytes to pre-specified report parameters map[batchingScope]'s filtering ID max bytes.

  7. If contribution["filteringId"] is not contained in the range 0 to 256filteringIdMaxBytes, exclusive, throw a RangeError.

  8. Let entry be a new contribution cache entry with the items:

    contribution

    contribution

    batching scope

    batchingScope

    debug scope

    The result of running scopingDetailsget debug scope steps.

  9. 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]

The enableDebugMode(optional PADebugModeOptions options) method steps are:
  1. Let scopingDetails be this's scoping details.

  2. Let debugScope be the result of running scopingDetailsget debug scope steps.

  3. If debug scope map[debugScope] exists, throw a "DataError" DOMException.

    Note: This would occur if enableDebugMode() has already been run for this debug scope.

  4. Let debugKey be null.

  5. If options was given:

    1. If options["debugKey"] is not contained in the range 0 to 264, exclusive, throw a "DataError" DOMException.

    2. Set debugKey to options["debugKey"].

  6. Let debugDetails be a new debug details with the items:

    enabled

    true

    key

    debugKey

  7. 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.

  8. 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.

Shared Storage only allows Private Aggregation when an operation is being invoked, not in the top-level context:
class ExampleOperation {
  async run(data) {
    privateAggregation.contributeToHistogram(...)  // This is allowed.
  }
}
register('example-operation', ExampleOperation);

privateAggregation.contributeToHistogram(...)  // This would cause an error.

So, Shared Storage sets the scoping details immediately after the initial execution of the module script is complete.

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:

  1. Shared Storage and

  2. Protected Audience.

4. Structures

4.1. Batching scope

A batching scope is a unique internal value that identifies which PAHistogramContributions 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 which PAHistogramContributions 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

A PAHistogramContribution

batching scope

A batching scope

debug scope

A 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 PAHistogramContributions

api

A context type

report ID

A string

debug details

A debug details

aggregation coordinator

An 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 the PrivateAggregation 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.

To get the privateAggregation given a PrivateAggregation this:
  1. Let scopingDetails be this’s scoping details.

  2. 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).

  3. If this’s allowed to use is false, throw an "InvalidAccessError" DOMException.

  4. Return this.

Ensure errors are of an appropriate type, e.g. InvalidAccessError is deprecated.

To append an entry to the contribution cache given a contribution cache entry entry:
  1. Append entry to the contribution cache.

To get a debug details given a debug scope debugScope, perform the following steps. They return a debug details.
  1. If debug scope map[debugScope] exists, return debug scope map[debugScope].

  2. Otherwise, return a new debug details.

To mark a debug scope complete given a debug scope debugScope and an optional debug details or null debugDetailsOverride (default null):
  1. Let debugDetails be debugDetailsOverride.

  2. If debug scope map[debugScope] exists:

    1. Assert: debugDetailsOverride is null.

      Note: The override can be provided if the debug details have not been set otherwise.

    2. Set debugDetails to debug scope map[debugScope].

    3. Remove debug scope map[debugScope].

    4. If debugDetails’s key is not null, assert: debugDetails’s enabled is true.

  3. If debugDetails is null, set debugDetails to a new debug details.

  4. For each entry of the contribution cache:

    1. If entry’s debug scope is debugScope, set entry’s debug details to debugDetails.

To determine if a report should be sent deterministically given a pre-specified report parameters preSpecifiedParams, perform the following steps. They return a boolean:
  1. If preSpecifiedParamscontext ID is not null, return true.

  2. If preSpecifiedParamsfiltering ID max bytes is not the default filtering ID max bytes, return true.

  3. 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.

To process contributions for a batching scope given a batching scope batchingScope, an origin reportingOrigin, a context type contextType and a moment or null timeout:
  1. Let batchEntries be a new list.

  2. For each entry of the contribution cache:

    1. If entry’s batching scope is batchingScope:

      1. 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.

      2. Append entry to batchEntries.

  3. Let aggregationCoordinator be the default aggregation coordinator.

  4. If aggregation coordinator map[batchingScope] exists:

    1. Set aggregationCoordinator to aggregation coordinator map[batchingScope].

    2. Remove aggregation coordinator map[batchingScope].

  5. Let preSpecifiedParams be a new pre-specified report parameters.

  6. If pre-specified report parameters map[batchingScope] exists:

    1. Set preSpecifiedParams to pre-specified report parameters map[batchingScope].

    2. Remove pre-specified report parameters map[batchingScope].

  7. Let isDeterministicReport be the result of determining if a report should be sent deterministically given preSpecifiedParams.

  8. If isDeterministicReport is false, assert: timeout is null.

    Note: Timeouts can only be used for deterministic reports.

  9. If batchEntries is empty and isDeterministicReport is false, return.

  10. Let batchedContributions be a new ordered map.

  11. For each entry of batchEntries:

    1. Remove entry from the contribution cache.

    2. Let debugDetails be entry’s debug details.

    3. If batchedContributions[debugDetails] does not exist:

      1. Set batchedContributions[debugDetails] to a new list.

    4. Append entry’s contribution to batchedContributions[debugDetails].

  12. If batchedContributions is empty:

    1. Let debugDetails be a new debug details.

    2. Set batchedContributions[debugDetails] to a new list.

  13. For each debugDetailscontributions of batchedContributions:

    1. 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.

To determine if an origin is an aggregation coordinator given an origin origin, perform the following steps. They return a boolean.
  1. Return whether origin is an aggregation coordinator.

To obtain the Private Aggregation coordinator given a USVString originString, perform the following steps. They return an aggregation coordinator or a DOMException.
  1. Let url be the result of running the URL parser on originString.

  2. If url is failure or null, return a new DOMException with name "SyntaxError".

    Consider throwing an error if the path is not empty.

  3. Let origin be url’s origin.

  4. If the result of determining if an origin is an aggregation coordinator given origin is false, return a new DOMException with name "DataError".

  5. Return origin.

To set the aggregation coordinator for a batching scope given an origin origin and a batching scope batchingScope:
  1. Assert: origin is an aggregation coordinator.

  2. 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.

To set the pre-specified report parameters for a batching scope given a pre-specified report parameters params and a batching scope batchingScope:
  1. Let contextId be paramscontext ID.

  2. Assert: contextId is null or contextId’s length is not larger than 64.

  3. Let filteringIdMaxBytes be params filtering ID max bytes.

  4. Assert: filteringIdMaxBytes is contained in the valid filtering ID max bytes range

  5. Set pre-specified report parameters map[batchingScope] to params.

9.2. Scheduling reports

To perform the report creation and scheduling steps with an origin reportingOrigin, a context type api, a list of PAHistogramContributions contributions, a debug details debugDetails, an aggregation coordinator aggregationCoordinator, a pre-specified report parameters preSpecifiedParams and a moment or null timeout:
  1. Assert: reportingOrigin is a potentially trustworthy origin.

  2. 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.

  3. Let mergedContributions be a new list.

  4. For each contribution of contributions:

    1. Let hasProcessedContribution be false.

    2. For each mergedContribution of mergedContributions:

      1. If contribution and mergedContribution have both the same bucket and the same filteringId:

        1. Add contribution’s value to mergedContribution’s value.

        2. Set hasProcessedContribution to true.

        3. Break.

    3. If hasProcessedContribution is false, append contribution to mergedContributions.

  5. Let truncatedContributions be a new list.

  6. If mergedContributions has a size greater than maximum report contributions[api]:

    1. For each n of the range 0 to maximum report contributions[api], exclusive:

      1. Append mergedContributions[n] to truncatedContributions.

  7. Otherwise, set truncatedContributions to mergedContributions.

  8. Let contributionSum be 0.

  9. For each contribution of truncatedContributions:

    1. Assert: contribution["value"] is non-negative.

    2. Add contribution["value"] to contributionSum.

  10. Let currentWallTime be the current wall time.

  11. 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.

  12. If sufficientBudget is false:

    1. Let isDeterministicReport be the result of determining if a report should be sent deterministically given preSpecifiedParams.

    2. If isDeterministicReport is false, return.

    3. Empty truncatedContributions.

  13. Let report be the result of obtaining an aggregatable report given reportingOrigin, api, truncatedContributions, debugDetails, aggregationCoordinator, preSpecifiedParams, timeout and currentWallTime.

  14. Append report to the user agent’s aggregatable report cache.

To consume budget if permitted given an integer value, an origin origin, a context type api and a moment currentTime, perform implementation-defined steps. They return a boolean, which indicates whether there is sufficient 'contribution budget' left to send the requested contribution value (or multiple contributions with a sum of values equal to value). This budget should be bound to usage over time, e.g. the contribution sum over the last 24 hours. The algorithm should assume that the contribution will be sent if and only if true is returned, i.e. it should consume the budget in that case. If value is zero, this algorithm should return true.
To obtain an aggregatable report given an origin reportingOrigin, a context type api, a list of PAHistogramContributions 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.
  1. Assert: reportingOrigin is a potentially trustworthy origin.

  2. Let reportTime be the result of running obtain a report delivery time given currentTime and timeout.

  3. 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

    preSpecifiedParamscontext ID

    filtering ID max bytes

    preSpecifiedParamsfiltering ID max bytes

    queued

    false

  4. Return report.

To obtain a report delivery time given a moment currentTime and a moment or null timeout, perform the following steps. They return a moment.
  1. If timeout is not null:

    1. Return timeout.

  2. If automation local testing mode enabled is true, return currentTime.

  3. Let r be a random double between 0 (inclusive) and 1 (exclusive) with uniform probability.

  4. 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.

To attempt to queue reports for sending given a list of aggregatable reports reports:
  1. For each report of reports, run these steps in parallel:

    1. Run these steps, but abort when the user agent shuts down:

      1. If report’s queued value is true, return.

      2. Set report’s queued value to true.

      3. Let currentWallTime be the current wall time.

      4. 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.

      5. Wait until the current wall time is equal to or after report’s report time.

      6. 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.

      7. Run attempt to deliver a report with report.

    2. 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.

To attempt to deliver a report given an aggregatable report report:
  1. Let url be the result of obtaining a reporting endpoint given report’s reporting origin and report’s api.

  2. Let data be the result of serializing an aggregatable report given report.

  3. If data is an error, remove report from the aggregatable report cache.

    Do we need to queue this task?

  4. Let request be the result of creating a report request given url and data.

  5. Queue a task to fetch request with processResponse being the following steps:

    1. Let shouldRetry be an implementation-defined boolean. The value should be false if no error occurred.

    2. If shouldRetry is true:

      1. Set report’s report time to the current wall time plus an implementation-defined non-negative duration.

      2. Set report’s queued value to false.

    3. Otherwise, remove report from the aggregatable report cache.

To obtain a reporting endpoint given an origin reportingOrigin and context type api, perform the following steps. They return a URL.
  1. Assert: reportingOrigin is a potentially trustworthy origin.

  2. Let path be the concatenation of «".well-known/private-aggregation/report-", api».

    Register this well-known directory. [Issue #67]

  3. Let base be the result on running the URL parser on the serialization of reportingOrigin.

  4. Assert: base is not failure.

  5. Let result be the result of running the URL parser on path with base.

  6. Assert: result is not failure.

  7. Return result.

To create a report request given a URL url and a byte sequence body:
  1. 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"

  2. Return request.

9.4. Serializing reports

Note: This section is largely copied from the Attribution Reporting API spec, adapting as necessary.

To serialize an aggregatable report given an aggregatable report report, perform the following steps. They return a byte sequence or an error.
  1. Let aggregationServicePayloads be the result of obtaining the aggregation service payloads given report.

  2. If aggregationServicePayloads is an error, return aggregationServicePayloads.

  3. 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.

  4. Let debugKey be report’s debug details's key.

  5. If debugKey is not null, set data["debug_key"] to debugKey.

  6. Let contextId be report’s context ID.

  7. If contextId is not null, set data["context_id"] to contextId.

  8. Return the byte sequence resulting from executing serialize an infra value to JSON bytes on data.

To obtain the aggregation service payloads given an aggregatable report report, perform the following steps. They return a list of maps or an error.
  1. Let publicKeyTuple be the result of obtaining the public key for encryption given report’s aggregation coordinator.

  2. If publicKeyTuple is an error, return publicKeyTuple.

  3. Let (pkR, keyId) be publicKeyTuple.

  4. Let plaintextPayload be the result of obtaining the plaintext payload given report.

  5. Let sharedInfo be the result of obtaining a report’s shared info given report.

  6. Let encryptedPayload be the result of encrypting the payload given plaintextPayload, pkR and sharedInfo.

  7. If encryptedPayload is an error, return encryptedPayload.

  8. Let aggregationServicePayloads be a new list.

  9. Let aggregationServicePayload be an ordered map of the following key/value pairs:

    "key_id"

    keyId

    "payload"

    encryptedPayload, base64 encoded

  10. If report’s debug details's enabled field is true:

    1. Set aggregationServicePayload[debug_cleartext_payload] to plaintextPayload, base64 encoded.

  11. Append aggregationServicePayload to aggregationServicePayloads.

  12. Return aggregationServicePayloads.

To obtain the public key for encryption given an aggregation coordinator aggregationCoordinator, perform the following steps. They return a tuple consisting of a public key and a string, or an error.
  1. Let url be a new URL record.

  2. Set url’s scheme to aggregationCoordinator’s scheme.

  3. Set url’s host to aggregationCoordinator’s host.

  4. Set url’s port to aggregationCoordinator’s port.

  5. Set url’s path to «".well-known", "aggregation-service", "v1", "public-keys"».

  6. 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.

To obtain the plaintext payload given an aggregatable report report, perform the following steps. They return a byte sequence.
  1. Let payloadData be a new list.

  2. Let contributions be report’s contributions.

  3. Let maxContributions be maximum report contributions[api].

  4. Assert: contributionssize is not greater than maxContributions.

  5. While contributionssize is less than maxContributions:

    1. Let nullContribution be a new PAHistogramContribution with the items:

      bucket

      0

      value

      0

      filteringId

      0

    2. Append nullContribution to contributions.

    Note: This padding protects against the number of contributions being leaked through the encrypted payload size, see discussion below.

  6. For each contribution of report’s contributions:

    1. Let filteringIdMaxBytes be report’s filtering id max bytes.

    2. Assert: contribution["filteringId"] is contained in the range 0 to 256filteringIdMaxBytes, exclusive.

    3. 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.

    4. Append contributionData to payloadData.

  7. Let payload be an ordered map of the following key/value pairs:

    "data"

    payloadData

    "operation"

    "histogram"

  8. Return the byte sequence resulting from CBOR encoding payload.

To encrypt the payload given a byte sequence plaintextPayload, public key pkR and a string sharedInfo, perform the following steps. They return a byte sequence or an error.
  1. Let info be the result of UTF-8 encoding the concatenation of « "aggregation_service", sharedInfo ».

  2. 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.

  3. 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.

  4. Let aad be `` (an empty byte sequence).

  5. 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.

  6. 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.

  7. Return the byte sequence encryptedPayload.

To encode an integer for the payload given an integer intToEncode and an integer byteLength, return the representation of intToEncode as a big-endian byte sequence of length byteLength, left padding with zeroes as necessary.
To obtain a report’s shared info given an aggregatable report report, perform the following steps. They return a string.
  1. Let scheduledReportTime be the duration from the UNIX epoch to report’s original report time.

  2. 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"

  3. 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
The remote end steps are:
  1. If parameters is not a JSON-formatted Object, return a WebDriver error with error code invalid argument.

  2. Let enabled be the result of getting a property named "enabled" from parameters.

  3. If enabled is undefined or is not a boolean, return a WebDriver error with error code invalid argument.

  4. Set automation local testing mode enabled to enabled.

  5. Return success with data null.

Note: Without this, aggregatable reports would be subject to delays, making testing difficult.

11. Privacy considerations

This section is non-normative.

11.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.

11.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.

11.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.

11.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.

11.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.

11.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]

11.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.

11.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.

11.3. Reporting delay concerns

Delaying sending reports after API invocation can enable side-channel leakage in some situations.

11.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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

11.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:

  1. 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.

  2. 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.

12. Security considerations

This section is non-normative.

12.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.

12.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.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[ATTRIBUTION-REPORTING-API]
Attribution Reporting. Draft Community Group Report. URL: https://wicg.github.io/attribution-reporting-api/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[ENCODING]
Anne van Kesteren. Encoding Standard. Living Standard. URL: https://encoding.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PERMISSIONS-POLICY-1]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[RFC8949]
C. Bormann; P. Hoffman. Concise Binary Object Representation (CBOR). December 2020. Internet Standard. URL: https://www.rfc-editor.org/rfc/rfc8949
[RFC9180]
R. Barnes; et al. Hybrid Public Key Encryption. February 2022. Informational. URL: https://www.rfc-editor.org/rfc/rfc9180
[SECURE-CONTEXTS]
Mike West. Secure Contexts. URL: https://w3c.github.io/webappsec-secure-contexts/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WebCryptoAPI]
Mark Watson. Web Cryptography API. URL: https://w3c.github.io/webcrypto/
[WebDriver]
Simon Stewart; David Burns. WebDriver. URL: https://w3c.github.io/webdriver/
[WEBDRIVER2]
Simon Stewart; David Burns. WebDriver. URL: https://w3c.github.io/webdriver/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[RFC8484]
P. Hoffman; P. McManus. DNS Queries over HTTPS (DoH). October 2018. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8484
[RFC8615]
M. Nottingham. Well-Known Uniform Resource Identifiers (URIs). May 2019. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8615

IDL Index

[Exposed=(InterestGroupScriptRunnerGlobalScope,SharedStorageWorklet),
 SecureContext]
interface PrivateAggregation {
  undefined contributeToHistogram(PAHistogramContribution contribution);
  undefined enableDebugMode(optional PADebugModeOptions options = {});
};

dictionary PAHistogramContribution {
  required bigint bucket;
  required long value;
  bigint filteringId = 0;
};

dictionary PADebugModeOptions {
  required bigint debugKey;
};

Issues Index

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.
Ensure errors are of an appropriate type, e.g. InvalidAccessError is deprecated.
Consider accepting an array of contributions. [Issue #44]
Ensure errors are of an appropriate type, e.g. InvalidAccessError is deprecated.
Unique internal value is not an exported definition. See infra/583.
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.
Elsewhere, link to definition when using user agent.
Consider adding more constants.
This would ideally be replaced by a more descriptive algorithm in Infra. See infra/201.
Consider improving developer ergonomics here (e.g. a way to detect this case).
Ensure errors are of an appropriate type, e.g. InvalidAccessError is deprecated.
Consider throwing an error if the path is not empty.
Elsewhere, surround algorithms in a <div algorithm> block to match, and add styling for all algorithms per bikeshed/1472.
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.
Do we have to use the queue a task algorithm here?
Do we need to queue this task?
Register this well-known directory. [Issue #67]
Specify this in terms of fetch. Add details about which encryption standards to use, length requirements, etc.
Tie enableDebugMode() to third-party cookie eligibility. [Issue #57]