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
Request a challenge for the claim type you want to verify by calling the challenge endpoint. This call is unauthenticated and is not tied to a specific relay — it only needs a claim-type in the request body.
Issue against the same claim type as your relay
The token-key you receive is determined by the claim-type in your request, so the resulting Privacy Pass is bound to that claim type. A pass can only redeem a relay created with the same claim type — if they don’t match, the pass won’t be able to redeem the claim. Always request the challenge with the same claim type you used to create the relay.
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) - 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.
Use the Implementation
Verification page to test
your buildTokenInput(), blind(), and finalize() implementations against
fixed test vectors before going live.

