Private State Token API

Draft Community Group Report,

This version:
https://wicg.github.io/trust-token-api/
Issue Tracking:
GitHub
Editors:
(Google)
(Google)
Participate:
GitHub WICG/trust-token-api (new issue, open issues)
Commits:
GitHub spec.bs commits

Abstract

The Private State Token API is a web platform API that allows propagating a limited amount of signals across sites, using the Privacy Pass protocol as an underlying primitive.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. 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. Goals

The goal of the Private State Tokens is to transfer a limited amount of signals across sites through time in a privacy preserving manner. It achieves this using a variant of the privacy pass protocol [PRIVACY-PASS-ISSUANCE-PROTOCOL] specified in the working documents of the IETF Privacy Pass Working Group [PRIVACY-PASS-WG]. Private State Tokens can be considered to be a web platform implementation of a variant of Privacy Pass.

The spec introduces a new field in request dictionary to support token operations. It describes how Private State Tokens are utilized through this new dictionary.

2. Background

The Private State Token API provides a mechanism for anonymous authentication. The API provided by the user agent does not authenticate clients, instead it facilitates transfer of authentication information.

Authentication of the clients and token signing are both carried by the same entity referred to as the issuer. This is the joint attester and issuer architecture described in [PRIVACY-PASS-ARCHITECTURE], [PRIVACY-PASS-AUTH-SCHEME].

User agents store tokens in persistent storage. Navigated origins might fetch/spend tokens in first party contexts or include third party code that fetch/spend tokens. Spending tokens is called redeeming.

Origins may ask the user agent to fetch tokens from the issuers of their choice. Tokens can be redeemed from a different origin than the fetching one.

Private State Tokens API performs cross site anonymous authentication without using linkable state carrying cookies [RFC6265]. Cookies do provide cross site authentication, however, they fail to provide anonymity.

Cookies store large amounts of information. [RFC6265] requires at least 4096 bytes per cookie and 50 cookies per domain. This means an origin has 50 x 4096 x 2^8 unique identifiers at its disposal. When backed with back end databases, a server can store arbitrary data for that many unique users/sessions.

Compared to a cookie, the amount of data stored in a Private State Token is very limited. A token stores a value from a set of six values (think of a value of an enum type of six possible values). Hence a token stores data between 2 and 3 bits (4 < 6 < 8). This is very small compared to 4096 bytes a cookie can store.

Moreover, Private State Tokens API use cryptographic protocols that prevents origins from tracking which tokens they issue to which user. When presented with their tokens, issuers can verify they issued them but cannot link the tokens to the context of their issuance. Cookies do not have this property.

Unlike cookies, storing multiple tokens from an issuer does not deteriorate privacy of the user due to the unlinkability of the tokens. The Private State Token API allows at most 2 different issuers in a top level origin. This is to limit the information stored for a user when the issuers are collaborating.

Private State Token operations rely on [FETCH]. A fetch request corresponding to a specific Private State Token operation can be created and used as a parameter to the fetch function.

3. Issuer Public Keys

This section describes the public interfaces that an issuer is required to support to provide public keys to be used by Private State Token protocols.

An issuer needs to maintain a set of keys and implement the Issue and Redeem cryptographic functions to sign and validate tokens. Issuers are required to serve a key commitment endpoint. Key commitments are collections of cryptographic keys and associated metadata necessary for executing the issuance and redemption operations. Issuers make these available through secure HTTP [RFC8446] endpoints. User agents should fetch the key commitments periodically. A key commitment is a map representing a collection of cryptographic keys and associated metadata necessary for executing the issuance and redemption operations.

Requests to key commitment endpoints should result in a JSON response [RFC8259] of the following format with a media type of "application/pst-issuer-directory":

{
  <cryptographic protocol_version>: {
    "protocol_version": <cryptographic protocol version>,
    "id": <key commitment identifier>
    "batchsize": <batch size>,
    "keys": {
      <keyID>: { "Y": <base64-encoded public key>,
                 "expiry": <key expiration date>},
      <keyID>: { "Y": <base64-encoded public key>,
                 "expiry": <key expiration date>}, ...
    }
  },
  ...
}

All field names and their values are strings. When new key commitments are fetched for an issuer, previous commitments are discarded.

3.1. Issuer Key Fetching/Registration

To maintain the privacy of this API and avoid user-specific keys, issuers should present the same keys to all clients that issue and redeem tokens against them.

To ensure this property, it is recommended that user agents fetch the key commitments in an user-agnostic manner, through some sort of proxied mechanism or centralized mechanism for fetching the keys and distributing them to individual clients.

If using a centralized mechanism for fetching keys, user agents should have a registration process to allow for issuers to register to have their key commitments fetched and sent to clients at a regular client. The requirements and mechanisms for registering are implementation-defined.

When using a registration process, it is recommended that user agents apply an expiration date to registration requests, to allow for the removal of deprecated or no longer active issuers.

4. VOPRF Methods

This document encodes protocol messages in the TLS presentation language from section 3 of [RFC8446].

To serialize protocol messages and deserialize protocol messages, protocol messages are encoded and interpreted as described in section 3 of [RFC8446].

For Private State Tokens, the VOPRF protocol is initialized using P-384 for the curve (G in the functions described below) and nonce_size is defined as 64.

Note: The use of X9.62 uncompressed points for the inputs/outputs in the current version of PST is a historical divergence from the existing [VOPRF] specification. The selection of nonce_size is a historical divergence from the current draft of [BatchedTokens].

