> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.withpersona.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.withpersona.com/_mcp/server.

# Challenge Mechanism

> How Persona's Privacy Pass challenge-response flow works.

Issuing a Privacy Pass requires implementing a challenge-response protocol based on [RFC 9578](https://www.rfc-editor.org/rfc/rfc9578) and [RFC 9577](https://www.rfc-editor.org/rfc/rfc9577).

> **Note**
>
> If you don't want to implement this yourself, use the [Server SDK](/relay-sdk-quickstart) or [Gateway Service](/relay-gateway-service-usage)
> — 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.

```bash
POST https://api.withpersona.com/api/privacy/v1/relays/challenge
Content-Type: application/json
```

```json
// Request
{
  "claim-type": "live_human_presence"
}
```

```json
// Response: 200 OK
{
  "challenge": "<base64url>",
  "token-key": "<base64url>",
  "token-key-id": "<base64url>"
}
```

| Field          | Description                                                                          |
| -------------- | ------------------------------------------------------------------------------------ |
| `challenge`    | A base64url-encoded JSON object containing metadata about the token being requested  |
| `token-key`    | Persona's RSA public key, base64url-encoded DER. Used to blind your token input      |
| `token-key-id` | SHA-256 of the public key, included as a convenience so you don't have to compute it |

> **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:

```json
{
  "token_type": 2,
  "issuer_name": "withpersona.com/api/v1/privacy-passes",
  "origin_info": "/api/privacy/v1/relays",
  "expires_at": "2026-05-18T18:00:00Z",
  "mac": "<hmac>"
}
```

| Field         | Description                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------------------- |
| `token_type`  | `2` = Blind RSA. Always `2` for Relay                                                                    |
| `issuer_name` | The issuance endpoint where you POST your blinded token                                                  |
| `origin_info` | The redemption endpoint this token is scoped to. Tokens cannot be used at other endpoints                |
| `expires_at`  | Token expiry, rounded up to the nearest hour. Gives a 1–2 hour issuance window                           |
| `mac`         | HMAC 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)
```

| Component              | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `0x0002`               | 2-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 challenge response |

> **Note**
>
> **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](/relay-privacy-pass-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 https://api.withpersona.com/api/v1/privacy-passes` for signing (see [API Usage](/relay-api-usage#issuing-a-privacy-pass))
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:

```json
{
  "token_nonce": "<your_nonce>",
  "signature": "<base64url_unblinded_signature>",
  "key_id": "<token-key-id>",
  "challenge": { "<parsed_challenge_object>" }
}
```

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

> **Note**
>
> 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. **RSA-PSS signature** — reconstructs `token_input` and verifies the unblinded signature against Persona's public key
4. **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.

> **Note**
>
> Use the [Implementation Verification](/relay-privacy-pass-implementation-verification) page to test
> your `buildTokenInput()`, `blind()`, and `finalize()` implementations against
> fixed test vectors before going live.