Authors:
- Peiwen Hu
- Russ Hamilton
Protected Audience is a privacy-preserving API that facilitates interest group based advertising. Trusted servers in Protected Audience are used to add real-time signals into ad selection for both buyers and sellers. The Protected Audience proposal specifies that these trusted servers should provide basic key-value lookups to facilitate fetching these signals but do no event-level logging or have other side effects.
This explainer proposes APIs for trusted servers. The trust model explainer covers the service in more detail, including threat models, trust models, and system design.
As mentioned in the main explainer, "As a temporary mechanism during the First Experiment timeframe, the buyer and seller can fetch these bidding signals from any server, including one they operate themselves (a "Bring Your Own Server" model). However, in the final version after the removal of third-party cookies, the request will only be sent to a trusted key-value-type server."
Designed on the foundation of the BYOS API, the trusted server API includes a few technical changes to facilitate communication with a key-value server running in a TEE (Trust Execution Environment). From the Trusted Key Value Server development perspective, the BYOS API is considered "Version 1". For the BYOS API, please refer to the Protected Audience Spec as the source of truth.
This doc is focused on the Trusted Key Value Server API. The API version starts with version 2. The latest version is version 2.
Keys may exist in different namespaces. The namespaces help the server identify keys needed from request query strings and prevent potential key collision, even though the keys may be unique across namespaces in today’s use cases.
- For a DSP, there are:
keys
andinterestGroupNames
. - For an SSP, there are
renderUrls
andadComponentRenderUrls
.
The Protected Audience explainer provides more context about these namespaces.
Query versions 2 and beyond are specifically designed for the trusted TEE key/value service as part of the trust model, published here for explanatory purposes. BYOS servers do not need to implement these.
In this version we present a protocol that enables trusted communication between Chrome and the trusted key/value service. The protocol assumes a functioning trust model as described in the key/value service explainer is in place, primarily that only service implementations recognized by Privacy Sandbox can obtain private decryption keys, and the mechanism for the client and service to obtain cryptographic keys is available but outside the scope of this document.
On a high level, the protocol is similar to the Bidding & Auction services protocol, with (bidirectional) HPKE encryption.
- TLS is used to ensure that the client is talking to the real service operator (identified by the domain). FLEDGE enforces that the origin of the trusted server matches the config owner (interest group owner for the trusted bidding signal server or auction config’s seller for the trusted scoring signal server).
- HPKE is used to ensure that the message is only visible to the approved versions of services inside the trusted execution environment (TEE).
- The reason to use HPKE is that the request must be encrypted and can only be decrypted by the trusted service itself. A notable alternative protocol is TLS which in addition to the domain validation, validates the service identity attestation. However, attestation verification as part of TLS can present performance challenges and is still being evaluated.
- The protocol to configure the HPKE is roughly based on the Oblivious HTTP proposal.
For more information on the design, please refer to the trust model explainer.
HTTPS is used to transport data. The method is POST
.
The HTTP POST body is encrypted.
We will use Oblivious HTTP with the following configuration for encryption:
- 0x0020 DHKEM(X25519, HKDF-SHA256) for KEM (Key encapsulation mechanisms)
- 0x0001 HKDF-SHA256 for KDF (key derivation functions)
- AES256GCM for AEAD scheme.
Since we are repurposing the OHTTP encapsulation mechanism, we are required to define new media types:
- The OHTTP request media type is “message/ad-auction-trusted-signals-request”
- The OHTTP response media type is “message/ad-auction-trusted-signals-response”
Note that these media types are concatenated with other fields when creating the HPKE encryption context, and are not HTTP content or media types.
Inside the ciphertext, the request/response is framed with a 5 byte header, where the first byte is the format+compression byte, and the following 4 bytes are the length of the request message in network byte order. Then the request is zero padded to a set of pre-configured lengths.
The lower 2 bits are used for compression specification. The higher 6 bits are currently unused.
0x00
- CBOR no compression0x01
- CBOR compressed in brotli0x02
- CBOR compressed in gzip
For request, the byte value is 0x00. For response, the byte value depends on the “acceptCompression” field in the request and the server behavior.
Padding is applied with sizes as multiples of 2^n KBs ranging from 0 to 2MB. So the valid response sizes will be [0, 128B, 256B, 512B, 1KB, 2KB, 4KB, 8KB, 16KB, 32KB, 64KB, 128KB, 256KB, 512KB, 1MB, 2MB].
Core request and response data structures are all in CBOR.
The schema below is defined following the spec by https://json-schema.org/.
The API is generic, agnostic to DSP or SSP use cases.
Requests are not compressed. Compression could save size but may add latency. Request size is presumed to be small, so compression may not improve overall performance. Version 2 will initially not implement compression for the request but more experimentation is necessary to determine if compression is an overall win and should be implemented in the future.
In the request, one major difference from V1/BYOS is that the keys are now grouped. There is a tree-like hierarchy:
- Each request contains one or more partitions. Each partition is a collection of keys that can be processed together by the service without any potential privacy leakage (For example, if the server uses User Defined Functions to process, one UDF call can only process one partition). Keys from one interest group must be in the same partition. Keys from different interest groups with the same joining site may or may not be in the same partition, so the server User Defined Functions should not make any assumptions based on that.
- Each partition contains one or more key groups. Each key group has its unique attributes among all key groups in the partition. The attributes are represented by a list of “Tags”. Besides tags, the key group contains a list of keys to look up.
- Each partition has a unique id.
- Each partition has a compression group field. Results of partitions belonging to the same compression group can be compressed together in the response. Different compression groups must be compressed separately. See more details below. The expected use case by the client is that interest groups from the same joining origin and owner can be in the same compression group.
{
"title": "tkv.request.v2.Request",
"description": "Key Value Service GetValues request",
"type": "object",
"additionalProperties": false,
"properties": {
"acceptCompression": {
"type": "array",
"items": {
"type": "string",
"description": "must contain at least one of none, gzip, brotli"
},
"description": "Algorithm accepted by the browser for the response."
},
"partitions": {
"description": "A list of partitions. Each must be processed independently. Accessible by UDF.",
"type": "array",
"items": {
"title": "tkv.request.v2.Partition",
"description": "Single partition object. A collection of keys that can be processed together",
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique id of the partition in this request",
"type": "unsigned integer"
},
"compressionGroupId": {
"description": "Unique id of a compression group in this request. Only partitions belonging to the same compression group will be compressed together in the response",
"type": "unsigned integer"
},
"metadata": {
"title": "tkv.request.v2.PartitionMetadata",
"description": "metadata",
"type": "object",
"additionalProperties": false,
"properties": {
"hostname": {
"description": "The hostname of the top-level frame calling runAdAuction().",
"type": "string"
},
"experimentGroupId": {
"type": "string"
},
"slotSize": {
"description": "Available if trustedBiddingSignalsSlotSizeMode=slot-size. In the form of <width>,<height>",
"type": "string"
},
"allSlotsRequestedSizes": {
"description": "Available if trustedBiddingSignalsSlotSizeMode=all-slots-requested-sizes. In the form of <width1>,<height1>,<width2>,<height2>,...",
"type": "string"
}
}
},
"arguments": {
"type": "array",
"items": {
"title": "tkv.request.v2.Argument",
"description": "One group of keys and common attributes about them",
"type": "object",
"additionalProperties": false,
"properties": {
"tags": {
"description": "List of tags describing this group's attributes",
"type": "array",
"items": {
"type": "string"
}
},
"data": {
"type": "array",
"description": "List of keys to get values for",
"items": {
"type": "string"
}
}
}
}
}
},
"required": [
"id",
"compressionGroupId",
"arguments"
]
}
}
},
"required": [
"partitions"
]
}
Tag category | Category description | Tag | Description |
Namespace | Each key group has exactly one tag from this category. | interestGroupNames | Names of interest groups in the encompassing partition. |
keys | “keys” is a list of trustedBiddingSignalsKeys strings. | ||
renderUrls | Similarly, sellers may want to fetch information about a specific creative, e.g. the results of some out-of-band ad scanning system. This works in much the same way, with the base URL coming from the trustedScoringSignalsUrl property of the seller's auction configuration object. | ||
adComponentRenderUrls |
Example trusted bidding signals request from Chrome:
{
"acceptCompression": [
"none",
"gzip"
],
"partitions": [
{
"id": 0,
"compressionGroupId": 0,
"metadata": {
"hostname": "example.com",
"experimentGroupId": "12345",
"slotSize": "100,200",
},
"arguments": [
{
"tags": [
"interestGroupNames"
],
"data": [
"InterestGroup1"
]
},
{
"tags": [
"keys"
],
"data": [
"keyAfromInterestGroup1",
"keyBfromInterestGroup1"
]
}
]
},
{
"id": 1,
"compressionGroupId": 0,
"arguments": [
{
"tags": [
"interestGroupNames"
],
"data": [
"InterestGroup2",
"InterestGroup3"
]
},
{
"tags": [
"keys"
],
"data": [
"keyMfromInterestGroup2",
"keyNfromInterestGroup3"
]
}
]
}
]
}
The response is compressed. Due to security and privacy reasons the compression is applied independently to each compression group. That means, The response object mainly contains a list of compressed blobs, each for one compression group. Each blob is for outputs of one or more partitions, sharing the same compressionGroup value as specified in the request.
{
"title": "tkv.response.v2.Response",
"type": "object",
"additionalProperties": false,
"properties": {
"compressionGroups": {
"type": "array",
"items": {
"title": "tkv.response.v2.CompressedCompressionGroup",
"type": "object",
"description": "Object for a compression group, compressed using the algorithm specified in the request",
"additionalProperties": false,
"properties": {
"compressionGroupId": {
"type": "unsigned integer"
},
"ttl_ms": {
"description": "Adtech-specified TTL for client-side caching. In milliseconds. Unset means no caching.",
"type": "unsigned integer"
},
"content": {
"description": "compressed CBOR binary string. For details see compressed response content schema: tkv.response.v2.CompressionGroup",
"type": "byte string"
}
}
}
}
}
}
The content of each compressed blob is a CBOR list of partition outputs. This object contains actual key value results for partitions in the corresponding compression group.
{
"type": "array",
"items": {
"title": "tkv.response.v2.PartitionOutput",
"description": "Output for one partition",
"type": "object",
"properties": {
"id": {
"description": "Unique id of the partition from the request",
"type": "unsigned integer"
},
"dataVersion" {
"description": "An optional field to indicate the state of the data that generated this response, which will then be available in bid generation/scoring and reporting.",
"type": "unsigned integer"
},
"keyGroupOutputs": {
"type": "array",
"items": {
"title": "tkv.response.v2.KeyGroupOutput",
"title": "Output for one key group",
"type": "object",
"additionalProperties": false,
"properties": {
"tags": {
"description": "Attributes of this key group.",
"type": "array",
"items": {
"description": "List of tags describing this key group's attributes",
"type": "string"
}
},
"keyValues": {
"description": "If a keyValues object exists, it must at least contain one key-value pair. If no key-value pair can be returned, the key group should not be in the response.",
"type": "object",
"patternProperties": {
".*": {
"description": "One value to be returned in response for one key",
"type": "object",
"additionalProperties": false,
"properties": {
"value": {
"type": [
"text string"
]
}
},
"required": [
"value"
]
}
}
}
}
}
}
}
}
}
Example:
[
{
"id": 0,
"dataVersion": 102,
"keyGroupOutputs": [
{
"tags": [
"interestGroupNames"
],
"keyValues": {
"InterestGroup1": {
"value": "{\"priorityVector\":{\"signal1\":1},\"updateIfOlderThanMs\": 10000}"
}
}
},
{
"tags": [
"keys"
],
"keyValues": {
"keyAfromInterestGroup1": {
"value": "valueForA"
},
"keyBfromInterestGroup1": {
"value":"[\"value1ForB\",\"value2ForB\"]"
}
}
}
]
}
]
Structured keys are keys that the browser is aware of and the browser can use the response to do additional processing. The value of these keys must abide by the following schema for the browser to successfully parse them.
Note that they must be serialized to string when stored as the value.
For values for keys from the interestGroupNames
namespace, they must conform to the following schema, prior to being serialized to string:
{
"title": "tkv.response.v2.InterestGroupResponse",
"description": "Format for value of keys in groups tagged 'interestGroupNames'",
"type": "object",
"additionalProperties": false,
"properties": {
"priorityVector": {
"type": "object",
"patternProperties": {
".*": {
"description": "signals",
"type": "number"
}
}
},
"updateIfOlderThanMs": {
"description": "This optional field specifies that the interest group should be updated if the interest group hasn't been joined or updated in a duration of time exceeding `updateIfOlderThanMs` milliseconds. Updates that ended in failure, either parse or network failure, are not considered to increment the last update or join time. An `updateIfOlderThanMs` that's less than 10 minutes will be clamped to 10 minutes.",
"type": "unsigned integer"
}
}
}
Example:
{
"priorityVector": {
"signal1": 1,
"signal2": 2
},
"updateIfOlderThanMs": 10000
}
For the K/V service, plaintext response payload before compression must not be more than 8MB. 8MB is a preliminary target and can be changed in the future if there is a need and no significant negative impact to the browser.
To be supported in the future.
The system will provide multiple private APIs and procedures, for loading data and user-defined functions whose model is described in detail in the Key/Value server design explainer.
These APIs and procedures are ACLs controlled by the ad tech operator of the system, for only the operator (and their designated parties) to use.
The key/value server code is available on its Privacy Sandbox github repo which reflects the most recent API and procedures. As an example, the data loading guide has specific instructions on integrating with the data ingestion procedure. The procedure may be based on the actual storage medium of the dataset, e.g., the server can read data from data files from a prespecified location.
As the FLEDGE API describes the client side flow, the APIs and procedures related to the server system design and implementation will have the specification posted and updated in the key/value server’s github repo.