When the server is performing an Issuance, the server performs the BlindEvaluateBatch function of the [BatchedTokens] protocol, where the input IssueRequest and output IssueResponse are serialized as:

// Scalars are elliptic curve scalars of length Ns (determined by the curve, 48 for P-384).
// ECPoints are elliptic curve points encoded using the X9.62 Uncompressed point representation (determined by the curve, 97 for P-384).

struct {
  uint16 count;
  ECPoint nonces[count]; // Corresponding to blindedElements
} IssueRequest;

struct {
  ECPoint evaluated; // Corresponding to evaluatedElements
} SignedNonce;

struct {
  Scalar c;
  Scalar s;
} DLEQProof;

struct {
  uint16 issued;
  uint32 key_id;
  SignedNonce signed[issued];
  opaque proof<1..2^16-1>; // Bytestring containing a serialized DLEQProof struct.
} IssueResponse;

When the server is performing a Redemption, the server performs PSTEvaluate on the Token, the input RedeemRequest and output RedeemResponse are serialized as:

struct {
  uint32 key_id;
  opaque nonce[nonce_size];
  ECPoint W;
} Token;

struct {
  opaque token<1..2^16-1>; // Bytestring containing a serialized Token struct.
  opaque client_data<1..2^16-1>;
} RedeemRequest;

struct {
  opaque rr<1..2^16-1>;
} RedeemResponse;

Private State Tokens defines PSTFinalize which is a variation of the FinalizeBatch function of the [BatchedTokens] protocol as follows:

def PSTFinalize(input, blinds, evaluatedElements,
               blindedElements, pkS, proof):
  if VerifyProof(G.Generator(), pkS, blindedElements,
                 evaluatedElements, proof) == false:
    raise VerifyError

  // PST: Use batched construction.
  unblindedElements = []
  for index in range(evaluatedElements.length):
    N = G.ScalarInverse(blinds[index]) * evaluatedElements[index]
    unblindedElements.append(G.SerializeElement(N))

  // PST: Return unblindedElements rather than hash output.
  return unblindedElements

Private State Tokens defines PSTEvaluate which is a variation of the Evaluate function of the [VOPRF] protocol as follows:

// skS is determined by the server by using key_id to lookup the corresponding private key.

def PSTEvaluate(skS, nonce, W, client_data):
  inputElement = G.HashToGroup(nonce)
  if inputElement == G.Identity():
    raise InvalidInputError
  evaluatedElement = skS * inputElement
  issuedElement = G.SerializeElement(evaluatedElement)

  // PST: Checks issuedElement rather than hash output.
  if issuedElement != W:
    raise InvalidInputError

  // PST: The server may use client_data and other information to construct a redemptionRecord to return to the client.
  return redemptionRecord

5. Algorithms

A user agent has issuerAssociations, which is a map where the keys are origin topLevel, and the values are a list of origins.

To determine whether associating an issuer would exceed the top-level limit given an origin issuer and an origin topLevel, run the following steps:

  1. If issuerAssociations[topLevel] does not exist, return false.

  2. If issuerAssociations[topLevel] contains issuer, return false.

  3. If the issuerAssociations[topLevel] size is less than 2, return false.

  4. Return true.

To associate the issuer issuer (an origin) with the origin topLevel, run the following steps:

  1. If issuerAssociations[topLevel] does not exist, set issuerAssociations[topLevel] to an empty list.

  2. Append issuer to issuerAssociations[topLevel].

To determine whether an origin issuer is associated with a given origin topLevel, run the following steps:

  1. If issuerAssociations[topLevel] does not exist, return false.

  2. If issuerAssociations[topLevel] contains issuer, return true.

  3. Return false.

A user agent has redemptionTimes, a map where the keys are a tuple (issuer, topLevel), and the values are a tuple (lastRedemption, penultimateRedemption).

To record redemption timestamp given an origin issuer and an origin topLevel, run the following steps:

  1. Let currentTime be the current date and time.

  2. Let previousRedemption be the earliest representable date and time.

  3. If redemptionTimes[(issuer,topLevel)] exists, let previousRedemption be the lastRedemption field of the tuple redemptionTimes[(issuer,topLevel)].

  4. set redemptionTimes[(issuer,topLevel)] to the tuple (currentTime, previousRedemption).

To look up penultimate redemption given an origin issuer and an origin topLevel, run the following steps:

  1. Let penultimateRedemption be the earliest representable date and time.

  2. If redemptionTimes[(issuer,topLevel)] exists, let penultimateRedemption be the penultimateRedemption field of the tuple redemptionTimes[(issuer,topLevel)].

  3. Return penultimateRedemption.

A user agent has redemptionRecords, a map where the keys are a tuple (issuer, topLevel), and the values are a tuple (record, expiration, signingKeys).

To record a redemption record given an origin issuer, an origin topLevel, a byte sequence header, and a duration lifetime, run the following steps:

  1. If lifetime is zero, return.

  2. Let currentTime be the current date and time in milliseconds since 01 January, 1970 UTC.

  3. Let expiration be the sum of currentTime and lifetime.

  4. Let signingKeys be the result of looking up of the latest keys for issuer.

  5. Set redemptionRecords[(issuer, topLevel))] to the tuple (header, expiration, signingKeys).

To retrieve a redemption record given an origin issuer and an origin topLevel, run the following steps:

  1. Let currentTime be the current date and time in milliseconds since 01 January, 1970 UTC.

  2. If redemptionRecords[(issuer,topLevel)] does not exist, return null.

  3. Let (record, expiration, signingKeys) be redemptionRecords[(issuer,topLevel)].

  4. If expiration is less than currentTime, return null.

  5. Let currentSigningKeys be the result of looking up of the latest keys for issuer.

  6. If currentSigningKeys does not equal signingKeys, return null.

  7. Return record.

