Version 3 is a working version and is subject to change.
This document documents the cryptographic protocol for the "PrivateStateTokenV3PMB" and "PrivateStateTokenV3VOPRF" experimental version of Private State Token. An issuer needs to support maintaining a set of keys and a key commitment endpoint, as well as implementing the Issue and Redeem cryptographic functions to sign and validate Private State Tokens. Experimental versions of Private State Token are not intended to be backwards-compatible with each other and will undergo rapid design/implementation changes during the experiment timeframe. Note that there is a distinct different between the issuer protocol version and the cryptographic protocol version, with the latter describing the underlying cryptographic primitives used in the issuer protocol.
This document uses TLS presentation language (https://tools.ietf.org/html/rfc8446#section-3) for structures and serialization.
This section describes the public issuer interfaces that an issuer will need to support.
A Private State Token issuer should have an endpoint at a publicly accessible secure URL (HTTPS) that serves the current key commitments used in the Private State Token protocol. Requests to this endpoint should result in a JSON response of the following format:
Key commitment result
{
<protocol_version>: {
"protocol_version": <protocol version as a string, "PrivateStateTokenV3PMB" or "PrivateStateTokenV3VOPRF" for this>,
"id": <key commitment identifier, as a monotonically increasing integer>
"batchsize": <batch size>,
"keys": {
<keyID>: { "Y": <base64-encoded PrivateStateTokenPublicKey>,
"expiry": <key expiry, encoded as a string representation of
an integer timestamp in microseconds since the Unix
epoch> },
<keyID>: { "Y": <base64-encoded public key>,
"expiry": <key expiry, encoded as a string representation of
an integer timestamp in microseconds since the Unix
epoch> }, ...
}
},
...
}
The commitment is a dictionary keyed by protocol version where each value is a self-contained key commitment for that version (for
current protocol versions, the protocol_version field is also included as a field of the individual commitment itself). To support compatibility with prior
key commitment formats, unknown fields in the top level dictionary should be ignored. The id
field is used as a unique identifier to identify each key commitment
and to allow comparing the freshness of key commitments (larger values indicate a newer key commitment).
To support the issuance of tokens made via Private State Token calls by the client, server paths that support token issuance should parse the Sec-Private-State-Token
header as a base64 binary blob. This decoded binary blob should be interpreted as a Private State Token Issuance Request and passed into the Issue crypto protocol along with the Issuance metadata based on the rest of the request. The result of Issue (a Private State Token Issuance Response) should be base64 encoded and returned via the Sec-Private-State-Token
header in the HTTP response.
As part of an issuance, two forms of metadata can be embedded into the token. Public metadata is embedded via the choice of key which is used as part of the issuance process and is passed into the Issue method as the keypair selection. Private metadata is embedded via a cryptographically hidden bit in the signed token itself and is passed into the Issue method as the private metadata boolean. PrivateStateTokenV3VOPRF
supports up to 6 buckets (keypairs) of public metadata and no private metadata, while PrivateStateTokenV3PMB
supports up to 3 buckets (keypairs) of public metadata and one bit of private metadata.
To support the redemption of tokens by the client, server paths that support token redemption should parse the Sec-Private-State-Token
header as a base64 binary blob. This decoded binary blob should be interpreted as a Private State Token Redemption Request and passed into the Redeem crypto protocol. The result of Redeem (a Private State Token Redemption Response) should be base64 encoded and returned via the Sec-Private-State-Token
header in the HTTP response. Additionally, the issuer should keep track of redeemed tokens to prevent token reuse from malicious clients.
At redemption time, the token is decoded and provided via the redemption API. The issuer can then choose to encode this in the Redemption Record in whatever way it prefers.
In Version 3, the algorithm used for request signing is ecdsa_secp256r1_sha256
.
This Private State Token crypto protocol is based on the PMBTokens design in https://eprint.iacr.org/2020/072 (appendix H) using P-384. This crypto protocol is used in both the V2 and V3 Private State Token protocol versions. The necessary keys and function mappings are described below.
The Private State Token protocol primarily requires keypairs consisting of each public metadata bucket:
- PrivateStateTokenSecretKey/PrivateStateTokenPublicKey - A Trust Token keypair used to sign and verify Private State Tokens.
These are keys used in Private State Token consisting of elliptic curve scalars and points. All scalars and points are sized based on the curve choice (P-384). Up to 3 keys may be configured in parallel in the key commitment.
Each keypair consist of the following:
opaque ECPoint<1..2^16-1>; // X9.62 Uncompressed point.
opaque Scalar<Ns>; // big-endian bytestring
struct {
// Corresponding to the FALSE private metadata bit (x0, y0).
Scalar x0;
Scalar y0;
// Corresponding to the TRUE private metadata bit (x1, y1).
Scalar x1;
Scalar y1;
// Corresponding to the token validity (x˜, y˜).
Scalar xs;
Scalar ys;
} TrustTokenSecretKey;
struct {
ECPoint pub0; // Corresponding to the FALSE private metadata bit.
ECPoint pub1; // Corresponding to the TRUE private metadata bit.
ECPoint pubs; // Corresponding to the token validity check.
} PrivateStateTokenPublicKey;
For the PMBTokens functions, the following serialization schemes and hashes are used internally using draft 07 of the hash-to-curve specification (https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-07):
Ht(t)
is defined to be P384_XMD:SHA-512_SSWU_RO_ with a big-endian bytestring input of t
and a dst of "PMBTokens Experiment V2 HashT".
Hs(T, s)
is defined to be P384_XMD:SHA-512_SSWU_RO_ with a bytestring input of T
with the following content and a dst of "PMBTokens Experiment V2 HashS".
struct {
ECPoint t;
opaque s[Nn];
} T;
The hash-to-curve document does not define hash to scalars, so Hc(x)
is defined to be the output of the hash_to_field function with the following parameters:
DST
is "PMBTokens Experiment V2 HashC"F
,p
, andm
are defined according to the finite fieldGF(r)
, wherer
is the order of P-384. Note this is a different modulus fromhash_to_field
as used in P384_XMD:SHA-512_SSWU_RO_.L
is 72, derived based on P384_XMD:SHA-512_SSWU_RO_'s security parameterk
(192), andp
defined above.expand_message
uses the corresponding function from P384_XMD:SHA-512_SSWU_RO_.
When used in the DLEQ proof, Hc
takes the following input.
struct {
uint8 label[6] = "DLEQ2\0";
ECPoint X;
ECPoint T;
ECPoint S;
ECPoint W;
ECPoint K0;
ECPoint K1;
} DLEQInput; // Used for DLEQ proof.
When used in the DLEQOR proof, Hc
takes the following input.
struct {
uint8 label[8] = "DLEQOR2\0";
ECPoint X0;
ECPoint X1;
ECPoint T;
ECPoint S;
ECPoint W;
ECPoint K00;
ECPoint K01;
ECPoint K10;
ECPoint K11;
} DLEQORInput; // Used for DLEQOR proof.
When used in the DLEQ batching step, Hc
is called once for each e_i
output, with the following input. The index
field contains i
.
struct {
uint8 label[11] = "DLEQ Batch\0";
ECPoint pubs;
ECPoint pub0;
ECPoint pub1;
ECPoint T'0;
ECPoint S'0;
ECPoint Wp0;
ECPoint Wsp0;
...
ECPoint T'n;
ECPoint S'n;
ECPoint Wpn;
ECPoint Wspn;
uint16 index;
} DLEQBatchInput; // Used for Batch proof.
The Issue function corresponds to the AT.Sig stage of the PMBTokens protocol.
Inputs:
- count (the number of tokens being requested, from the IssueRequest)
- nonces (the list of nonces to sign)
- secretKey (the secret key that should be used to sign this request, determined by the public metadata)
- publicKey (the public key corresponding to the secretKey)
- keyID (the ID corresponding to this key)
- privateMetadata (the boolean value corresponding to the private metadata value)
- toIssue (number of tokens to issue)
Outputs:
- issued (the number of tokens being issued)
- keyID (the key used to sign these tokens)
- signed (the list of signed nonces)
- cs, us, vs, c0, u0, v0, c1, u1, v1 (the list of scalars corresponding to the zero-knowledge proof)
Issue:
issued = min(count, toIssue)
xb,yb = secretKey.x1, secretKey.y1
if privateMetadata == 0:
xb,yb = secretKey.x0, secretKey.y0
signed = []
T,S,W,Ws = [], [], [], []
for i in 0..issued:
T' = nonces[i]
s ←$ {0, 1}λ
S' = Hs(T', s)
Wp = xb*T' + yb*S'
Wsp = secretKey.xs*T' + secretKey.ys*S'
T,S,W,Ws += T', S', Wp, Wsp
signed += SignedNonce{s, Wp, Wsp}
proof = DLEQbatched.P((publicKey,T,S,W,Ws),(secretKey.xs, secretKey.ys, xb, yb))
return (issued, keyID, signed, proof)
DLEQbatched.P((X,T,S,W,Ws),(xs, ys, xb, yb)):
e0,...,em = Hc(X,T,S,W,Ws)
Tt = Sum(ej*Tj) // over j from 0 to m
St = Sum(ej*Sj) // over j from 0 to m
Wt = Sum(ej*Wj) // over j from 0 to m
Wst = Sum(ej*Wsj) // over j from 0 to m
dleqProof = DLEQ2.P((X,T,S,Ws), (xs,ys)) // cs,us,vs
dleqorProof = DLEQOR2.P((X,T,S,W), (xb,yb)) // c0,c1,u0,u1,v0,v1
return dleqProof + dleqorProof
DLEQOR2.P
is specified by replacing n = 2
in the above and DLEQ2.P
is specified by replacing n = 1
in the below procedure.
DLEQOR.P((X, T, S, W), (xb, yb)):
k0, k1 sampled uniformly from Z^2_p
Kb = k0 * (G; T) + k1 * (H; S)
for i in [n], i != b:
ci, ui, vi sampled uniformly from Z_p
K = ui * (G; T) + vi * (H; S) - ci * (Xi; W)
c = Hc((X, T, S, W), K0, ..., Nn)
cb = c - Sum(ci) // over i in [n] not equal to b
ub = k0 + cb * xb
vb = k1 + cb * yb
return (c, u, v)
Input Serialization:
The Private State Token Issuance Request contains an IssueRequest
structure defined below.
struct {
uint16 count;
ECPoint nonces[count];
} IssueRequest;
Output Serialization:
The Private State Token Issuance Response contains an IssueResponse
structure defined below.
struct {
opaque s<Nn>; // big-endian bytestring
ECPoint Wp;
ECPoint Wsp;
} SignedNonce;
struct {
Scalar cs;
Scalar us;
Scalar vs;
Scalar c0;
Scalar c1;
Scalar u0;
Scalar u1;
Scalar v0;
Scalar v1;
} DLEQProof;
struct {
uint16 issued;
uint32 key_id = keyID;
SignedNonce signed[issued];
opaque proof<1..2^16-1>; // Length-prefixed form of DLEQProof.
} IssueResponse;
The Redeem function corresponds to the AT.VerValid and AT.ReadBit stage of the PMBTokens protocol.
Inputs:
- token (The private state token to redeem)
- client_data (the client data sent as part of the redemption request to include in the SRR)
- secretKey (the secret key that should be used to sign this request, determined by the public metadata)
- keys (dictionary from known key IDs to secret/public keys)
Outputs:
- public_metadata (the ID of the keypair used to generate this token)
- private_metadata (the bit value of the private metadata, if aplicable)
Redeem:
secretKey, publicKey = keys[token.key_id]
T = Ht(token.nonce)
if token.Ws != secretKey.xs*T+secretKey.ys*token.S:
return 0 // Invalid validity verification.
W0 = secretKey.x0*T+secretKey.y0*token.S
W1 = secretKey.x1*T+secretKey.y1*token.S
// These equalities are timing-sensitive as they could be a timing side-channel.
isW0 = constantTimeEquality(tokenW, W0)
isW1 = constantTimeEquality(tokenW, W1)
if !(isW0 ^ isW1):
return 0 // Internal Error
privateMetadata = isW1
return (token.key_id, privateMetadata)
Input Serialization:
The Private State Token redemption request contains a RedemptionRequest
structure as defined below.
struct {
uint32 key_id;
opaque nonce<nonce_size>;
ECPoint S;
ECPoint W;
ECPoint Ws;
} Token;
struct {
opaque token<1..2^16-1>; // Bytestring containing a serialized Token struct.
opaque client_data<1..2^16-1>;
uint64 redemption_time;
} RedeemRequest;
Output Serialization:
The Private State Token redemption response contains a RedemptionResponse
structure as defined below.
struct {
opaque rr<1..2^16-1>;
} RedeemResponse;
This Private State Token crypto protocol is based on the VOPRF design in https://datatracker.ietf.org/doc/draft-irtf-cfrg-voprf/. This crypto protocol is used in both the V2 and V3 Private State Token protocol versions.
The Private State Token protocol primarily requires keypairs consisting of each public metadata bucket:
- PrivateStateTokenSecretKey/PrivateStateTokenPublicKey - A Private State Token keypair used to sign and verify Private State Tokens.
These are keys used in Private State Token consisting of elliptic curve scalars and points. All scalars and points are sized based on the curve choice (P-384). Up to 6 keys may be configured in parallel in the key commitment.
Each keypair consist of the following:
opaque ECPoint<1..2^16-1>; // X9.62 Uncompressed point.
opaque Scalar<Ns>; // big-endian bytestring
struct {
Scalar x;
} PrivateStateTokenSecretKey;
struct {
ECPoint pub;
} PrivateStateTokenPublicKey;
For the VOPRF functions, the following serialization schemes and hashes are used internally using draft 07 of the hash-to-curve specification (https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-07):
H2C(t)
is defined to be P384_XMD:SHA-512_SSWU_RO_ with a big-endian bytestring input of t
and a dst of "PrivateStateToken VOPRF Experiment V2 HashToGroup".
The hash-to-curve document does not define hash to scalars, so H2S(x)
is defined to be the output of the hash_to_field function with the following parameters:
DST
is "PrivateStateToken VOPRF Experiment V3 HashToScalar"F
,p
, andm
are defined according to the finite fieldGF(r)
, wherer
is the order of P-384. Note this is a different modulus fromhash_to_field
as used in P384_XMD:SHA-512_SSWU_RO_.L
is 72, derived based on P384_XMD:SHA-512_SSWU_RO_'s security parameterk
(192), andp
defined above.expand_message
uses the corresponding function from P384_XMD:SHA-512_SSWU_RO_.
When used in the DLEQ proof, H2S
takes the following input.
struct {
uint8 label[6] = "DLEQ2\0";
ECPoint X;
ECPoint T;
ECPoint W;
ECPoint K0;
ECPoint K1;
} DLEQInput; // Used for DLEQ proof.
When used in the DLEQ batching step, H2C
is called once for each e_i
output, with the following input. The index
field contains i
.
struct {
uint8 label[11] = "DLEQ BATCH\0";
ECPoint pub;
ECPoint BT0;
ECPoint Z0;
...
ECPoint Tn;
ECPoint Zn;
uint16 index;
} DLEQBatchInput; // Used for Batch proof.
The Issue Blind/Evaluate/Unblind stages of the VOPRF protocol.
Input Serialization:
The Private State Token Issuance Request contains an IssueRequest
structure defined below.
struct {
uint16 count;
ECPoint nonces[count];
} IssueRequest;
Output Serialization:
The Private State Token Issuance Response contains an IssueResponse
structure defined below.
struct {
opaque s<Nn>; // big-endian bytestring
ECPoint W;
} SignedNonce;
struct {
Scalar c;
Scalar u;
Scalar v;
} DLEQProof;
struct {
uint16 issued;
uint32 key_id = keyID;
SignedNonce signed[issued];
opaque proof<1..2^16-1>; // Length-prefixed form of DLEQProof.
} IssueResponse;
The Redeem function corresponds to the VerifyFinalize stage of the VOPRF protocol.
Input Serialization:
The Private State Token redemption request contains a RedemptionRequest
structure as defined below.
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>;
uint64 redemption_time;
} RedeemRequest;
Output Serialization:
The Private State Token redemption response contains a RedemptionResponse
structure as defined below.
struct {
opaque rr<1..2^16-1>;
} RedeemResponse;
V3 uses ecdsa_secp256r1_sha256
as the signing algorithm for request signing and updates the key commitment format to nest it in a version-keyed dictionary (and to move the set of keys to their own dictionary within the commitment). The send-redemption-record
API is also updated to support sending multiple redemption records from different issuers with differing signing algorithms for future compatibility.
V2 introduces two protocol versions, each supporting a different arrangement of public and private metadata. It also enables the issuer to structure the Redemption Record as they choose, and removes the signing requirement.