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- encodedpublic key> , "expiry" : < key expiration date> }, < keyID>: { "Y" : < base64- encodedpublic key> , "expiry" : < key expiration date> }, ... } }, ... }
-
<cryptographic protocol version>
is a string identifier for the Private State Token protocol version used. The same string is used as a value of the inner"protocol_version"
field. Protocol version string identifier is"PrivateStateTokenV1VOPRF"
.-
Protocol version
“PrivateStateTokenV1VOPRF”
implements [VOPRF] cryptographic protocol. Issuers can use up to six valid token signing keys.
-
-
"id"
field provides the identifier of the key commitment. It is a non-negative integer that is within the range of an unsigned 32 bit integer type. Values should be montonically increasing. -
"batchsize"
specifies the maximum number of masked tokens that the issuer supports for each token issuance operation. Its value is a positive integer. The user agent might send fewer tokens in a single operation, but will generally default to sendingbatchsize
many tokens per operation. -
"keys"
field is a dictionary of public keys listed by their identifiers.-
<keyID>
is a string representation of a non-negative integer that is within the range of an unsigned 32 bit integer type. -
Each key has a
"Y"
field which is a string representation of a big-endian base64 encoding [RFC4648] of the byte string of the key. -
"expiry"
field specifies how long the underlying key is valid. It is a string representation of a nonnegative integer that is within the range of an unsigned 64 bit integer type. Underlying key expires if this amount many or more microseconds are elapsed since the POSIX epoch [RFC8536].
-
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:
-
If issuerAssociations[topLevel] does not exist, return false.
-
If issuerAssociations[topLevel] contains issuer, return false.
-
If the issuerAssociations[topLevel] size is less than 2, return false.
-
Return true.
To associate the issuer issuer (an origin) with the origin topLevel, run the following steps:
-
If issuerAssociations[topLevel] does not exist, set issuerAssociations[topLevel] to an empty list.
-
Append issuer to issuerAssociations[topLevel].
To determine whether an origin issuer is associated with a given origin topLevel, run the following steps:
-
If issuerAssociations[topLevel] does not exist, return false.
-
If issuerAssociations[topLevel] contains issuer, return true.
-
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:
-
Let currentTime be the current date and time.
-
Let previousRedemption be the earliest representable date and time.
-
If redemptionTimes[(issuer,topLevel)] exists, let previousRedemption be the lastRedemption field of the tuple redemptionTimes[(issuer,topLevel)].
-
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:
-
Let penultimateRedemption be the earliest representable date and time.
-
If redemptionTimes[(issuer,topLevel)] exists, let penultimateRedemption be the penultimateRedemption field of the tuple redemptionTimes[(issuer,topLevel)].
-
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:
-
If lifetime is zero, return.
-
Let currentTime be the current date and time in milliseconds since 01 January, 1970 UTC.
-
Let expiration be the sum of currentTime and lifetime.
-
Let signingKeys be the result of looking up of the latest keys for issuer.
-
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:
-
Let currentTime be the current date and time in milliseconds since 01 January, 1970 UTC.
-
If redemptionRecords[(issuer,topLevel)] does not exist, return null.
-
Let (record, expiration, signingKeys) be redemptionRecords[(issuer,topLevel)].
-
If expiration is less than currentTime, return null.
-
Let currentSigningKeys be the result of looking up of the latest keys for issuer.
-
If currentSigningKeys does not equal signingKeys, return null.
-
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:
-
If pstKeyCommitments[issuer] does not exist, return null.
-
Let issuerKeys be pstKeyCommitments[issuer].
-
For each cryptoProtocolVersion that the user agent supports for this API in an implementation-defined order, run the following steps:
-
Return issuerKeys[cryptoProtocolVersion], if it exists.
-
-
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:
-
Let commitment be the result of looking up the key commitments for issuer.
-
If commitment is null, return null.
-
Let chosenKey be null.
-
Let currentTime be the current date and time.
-
For each key of commitment["keys"], run the following steps:
-
If key["expiry"] is less than currentTime, continue.
-
If chosenKey is null, set chosenKey to key.
-
If key["expiry"] is less than chosenKey["expiry"], set chosenKey to key.
-
-
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:
-
Create a new tuple storedToken consisting of (token, signingKey).
-
If tokenStore[issuer] does not exist, set tokenStore[issuer] to an empty list.
-
Append storedToken to tokenStore[issuer].
To retrieve a token for an origin issuer, run the following steps:
-
If tokenStore[issuer] does not exist, return null.
-
If the size of tokenStore[issuer] is zero, return null.
-
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:
-
If tokenStore[issuer] does not exist, return.
-
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:
-
If tokenStore[issuer] does not exist, return 0.
-
Return the size of tokenStore[issuer].
To get the max batch size for an origin issuer, run the following steps:
-
Let issuerKey be the result of running look up the key commitments on issuer.
-
If issuerKey is null, return 0.
-
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).
-
Let issueRequest be an empty IssueRequest.
-
Set issueRequest["count"] to numTokens.
-
Let blinds be an empty byte sequence.
-
Repeat the following steps, numTokens times:
-
Let input be a random byte sequence.
-
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.
-
Append blind to blinds.
-
Append blindedElement to issueRequest["nonces"].
-
-
Let issueHeader be the result of serializing protocol message issueRequest.
-
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.
-
Let input be an empty byte sequence.
-
Let evaluatedElements be an empty list.
-
Let blindedElements be an empty list.
-
Let issueResponse be the result of deserializing protocol message response as an IssueResponse.
-
For each nonce of issueResponse["signed"]:
-
Let pkS be issuerKeys["keys"][issueResponse["key_id"]]["Y"].
-
Let proof be issueResponse["proof"].
-
Let blindsList be an empty list.
-
While the length of blinds is greater than 0:
-
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.
-
Append blind to blindsList.
-
-
Let result (list of byte strings) be the result of running PSTFinalize on input, blindsList, evaluatedElements, blindedElements, pkS, and proof.
-
Return result.
To set private token properties for request from private token, given a PrivateToken
privateToken and a request request, run the following steps:
-
Set request’s private token operation to privateToken["
operation
"]. -
If privateToken["
operation
"] is"token-request"
:-
If Should request be allowed to use feature? on "
private-state-token-issuance
" and request returnsfalse
, then throw a "NotAllowedError
"DOMException
. -
Abort the remaining steps.
-
-
Assert: privateToken["
operation
"] is"token-redemption"
or"send-redemption-record"
. -
If Should request be allowed to use feature? on "
private-state-token-redemption
" and request returnsfalse
, then throw a "NotAllowedError
"DOMException
. -
If privateToken["
operation
"] is"token-redemption"
:-
Set request’s private token refresh policy to privateToken["
refreshPolicy
"]. -
Abort the remaining steps.
-
-
If privateToken["
issuers
"] does not exist, then throwTypeError
. -
For each issuer of privateToken["
issuers
"]:-
Let issuerURL be the the result of running the URL parser on issuer.
-
If issuerURL is failure, then throw
TypeError
. -
If issuerURL’s scheme is not an HTTP(S) scheme, then throw
TypeError
. -
Let issuerOrigin be issuerURL’s origin.
-
If issuerOrigin is not a potentially trustworthy origin, then throw
TypeError
. -
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 = "none";
refreshPolicy 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:
-
If init["
privateToken
"] exists:-
Let privateToken be init["
privateToken
"]. -
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:
-
If request’s private token operation is null, abort remaining steps.
-
If request’s private token operation is
"token-request"
:-
Append private state token issue request headers on httpRequest.
-
Abort the remaining steps.
-
-
If request’s private token operation is
"token-redemption"
:-
Append private state token redemption request headers on httpRequest.
-
Abort the remaining steps.
-
-
Assert: request’s private token operation is
"send-redemption-record"
. -
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, ..."):
-
Let issue response result be the result of handling an issue response, given request request and response actualResponse as input.
-
If issue response result is a network error, return issue response result.
-
Let redeem response result be the result of handling a redeem response, given request request and response actualResponse as input.
-
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
7.2. Modification to "create navigation params by fetching" steps
The following step is added to the create navigation params by fetching, before step "25. Return a new navigation params, with ...":
-
If navigable’s container is an
iframe
element, and if it has aprivateToken
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:
-
If this's state is not "opened", then throw an "
InvalidStateError
"DOMException
. - If this’s
send()
flag is set, then throw an "InvalidStateError
"DOMException
. - Set this's private state token to privateToken.
8.2. send() monkeypatch
Modifysend(body)
as follows:
After the step:
Let req be a new request, initialized as follows...
Add the step:
-
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
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:
-
If request’s client is not a secure context, return.
-
Let topLevel be request’s client's top-level origin.
-
If associating issuer with topLevel would exceed the top level’s number-of-issuers limit, return.
-
Associate the issuer issuer with topLevel.
-
If the number of tokens for issuer is at least 500, return.
-
Let issuerKeys be the result of looking up the key commitments for issuer.
-
If issuerKeys is null, return.
-
Let signingKey be the result of looking up of the latest keys for issuer.
-
Run discard tokens with issuer and signingKey.
-
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.
-
Let (issueHeader, pretokens) be the result of generating masked tokens with issuerKeys and numTokens.
-
Set request’s cache mode to
"no-store"
. -
Set request’s pstPretokens to pretokens.
-
Let base64EncodedTokens be the base64-encoded [RFC4648] version of issueHeader.
-
Let cryptoProtocolVersion be the version of the cryptographic protocol used.
-
Set a structured field value given (
Sec-Private-State-Token
, base64EncodedTokens) in request’s header list. -
Set a structured field value given (
Sec-Private-State-Token-Crypto-Version
, cryptoProtocolVersion) in request’s header list.
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.
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:
-
If request’s header list does not contain Sec-Private-State-Token, return null.
-
If response’s header list does not contain Sec-Private-State-Token, return a network error.
-
Let header be the result of getting Sec-Private-State-Token from response’s header list.
-
If header is empty, return.
-
Delete Sec-Private-State-Token from response’s header list.
-
Let issuerKeys be the result of looking up the key commitments for issuer.
-
If issuerKeys is null, return.
-
Let pretokens be request’s pstPretokens.
-
If pretokens is null, return.
-
Let rawResponse be the base64-decoded [RFC4648] version of header.
-
Let unmasked tokens be the result of unmasking response tokens given issuerKeys, pretokens, and rawResponse.
-
If unmasked tokens is null, return a network error.
-
Let signingKey be the result looking up the latest keys for issuer.
-
For each token in unmasked tokens, run the following steps:
-
Insert a token for issuer, token, and signingKey.
-
-
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.
'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:
-
Let redemptionRequest be the result of serializing protocol message record.
-
Let cryptoProtocolVersion be the version of the cryptographic protocol used.
-
Let token-lifetime be the expiration time of the redemption record in seconds.
-
Set a structured field value given (
Sec-Private-State-Token
, redemptionRequest) in request’s header list. -
Set a structured field value given (
Sec-Private-State-Token-Crypto-Version
, cryptoProtocolVersion) in request’s header list. -
Optionally, set a structured field value given (
Sec-Private-State-Token-Lifetime
, token-lifetime) in request’s header list. -
Set request’s cache mode to
"no-store"
.
To append private state token redemption request headers given a request request, run the following steps:
-
Let topLevel be request’s client's top-level origin.
-
If request’s client is not a secure context, return.
-
If associating issuer with topLevel would exceed the top level’s number-of-issuers limit, return.
-
Associate the issuer issuer with topLevel.
-
If request’s private token refresh policy is
"none"
:-
Let record be the result of performing retrieve a redemption record with issuer and topLevel.
-
If record is not null, set redemption headers with request and record and return.
-
-
Let penultimateRedemption be the result of look up penultimate redemption with issuer and topLevel
-
If penultimateRedemption is less than an implementation-defined time period (which is recommended to be 48 hours), return error.
-
Let commitments be the result of looking up the key commitments for issuer.
-
If commitments is null, return.
-
Discard tokens from issuer that are signed with keys other than those from the issuer’s most recent commitments.
-
Let token be the result of retrieving a token for issuer.
-
If token is null, return.
-
Let redeemRequest be an empty RedeemRequest.
-
Set redeemRequest["token"] to token.
-
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:
-
If request’s header list does not contain Sec-Private-State-Token, return null.
-
If response’s header list does not contain Sec-Private-State-Token, return a network error.
-
Let rawHeader be the result of getting Sec-Private-State-Token from response’s header list.
-
If rawHeader is empty, return null.
-
Let rawResponse be the base64-decoded [RFC4648] version of rawHeader.
-
Let header be the result of deserializing protocol message rawHeader as a RedeemResponse.
-
Delete Sec-Private-State-Token from response’s header list.
-
Set lifetime to be the largest representable duration.
-
If response’s header list contains Sec-Private-State-Token-Lifetime response header, set lifetime to that value.
-
Delete Sec-Private-State-Token-Lifetime from response’s header list.
-
Let topLevel be request’s client's top-level origin.
-
Perform record redemption timestamp with issuer and topLevel.
-
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:
-
If request’s client is not a secure context, then abort these steps.
-
Let topLevel be request’s client's top-level origin.
-
For each issuer of private token issuers:
-
Let issuerURL be the the result of running the URL parser on issuer.
-
If issuerURL is failure, then abort these steps.
-
If issuerURL’s scheme is not an HTTP(S) scheme, then abort these steps.
-
Let issuerOrigin be issuerURL’s origin.
-
If issuerOrigin is not a potentially trustworthy origin, then abort these steps.
-
-
Let records_per_issuer be a map where keys are USVString and values are redemption records.
-
For each issuer of private token issuers:
-
Let record be the result of performing retrieve a redemption record with issuer and topLevel.
-
If record is null, then continue.
-
Set records_per_issuer[issuer] to record.
-
-
If records_per_issuer is empty, then abort these steps.
-
Let headerItems be a structured headers list [RFC8941].
-
For each issuer -> record of records_per_issuer:
-
Let serializedIssuer be result of serializing issuer.
-
Let serializedRecord be result of serializing record.
-
Append serializedIssuer and serializedRecord pairs to headerItems.
-
-
Let serializedHeaderItems be result of serializing headerItems.
-
If serializedHeaderItems is null, then abort these steps.
-
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:
-
Let p be a new promise.
-
If doc is not fully active, then reject p with an "
InvalidStateError
"DOMException
and return p. -
Let global be doc’s relevant global object.
-
If global is not a secure context, then reject p with a "
NotAllowedError
"DOMException
and return p. -
Let parsedURL be the the result of running the URL parser on issuer.
-
If parsedURL is failure, reject p with a "
TypeError
"DOMException
and return p. -
Let origin be parsedURL’s origin.
-
Let topLevel be the top-level origin of doc’s relevant settings object.
-
Run the following steps in parallel:
-
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. -
Associate the issuer origin with topLevel.
-
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.
-
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.
-
-
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:
-
Let p be a new promise.
-
If doc is not fully active, then reject p with an "
InvalidStateError
"DOMException
and return p. -
Let global be doc’s relevant global object.
-
If global is not a secure context, then reject p with a "
NotAllowedError
"DOMException
and return p. -
Let parsedURL be the the result of running the URL parser on issuer.
-
If parsedURL is failure, reject p with a "
TypeError
"DOMException
and return p. -
Let origin be parsedURL’s origin.
-
Let topLevel be the top-level origin of doc’s relevant settings object.
-
Run the following steps in parallel:
-
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.
-
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.
-
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.
-
-
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 callhasPrivateToken(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.