A user agent has pstKeyCommitments, a map where the keys are an origin, and the values are key commitments.

Note: It is recommended that each user agent fetches the key commitments from issuers at a regular cadence and through trusted infrastructure, and then sends the concatenated map of issuers and key commitments to the client to ensure consistency between the keys different user agent instances use.

To look up the key commitments for a given origin issuer, run the following steps:

  1. If pstKeyCommitments[issuer] does not exist, return null.

  2. Let issuerKeys be pstKeyCommitments[issuer].

  3. For each cryptoProtocolVersion that the user agent supports for this API in an implementation-defined order, run the following steps:

    1. Return issuerKeys[cryptoProtocolVersion], if it exists.

  4. Return null.

Note: cryptoProtocolVersion is a string identifier representing different cryptographic versions of tokens that can be used with this API. User agents should only select keys for versions they support, ordered by which versions they prefer based on performance and any user defined preferences.

To look up the latest keys for a given origin issuer, run the following steps:

  1. Let commitment be the result of looking up the key commitments for issuer.

  2. If commitment is null, return null.

  3. Let chosenKey be null.

  4. Let currentTime be the current date and time.

  5. For each key of commitment["keys"], run the following steps:

    1. If key["expiry"] is less than currentTime, continue.

    2. If chosenKey is null, set chosenKey to key.

    3. If key["expiry"] is less than chosenKey["expiry"], set chosenKey to key.

  6. Return chosenKey.

A user agent has a tokenStore, a map where the keys are origins and the values are lists of storedTokens. A storedToken is a tuple of (byte sequence, byte sequence).

To insert a token for an origin issuer, byte sequence token, and byte sequence signingKey, run the following steps:

  1. Create a new tuple storedToken consisting of (token, signingKey).

  2. If tokenStore[issuer] does not exist, set tokenStore[issuer] to an empty list.

  3. Append storedToken to tokenStore[issuer].

To retrieve a token for an origin issuer, run the following steps:

  1. If tokenStore[issuer] does not exist, return null.

  2. If the size of tokenStore[issuer] is zero, return null.

  3. Remove a random element of tokenStore[issuer] and return the removed element.

To discard tokens from an origin issuer and byte sequence signingKey, run the following steps:

  1. If tokenStore[issuer] does not exist, return.

  2. Remove all elements of tokenStore[issuer] whose second element is not equal to signingKey.

To get the number of tokens from an origin issuer, run the following steps:

  1. If tokenStore[issuer] does not exist, return 0.

  2. Return the size of tokenStore[issuer].

To get the max batch size for an origin issuer, run the following steps:

  1. Let issuerKey be the result of running look up the key commitments on issuer.

  2. If issuerKey is null, return 0.

  3. Return issuerKey["batchsize"].

To generate masked tokens given key commitments issuerKeys and number numTokens, run the following steps. They return a tuple (byte sequence, byte sequence).

  1. Let issueRequest be an empty IssueRequest.

  2. Set issueRequest["count"] to numTokens.

  3. Let blinds be an empty byte sequence.

  4. Repeat the following steps, numTokens times:

    1. Let input be a random byte sequence.

    2. Let (blind, blindedElement) (tuple (byte sequence, byte sequence)) be the result of running the Blind function of the [VOPRF] protocol on input, where blindedElement is encoded as a X9.62 uncompressed point.

    3. Append blind to blinds.

    4. Append blindedElement to issueRequest["nonces"].

  5. Let issueHeader be the result of serializing protocol message issueRequest.

  6. Return tuple (issueHeader, blinds).

To unmask tokens given key commitments issuerKeys, byte string blinds, and a byte sequence response, run the following steps. They return a list of byte sequence.

  1. Let input be an empty byte sequence.

  2. Let evaluatedElements be an empty list.

  3. Let blindedElements be an empty list.

  4. Let issueResponse be the result of deserializing protocol message response as an IssueResponse.

  5. For each nonce of issueResponse["signed"]:

    1. Append nonce["blinded"] to blindedElements.

    2. Append nonce["evaluated"] to evaluatedElements.

  6. Let pkS be issuerKeys["keys"][issueResponse["key_id"]]["Y"].

  7. Let proof be issueResponse["proof"].

  8. Let blindsList be an empty list.

  9. While the length of blinds is greater than 0:

    1. Let blind be the first N elements of blinds and blinds be the remainder, where N is the length of a X9.62 uncompressed point.

    2. Append blind to blindsList.

  10. Let result (list of byte strings) be the result of running PSTFinalize on input, blindsList, evaluatedElements, blindedElements, pkS, and proof.

  11. Return result.

To set private token properties for request from private token, given a PrivateToken privateToken and a request request, run the following steps:

  1. Set request’s private token operation to privateToken["operation"].

  2. If privateToken["operation"] is "token-request":

    1. If Should request be allowed to use feature? on "private-state-token-issuance" and request returns false, then throw a "NotAllowedError" DOMException.

    2. Abort the remaining steps.

  3. Assert: privateToken["operation"] is "token-redemption" or "send-redemption-record".

  4. If Should request be allowed to use feature? on "private-state-token-redemption" and request returns false, then throw a "NotAllowedError" DOMException.

  5. If privateToken["operation"] is "token-redemption":

    1. Set request’s private token refresh policy to privateToken["refreshPolicy"].

    2. Abort the remaining steps.

  6. If privateToken["issuers"] does not exist, then throw TypeError.

  7. If privateToken["issuers"] is empty, then throw TypeError.

  8. For each issuer of privateToken["issuers"]:

    1. Let issuerURL be the the result of running the URL parser on issuer.

    2. If issuerURL is failure, then throw TypeError.

    3. If issuerURL’s scheme is not an HTTP(S) scheme, then throw TypeError.

    4. Let issuerOrigin be issuerURL’s origin.

    5. If issuerOrigin is not a potentially trustworthy origin, then throw TypeError.

    6. Append issuerURL to request’s private token issuers.

