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).
What’s inside the challenge
Decode challenge from base64url to get the following JSON:
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:
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:
- Blind the token input using Persona’s public key (
token-key), producingblindedMsgand blinding inverseinv - Submit the blinded token to
POST /api/v1/privacy-passesfor signing (see API Usage) - Unblind the returned
blind-sigusinginvto produce the final RSA-PSS signature
Step 4 — Construct the Privacy Pass token
Assemble the final token payload:
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:
- Challenge MAC — recomputes the HMAC over the challenge fields and compares against
challenge.mac - Expiry — checks that
challenge.expires_athasn’t passed (challenges are valid for 1–2 hours) - Origin info — verifies
challenge.origin_infomatches/api/privacy/v1/relays - RSA-PSS signature — reconstructs
token_inputand verifies the unblinded signature against Persona’s public key - Double-spend — checks
token_nonceagainst 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.

