Challenge Mechanism

Issuing a Privacy Pass requires implementing a challenge-response protocol based on RFC 9578 and RFC 9577.

If you don’t want to implement this yourself, use the Server SDK or Gateway Service — they handle the cryptography for you.

Step 1 — Get the challenge

Call generate-claim without a PrivateToken authorization header. Persona responds with 401 Unauthorized. This is not an error — it is the first step of the protocol, called a challenge-response flow (RFC 9110 §11.3).

$POST /api/privacy/v1/relays/:relayToken/generate-claim
$Persona-Relay-Secret: <relay-secret>
$# Response: 401 Unauthorized
$WWW-Authenticate: PrivateToken challenge="<base64url>", token-key="<base64url>", token-key-id="<base64url>"
FieldDescription
challengeA base64url-encoded JSON object containing metadata about the token being requested
token-keyPersona’s RSA public key, base64url-encoded DER. Used to blind your token input
token-key-idSHA-256 of the public key, included as a convenience so you don’t have to compute it

What’s inside the challenge

Decode challenge from base64url to get the following JSON:

1{
2 "token_type": 2,
3 "issuer_name": "withpersona.com/api/v1/privacy-passes",
4 "origin_info": "/api/privacy/v1/relays",
5 "expires_at": "2026-05-18T18:00:00Z",
6 "mac": "<hmac>"
7}
FieldDescription
token_type2 = Blind RSA. Always 2 for Relay
issuer_nameThe issuance endpoint where you POST your blinded token
origin_infoThe redemption endpoint this token is scoped to. Tokens cannot be used at other endpoints
expires_atToken expiry, rounded up to the nearest hour. Gives a 1–2 hour issuance window
macHMAC of the above fields. Persona uses this to verify the challenge wasn’t tampered with during issuance

The mac binds the expiry and endpoint scope into the signed token — if a client tries to forge a challenge with a longer expiry, the MAC fails and the token is rejected at issuance.

Step 2 — Construct the token input

Construct a 98-byte token input. Each component is SHA-256 hashed before concatenation:

token_input = 0x0002 || SHA256(nonce) || SHA256(challengeMac) || SHA256(keyId)
ComponentDescription
0x00022-byte token type (Blind RSA)
SHA256(nonce)SHA-256 of a random 32-byte hex nonce you generate
SHA256(challengeMac)SHA-256 of the mac field from the decoded challenge
SHA256(keyId)SHA-256 of the token-key-id from the WWW-Authenticate header

Spec deviation: RFC 9578 §6.1 puts raw bytes directly into token_input. We hash each component to SHA-256 first. Your implementation must match this exactly — using the raw values will produce an invalid token.

Step 3 — Blind, sign, and unblind

See Blind RSA for a full explanation of the protocol. In summary:

  1. Blind the token input using Persona’s public key (token-key), producing blindedMsg and blinding inverse inv
  2. Submit the blinded token to POST /api/v1/privacy-passes for signing (see API Usage)
  3. Unblind the returned blind-sig using inv to produce the final RSA-PSS signature

Step 4 — Construct the Privacy Pass token

Assemble the final token payload:

1{
2 "token_nonce": "<your_nonce>",
3 "signature": "<base64url_unblinded_signature>",
4 "key_id": "<token-key-id>",
5 "challenge": { "<parsed_challenge_object>" }
6}

Base64url-encode this JSON object. The result is your privacy-pass-token, ready to be used in the redemption call.

Use the same nonce string here that you used in token_input. The verifier recomputes SHA256(nonce) during redemption — using a different value will fail signature verification.

What Persona verifies at redemption

When you submit the PrivateToken, Persona performs these checks in order:

  1. Challenge MAC — recomputes the HMAC over the challenge fields and compares against challenge.mac
  2. Expiry — checks that challenge.expires_at hasn’t passed (challenges are valid for 1–2 hours)
  3. Origin info — verifies challenge.origin_info matches /api/privacy/v1/relays
  4. RSA-PSS signature — reconstructs token_input and verifies the unblinded signature against Persona’s public key
  5. Double-spend — checks token_nonce against a one-time-use tracker. The same token cannot be redeemed twice

If all checks pass, the claim is returned. Persona never learns which customer made the request — only that a valid token was presented.