6. Integration with Fetch

6.1. Definitions

The RefreshPolicy is attached to a redemption request, determining whether or not the redemption should result in a previously returned, unexpired redemption record or a new one.

enum RefreshPolicy { "none", "refresh" };

The TokenVersion is currently set to 1, as this is the only version that the specification supports at this time.

enum TokenVersion { "1" };

The OperationType refers to which operation the user agent is attempting to complete.

enum OperationType { "token-request", "send-redemption-record", "token-redemption" };

The PrivateToken contains the information required to make a fetch request.

dictionary PrivateToken {
  required TokenVersion version;
  required OperationType operation;
  RefreshPolicy refreshPolicy = "none";
  sequence<USVString> issuers;
};

This specification adds a new property to the RequestInit dictionary:

partial dictionary RequestInit {
  PrivateToken privateToken;
};

6.2. Modifications to request

A request has an associated private token refresh policy, which is of type RefreshPolicy with default value of "none".

A request has an associated private token operation, which is of type OperationType.

A request has an associated private token issuers, which is a list of strings.

Note: Private token refresh policy is ignored unless private token operation is "token-redemption". Private token issuers is ignored unless private token operation is "send-redemption-record". Private token issuers must be specified and non-empty when private token operation is "send-redemption-record".

This specification defines two new policy-controlled features. Exactly one of these policy features applies for a given Private State Token operation.

The policy-controlled feature identified by "private-state-token-issuance" applies for the "token-request" operation. The default allowlist for this feature is ["self"].

The policy-controlled feature identified by "private-state-token-redemption" applies for the "send-redemption-record" and "token-redemption" operations. The default allowlist for this feature is ["self"].

A request has an associated pstPretokens, which is null or a byte sequence.

Add the following steps to the new Request (input, init) constructor, before step 28 ("Set this's request to request"):

Given a RequestInit init and a Request request run the following steps:

  1. If init["privateToken"] exists:

    1. Let privateToken be init["privateToken"].

    2. Run set private token properties for request from private token on privateToken and request.

6.3. Modifications to http-network-or-cache fetch

This specification adds the following steps to the http-network-or-cache fetch algorithm, before modifying the header list:

  1. If request’s private token operation is null, abort remaining steps.

  2. If request’s private token operation is "token-request":

    1. Append private state token issue request headers on httpRequest.

    2. Abort the remaining steps.

  3. If request’s private token operation is "token-redemption":

    1. Append private state token redemption request headers on httpRequest.

    2. Abort the remaining steps.

  4. Assert: request’s private token operation is "send-redemption-record".

  5. Append private state token redemption record headers on httpRequest.

6.4. Modifications to HTTP fetch steps

The specification adds the following steps to the HTTP fetch algorithm, before checking the redirect status (i.e. "7. If actualResponse’s status is a redirect status, ..."):

  1. Let issue response result be the result of handling an issue response, given request request and response actualResponse as input.

  2. If issue response result is a network error, return issue response result.

  3. Let redeem response result be the result of handling a redeem response, given request request and response actualResponse as input.

  4. If redeem response result is a network error, return issue response result.

7. Integration with iframe

7.1. privateToken content attribute for HTMLIframeElement

The iframe element contains a privateToken content attribute. The IDL attribute privateToken reflects the privateToken content attribute.

partial interface HTMLIFrameElement {
  [SecureContext] attribute DOMString privateToken;
};

The following step is added to the create navigation params by fetching, before step "25. Return a new navigation params, with ...":

  1. If navigable’s container is an iframe element, and if it has a privateToken content attribute, then run set private token properties for request from private token on navigable’s privateToken and request.

8. Integration with XMLHttpRequest

8.1. Attach PrivateToken

An XMLHttpRequest has an associated private state token, a PrivateToken object that specifies the OperationType to execute against the request.

partial interface XMLHttpRequest {
  undefined setPrivateToken(PrivateToken privateToken);
};

The setPrivateToken(PrivateToken privateToken) method steps are:

  1. If this's state is not "opened", then throw an "InvalidStateError" DOMException.

  2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
  3. Set this's private state token to privateToken.

8.2. send() monkeypatch

Modify send(body) as follows:

After the step:

Let req be a new request, initialized as follows...

Add the step:

  1. Run set private token properties for request from private token with this’s private state token and req.

9. Issuing Protocol

This section explains the issuing protocol. It has two sections that explains the issuing protocol steps for user agents and issuers.

9.1. Creating An Issue Request

An issue request is created and fetched as demonstrated in the following snippet.
let issueRequest = new Request("https://example.issuer:1234/issuer_path", {
  privateToken: {
    version: 1,
    operation: "token-request",
  }
});
fetch(issueRequest);

To append private state token issue request headers given a request request, run the following steps:

  1. If request’s client is not a secure context, return.

  2. Let issuer be request’s URL's origin.

  3. Let topLevel be request’s client's top-level origin.

  4. If associating issuer with topLevel would exceed the top level’s number-of-issuers limit, return.

  5. Associate the issuer issuer with topLevel.

  6. If the number of tokens for issuer is at least 500, return.

  7. Let issuerKeys be the result of looking up the key commitments for issuer.

  8. If issuerKeys is null, return.

  9. Let signingKey be the result of looking up of the latest keys for issuer.

  10. Run discard tokens with issuer and signingKey.

  11. Let numTokens be issuer’s max batch size or an implementation-defined limit on the number of tokens (which is recommended to be 100), whichever is smaller.

  12. Let (issueHeader, pretokens) be the result of generating masked tokens with issuerKeys and numTokens.

  13. Set request’s cache mode to "no-store".

  14. Set request’s pstPretokens to pretokens.

  15. Let base64EncodedTokens be the base64-encoded [RFC4648] version of issueHeader.

  16. Let cryptoProtocolVersion be the version of the cryptographic protocol used.

  17. Set a structured field value given (Sec-Private-State-Token, base64EncodedTokens) in request’s header list.

  18. Set a structured field value given (Sec-Private-State-Token-Crypto-Version, cryptoProtocolVersion) in request’s header list.

Private State Token HTTP request headers created for a typical fetch is as follows.
Sec-Private-State-Token: <masked tokens encoded as base64 string>
Sec-Private-State-Token-Crypto-Version: <cryptographic protocol version, VOPRF>

9.2. Issuer Signing Tokens

This section explains the signing of tokens that happens in the issuer servers. VOPRF can only encode one of six values by the selection of which key to use.

Using its private keys, issuer signs the masked tokens obtained in the Sec-Private-State-Token request header value with a value dependent on other information passed as part of the issuance request. Issuer uses the cryptographic protocol specified in the request Sec-Private-State-Token-Crypto-Version header. Issuer returns the signed tokens in the Sec-Private-State-Token response header value encoded as a base64 [RFC4648] byte string.

The following snippet displays a typical response demonstrating the Private State Token header.
Sec-Private-State-Token: <token encoded as base64 string>

9.3. Handling Issue Responses

To handle an issue response, given request request and response response, run the following steps:

  1. If request’s header list does not contain Sec-Private-State-Token, return null.

  2. If response’s header list does not contain Sec-Private-State-Token, return a network error.

  3. Let header be the result of getting Sec-Private-State-Token from response’s header list.

  4. If header is empty, return.

  5. Delete Sec-Private-State-Token from response’s header list.

  6. Let issuer be request’s URL's origin.

  7. Let issuerKeys be the result of looking up the key commitments for issuer.

  8. If issuerKeys is null, return.

  9. Let pretokens be request’s pstPretokens.

  10. If pretokens is null, return.

  11. Let rawResponse be the base64-decoded [RFC4648] version of header.

  12. Let unmasked tokens be the result of unmasking response tokens given issuerKeys, pretokens, and rawResponse.

  13. If unmasked tokens is null, return a network error.

  14. Let signingKey be the result looking up the latest keys for issuer.

  15. For each token in unmasked tokens, run the following steps:

    1. Insert a token for issuer, token, and signingKey.

  16. Return.

10. Redeeming Tokens

When the user agent navigates to a top-level origin, this top-level origin or a third party site embedded on the top level origin may redeem tokens stored in the user agent from a specific issuer to learn the data encoded in the tokens.

Redemption is carried through fetch as demonstrated in the following snippet. The default value for refreshPolicy is 'none'.
let redemptionRequest = new Request('https://example.issuer:1234/redemption_path', {
  privateToken: {
    version: 1,
    operation: 'token-redemption',
    refreshPolicy: {'none', 'refresh'}
  }
});

To set redemption headers with request request and a RedeemRequest record:

  1. Let redemptionRequest be the result of serializing protocol message record.

  2. Let cryptoProtocolVersion be the version of the cryptographic protocol used.

  3. Let token-lifetime be the expiration time of the redemption record in seconds.

  4. Set a structured field value given (Sec-Private-State-Token, redemptionRequest) in request’s header list.

  5. Set a structured field value given (Sec-Private-State-Token-Crypto-Version, cryptoProtocolVersion) in request’s header list.

  6. Optionally, set a structured field value given (Sec-Private-State-Token-Lifetime, token-lifetime) in request’s header list.

  7. Set request’s cache mode to "no-store".

To append private state token redemption request headers given a request request, run the following steps:

  1. Let issuer be request’s URL's origin.

  2. Let topLevel be request’s client's top-level origin.

  3. If request’s client is not a secure context, return.

  4. If associating issuer with topLevel would exceed the top level’s number-of-issuers limit, return.

  5. Associate the issuer issuer with topLevel.

  6. If request’s private token refresh policy is "none":

    1. Let record be the result of performing retrieve a redemption record with issuer and topLevel.

    2. If record is not null, set redemption headers with request and record and return.

  7. Let penultimateRedemption be the result of look up penultimate redemption with issuer and topLevel

  8. If penultimateRedemption is less than an implementation-defined time period (which is recommended to be 48 hours), return error.

  9. Let commitments be the result of looking up the key commitments for issuer.

  10. If commitments is null, return.

  11. Discard tokens from issuer that are signed with keys other than those from the issuer’s most recent commitments.

  12. Let token be the result of retrieving a token for issuer.

  13. If token is null, return.

  14. Let redeemRequest be an empty RedeemRequest.

  15. Set redeemRequest["token"] to token.

  16. Set redemption headers with request and record.

10.1. Handling Redeem Responses

To handle a redeem response, given request request and response response, run the following steps:

  1. If request’s header list does not contain Sec-Private-State-Token, return null.

  2. If response’s header list does not contain Sec-Private-State-Token, return a network error.

  3. Let rawHeader be the result of getting Sec-Private-State-Token from response’s header list.

  4. If rawHeader is empty, return null.

  5. Let rawResponse be the base64-decoded [RFC4648] version of rawHeader.

  6. Let header be the result of deserializing protocol message rawHeader as a RedeemResponse.

  7. Delete Sec-Private-State-Token from response’s header list.

  8. Set lifetime to be the largest representable duration.

  9. If response’s header list contains Sec-Private-State-Token-Lifetime response header, set lifetime to that value.

  10. Delete Sec-Private-State-Token-Lifetime from response’s header list.

  11. Let issuer be request’s URL's origin.

  12. Let topLevel be request’s client's top-level origin.

  13. Perform record redemption timestamp with issuer and topLevel.

  14. Perform record a redemption record with issuer, topLevel, header, and lifetime.

Note: The redemption record is HTTP-only and JavaScript is only able to access/send the redemption record via Private State Token Fetch APIs. The redemption record is treated as an arbitrary blob of bytes from the issuer, that may have semantic meaning to downstream consumers.

10.2. Redemption Records

To reduce communication overhead, the user agent might cache blobs returned in Sec-Private-State-Token header value in redemption responses. These blobs are referred as Redemption Records. User agents might choose to store these records to include them in subsequent requests to the origins that can verify its validity. An issuer might choose to include an optional Sec-Private-State-Token-Lifetime header in the redemption response. The value of this header indicates the expiration time for the redemption record provided. This expiration is specified as number of seconds in the Sec-Private-State-Token-Lifetime HTTP response header value.

A Redemption Record is a byte sequence.

The Private State Tokens API provides a 'send-redemption-record' operation to append private state token redemption record headers. This operation attaches a previously recorded redemption record from handle a redeem response.

To append private state token redemption record headers given a request request, run the following steps:

  1. If request’s client is not a secure context, then abort these steps.

  2. Let topLevel be request’s client's top-level origin.

  3. For each issuer of private token issuers:

    1. Let issuerURL be the the result of running the URL parser on issuer.

    2. If issuerURL is failure, then abort these steps.

    3. If issuerURL’s scheme is not an HTTP(S) scheme, then abort these steps.

    4. Let issuerOrigin be issuerURL’s origin.

    5. If issuerOrigin is not a potentially trustworthy origin, then abort these steps.

  4. Let records_per_issuer be a map where keys are USVString and values are redemption records.

  5. For each issuer of private token issuers:

    1. Let record be the result of performing retrieve a redemption record with issuer and topLevel.

    2. If record is null, then continue.

    3. Set records_per_issuer[issuer] to record.

  6. If records_per_issuer is empty, then abort these steps.

  7. Let headerItems be a structured headers list [RFC8941].

  8. For each issuer -> record of records_per_issuer:

    1. Let serializedIssuer be result of serializing issuer.

    2. Let serializedRecord be result of serializing record.

    3. Append serializedIssuer and serializedRecord pairs to headerItems.

  9. Let serializedHeaderItems be result of serializing headerItems.

  10. If serializedHeaderItems is null, then abort these steps.

  11. Set Sec-Redemption-Record to serializedHeaderItems.

10.3. Changes to Document

partial interface Document {
  Promise<boolean> hasPrivateToken(USVString issuer);
  Promise<boolean> hasRedemptionRecord(USVString issuer);
};

11. Query APIs

11.1. Token Query

When invoked on Document doc with USVString issuer, the hasPrivateToken(issuer) method must run these steps:

  1. Let p be a new promise.

  2. If doc is not fully active, then reject p with an "InvalidStateError" DOMException and return p.

  3. Let global be doc’s relevant global object.

  4. If global is not a secure context, then reject p with a "NotAllowedError" DOMException and return p.

  5. Let parsedURL be the the result of running the URL parser on issuer.

  6. If parsedURL is failure, reject p with a "TypeError" DOMException and return p.

  7. Let origin be parsedURL’s origin.

  8. Let topLevel be the top-level origin of doc’s relevant settings object.

  9. Run the following steps in parallel:

    1. If associating issuer with topLevel would exceed the top level’s number-of-issuers limit, queue a global task on the networking task source given global to reject p with a "NotAllowedError" DOMException and return.

    2. Associate the issuer origin with topLevel.

    3. Look up the key commitments for origin. If there are key commitments, discard tokens from origin that are signed with keys other than those from the issuer’s most recent commitments.

    4. Queue a global task on the networking task source given global to resolve p with true if there are tokens stored for the given issuer, with false otherwise.

  10. Return p.

Note: This query modifies the user agent state. It associates the issuer argument with the current origin. The specification allows at most 2 issuers associated with an origin. This is to prevent leaking information through the issuers a user has tokens from. Note that querying tokens triggers removal of stale tokens.

11.2. Redemption Record Query

When invoked on Document doc with USVString issuer, the hasRedemptionRecord(issuer) method must run these steps:

  1. Let p be a new promise.

  2. If doc is not fully active, then reject p with an "InvalidStateError" DOMException and return p.

  3. Let global be doc’s relevant global object.

  4. If global is not a secure context, then reject p with a "NotAllowedError" DOMException and return p.

  5. Let parsedURL be the the result of running the URL parser on issuer.

  6. If parsedURL is failure, reject p with a "TypeError" DOMException and return p.

  7. Let origin be parsedURL’s origin.

  8. Let topLevel be the top-level origin of doc’s relevant settings object.

  9. Run the following steps in parallel:

    1. If origin is not associated with topLevel, queue a global task on the networking task source given global to resolve p with false and return.

    2. Look up the key commitments for origin. If there are key commitments, discard tokens from origin that are signed with keys other than those from the issuer’s most recent commitments.

    3. Queue a global task on the networking task source given global to resolve p with true if there is a redemption record for the issuer and top level pair, with false otherwise.

  10. Return p.

Note: Similar to token query, redemption query might modify the user agent state. Unlike token query, redemption query does not associate issuer with the top level origin. There is no need to associate the issuer queried with the top level origin, because the answer to the redemption query does not leak information about the issuers of the currently stored tokens. Similar to token query, redemption query clears stale tokens.

11.3. Clearing PST Data

User interface guidance from storage standard should be followed. User agents should provide interfaces to clear PST data from storage.

12. Private State Token HTTP Header Fields

12.1. The 'Sec-Private-State-Token' Header Field

The Sec-Private-State-Token request header field sends a collection of unsigned, masked tokens during issuance. During redemption, it sends a singled signed, unmasked token along with associated redemption metadata.

The Sec-Private-State-Token response header field sends a collection of signed, masked tokens. During redemption it sends the just-created signed redemption record.

It is a Structured Header whose value MUST be an string [RFC8941].

The header’s ABNF is:

Sec-Private-State-Token = sf-string

12.2. The 'Sec-Private-State-Token-Lifetime' Header Field

The Sec-Private-State-Token-Lifetime response header field gives the expiration for the redemption record given in the associated Sec-Private-State-Token response header. The expiration is given in seconds.

It is a Structured Header whose value MUST be an integer [RFC8941].

The header’s ABNF is:

Sec-Private-State-Token-Lifetime = sf-integer

12.3. The 'Sec-Private-State-Token-Crypto-Version' Header Field

The Sec-Private-State-Token-Crypto-Version header field gives the cryptographic protocol version of the Private State Token.

It is a Structured Header whose value MUST be an string [RFC8941].

The header’s ABNF is:

Sec-Private-State-Token-Crypto-Version = sf-string

12.4. The 'Sec-Redemption-Record' Header Field

The Sec-Redemption-Record request header field sends a cached redemption record of a previous redemption operation.

It is a Structured Header whose value MUST be an string [RFC8941].

The header’s ABNF is:

Sec-Redemption-Record = sf-string

13. Privacy Considerations

13.1. Unlinkability

Cryptographic protocols [VOPRF] provide masked signatures. At redemption time, issuers can recognize their signature on the provided token, however they can not determine at what time or in which context they signed the token. This prevents issuers from correlating their issuances on an origin with redemptions on another origin. Issuers learn only the aggregate information about the origins users visit.

13.2. Limiting Encoded Information

User agents should enforce limits on the number of unique keys an issuer can have at any point in time, to preserve client privacy. Without limits, an issuer could de-anonymize clients by simply using a unique key for each client. For [VOPRF], the number of keys is limited to six.

Issuers can utilize different keys to represent different "labels", which correspond to an arbritrary client state, such as client trust level or some other useful anti-fraud signal. Issuers are responsible for understanding this designation and sharing the key "labels" with token redeemers so they know how to interpret the significance of each token. This helps reduce reverse engineering from malicious actors and preserves client privacy over human-readable labels. When using [VOPRF], the issuer’s 6 keys can effectively represent six labels.

13.2.1. Potential Attack: Side Channel Fingerprinting

Unlinkability is lost if the issuer is able to use network-level fingerprinting or any other side-channel and can associate the user agent at redemption time with the user agent at token issuance time, even though the Private State Token API itself has only stored and revealed limited amount of information about the user agent.

13.3. Cross-site Information Transfer

Private State Tokens transfer limited information between first-party contexts. Underlying cryptographic protocols guarantee that each token only contains a small amount of information. Still, if we allow many token redemptions on a single page, the first-party cookie for user U on domain A can be encoded in the Private State Token information channel and decoded on domain B, allowing domain B to learn the user’s domain A cookie until either 1p cookie is cleared. Separate from the concern of channels allowing arbitrary communication between domains, some identification attacks---for instance, a malicious redeemer attempting to learn the exact set of issuers that have granted tokens to a particular user, which could be identifying---have similar mitigations.

13.3.1. Mitigation: Dynamic Issuance/Redemption Limits

To mitigate this attack, the specification places limits on both issuance and redemption. User activation with the issuing site is required in the issuing operation. The specification does not allow a third redemption in an implementation-defined time window, usually 48 hours.

13.3.2. Mitigation: Per-Site Issuer Limits

The rate of identity leakage from one origin to another increases with the number of issuers allowed in an origin. To avoid abuse, the user agent allows association of at most two issuers per top level origin. Issuers are associated with top level origins for token query API as well, see § 11.1 Token Query.

14. Security Considerations

14.1. Preventing Token Exhaustion

Malicious origins might attempt to exhaust all tokens stored in the user agent by redeeming them all. To prevent this, the specification limits the number of redemption operations. In the context of a given origin, two redemptions are allowed initially. However, the third redemption is only allowed once more than an implementation-defined amount of time, usually 48 hours, have elapsed since the first redemption.

14.2. Preventing Issuer Exhaustion

Competing scripts might race to call hasPrivateToken(issuer) to ensure their issuer enters the issuerAssociations map before the issuer of others given a limit of two per top-level origin. To control this process, the top-level origin could call hasPrivateToken(issuer) up to twice before any other JavaScript is included to ensure their preferred issuers are available.

14.3. Preventing Double Spending

Issuers can verify that each token is seen only once, because every redemption is sent to the same token issuer. This means that even if a malicious piece of malware exfiltrates all of a user’s tokens, the tokens will run out over time. Issuers can sign fewer tokens at a time to mitigate the risk.

15. IANA Considerations

This document intends to define the Sec-Private-State-Token, Sec-Private-State-Token-Lifetime, Sec-Private-State-Token-Crypto-Version, HTTP request header fields, and register them in the permanent message header field registry ([RFC9110]).

15.1. 'Sec-Private-State-Token' Header Field

Header field name: Sec-Private-State-Token

Applicable protocol: http

Status: standard

Author/Change controller: IETF

Specification document: this specification (§ 12.1 The 'Sec-Private-State-Token' Header Field)

15.2. 'Sec-Private-State-Token-Lifetime' Header Field

Header field name: Sec-Private-State-Token-Lifetime

Applicable protocol: http

Status: standard

Author/Change controller: IETF

Specification document: this specification (§ 12.2 The 'Sec-Private-State-Token-Lifetime' Header Field)

15.3. 'Sec-Private-State-Token-Crypto-Version' Header Field

Header field name: Sec-Private-State-Token-Crypto-Version

Applicable protocol: http

Status: standard

Author/Change controller: IETF

Specification document: this specification (§ 12.3 The 'Sec-Private-State-Token-Crypto-Version' Header Field)

15.4. 'Sec-Redemption-Record' Header Field

Header field name: Sec-Redemption-Record

Applicable protocol: http

Status: standard

Author/Change controller: IETF

Specification document: this specification (§ 12.4 The 'Sec-Redemption-Record' Header Field)

Acknowledgments

Thanks to Alex Kallam, Charlie Harrison, Chris Fredrickson, David Van Cleve, Dylan Cutler, Eric Trouton, Johann Hofmann, Kaustubha Govind, Mike Taylor, Ryan Kalla, and Sam Schlesinger for their contributions. Thanks to Chris Wilson for reviewing and mentoring this spec.

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

[BatchedTokens]
R. Robert; C. A. Wood. Batched Token Issuance Protocol. URL: https://www.ietf.org/archive/id/draft-robert-privacypass-batched-tokens-01.html
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.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/
[PRIVACY-PASS-ARCHITECTURE]
A. Davidson; J. Iyengar; C. A. Wood. Privacy Pass Architectural Framework. URL: https://www.ietf.org/archive/id/draft-ietf-privacypass-architecture-10.html
[PRIVACY-PASS-AUTH-SCHEME]
T. Pauly; S. Valdez; C. A. Wood. The Privacy Pass HTTP Authentication Scheme. URL: https://www.ietf.org/archive/id/draft-ietf-privacypass-auth-scheme-10.html
[PRIVACY-PASS-ISSUANCE-PROTOCOL]
S. Celi; et al. Privacy Pass Issuance Protocol. URL: https://www.ietf.org/archive/id/draft-ietf-privacypass-protocol-10.html
[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
[RFC4648]
. URL: https://www.rfc-editor.org/rfc/rfc4648
[RFC8259]
T. Bray, Ed.. The JavaScript Object Notation (JSON) Data Interchange Format. December 2017. Internet Standard. URL: https://www.rfc-editor.org/rfc/rfc8259
[RFC8446]
E. Rescorla. The Transport Layer Security (TLS) Protocol Version 1.3. August 2018. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8446
[RFC8536]
A. Olson; P. Eggert; K. Murchison. The Time Zone Information Format (TZif). February 2019. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8536
[RFC8941]
M. Nottingham; P-H. Kamp. Structured Field Values for HTTP. February 2021. Proposed Standard. URL: https://httpwg.org/specs/rfc8941.html
[RFC9110]
R. Fielding, Ed.; M. Nottingham, Ed.; J. Reschke, Ed.. HTTP Semantics. June 2022. Internet Standard. URL: https://httpwg.org/specs/rfc9110.html
[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/
[VOPRF]
A. Davidson; et al. Oblivious Pseudorandom Functions (OPRFs) using Prime-Order Groups. URL: https://www.ietf.org/archive/id/draft-irtf-cfrg-voprf-21.html
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard. Living Standard. URL: https://xhr.spec.whatwg.org/

Informative References

[PRIVACY-PASS-WG]
. URL: https://datatracker.ietf.org/wg/privacypass/about/
[RFC6265]
A. Barth. HTTP State Management Mechanism. April 2011. Proposed Standard. URL: https://httpwg.org/specs/rfc6265.html

IDL Index

enum RefreshPolicy { "none", "refresh" };

enum TokenVersion { "1" };

enum OperationType { "token-request", "send-redemption-record", "token-redemption" };

dictionary PrivateToken {
  required TokenVersion version;
  required OperationType operation;
  RefreshPolicy refreshPolicy = "none";
  sequence<USVString> issuers;
};

partial dictionary RequestInit {
  PrivateToken privateToken;
};

partial interface HTMLIFrameElement {
  [SecureContext] attribute DOMString privateToken;
};

partial interface XMLHttpRequest {
  undefined setPrivateToken(PrivateToken privateToken);
};

partial interface Document {
  Promise<boolean> hasPrivateToken(USVString issuer);
  Promise<boolean> hasRedemptionRecord(USVString issuer);
};