> 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.

# Implementation Verification

> Test vectors to verify your blind RSA and tokenInput implementation.

Use these test vectors to verify that your implementation produces correct output at each step. All vectors are derived from [RFC 9474 Appendix A.1](https://www.rfc-editor.org/rfc/rfc9474#appendix-A.1) (RSABSSA-SHA384-PSS-Randomized).

> **Note**
>
> To reproduce `blindedMsg` exactly, your `blind()` implementation must accept
> an injectable `inv` parameter (the modular inverse of the blinding factor
> `r`). See [RFC 9474 §4.2](https://www.rfc-editor.org/rfc/rfc9474#section-4.2).
> In production, `inv` is always generated randomly — only fix it for test
> vector verification.

## Test key pair

Use this public key for all test vector verification. Do not use in production.

#### Public key

```
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArsTWmt3HC5kOpmpecGA7
b+4nqv69CPLZTL4SUMVW4EepKNY1w/Re6bZtG8YooDusm3w/QW/iDavqjz17S79/
ljvjNdIyjWfmwT7kqPlV4Foyg3INPh8TnDjkPgM4rQWKlJXFM3f8Nb5k0gj4m0qn
Ib9/fT/vg3viqA4Pit8LzR7sW7BARDorJ5L9ylIqdHKu108xoevh7rwfQIZgoFQ9
/iqFDxBqYX7GaFVzcC6qohpWQKXcr5t045f6OvGKLxt8A7qRpjNhWN5CDWMYjuFD
hm7kFXNdFVt8LYVNeVt7wjbP/XFULfNCNCIaBBPhQtjGE1XMRNRb2pQgSXRVesJw
TNi1k/A1pXJLGt9ELnjFQs1EFPzm8SmBgvttjlPO8a39LpDh5N7sUpmb3GwpFE6N
UqElIyyMbXXHBuo8wGhBx72jNWjGOmwDgX9yK1D8+JgjfXiKRACGnkTZCjAgkj3G
RjiKvMkUMVIV/NG64RscdR/VJEOqyPYBCH2NQnN8GKP6EezUEx7K4BeuChSs/E74
W4PBn+0zz9HNYp2ixMCeIis5jhjYIvd7s3jeo8s2C2BeWqWLIO3CnQAKZr0XfGgq
F+frEqY+98LkGD4NiY89a/VnuoroT4Tx0jv4uOJhw3KeL6bQe4MuB83dHRT1UyXG
+SQmeVcSGQLcGbOzKUi96tUCAwEAAQ==
-----END PUBLIC KEY-----
```

> **Note**
>
> The RFC 9474 test vectors reference raw key components (`n`, `e`, `modLen`)
> directly. Our docs use `publicKey` as the argument to `blind()` and
> `finalize()` — these components can be extracted from the public key PEM above
> using any standard RSA library.

These values are extracted from the test public key above and needed if your implementation works with raw key components rather than a PEM file.

```
n      = aec4d69addc70b990ea66a5e70603b6fee27aafebd08f2d94cbe1250c556e047
a928d635c3f45ee9b66d1bc628a03bac9b7c3f416fe20dabea8f3d7b4bbf7f963be3
35d2328d67e6c13ee4a8f955e05a3283720d3e1f139c38e43e0338ad058a9495c533
77fc35be64d208f89b4aa721bf7f7d3fef837be2a80e0f8adf0bcd1eec5bb040443a
2b2792fdca522a7472aed74f31a1ebe1eebc1f408660a0543dfe2a850f106a617ec6
685573702eaaa21a5640a5dcaf9b74e397fa3af18a2f1b7c03ba91a6336158de420d
63188ee143866ee415735d155b7c2d854d795b7bc236cffd71542df34234221a0413
e142d8c61355cc44d45bda94204974557ac2704cd8b593f035a5724b1adf442e78c5
42cd4414fce6f1298182fb6d8e53cef1adfd2e90e1e4deec52999bdc6c29144e8d52
a125232c8c6d75c706ea3cc06841c7bda33568c63a6c03817f722b50fcf898237d78
8a4400869e44d90a3020923dc646388abcc914315215fcd1bae11b1c751fd52443aa
c8f601087d8d42737c18a3fa11ecd4131ecae017ae0a14acfc4ef85b83c19fed33cf
d1cd629da2c4c09e222b398e18d822f77bb378dea3cb360b605e5aa58b20edc29d00
0a66bd177c682a17e7eb12a63ef7c2e4183e0d898f3d6bf567ba8ae84f84f1d23bf8
b8e261c3729e2fa6d07b832e07cddd1d14f55325c6f924267957121902dc19b3b329
48bdead5
e      = 010001
modLen = 256   // byte length of n — always n.length for a 2048-bit RSA key
```

---

## Test 1 — `blind()`

`RSABSSA-SHA384-PSS-Randomized` prepends a random 32-byte `msg_prefix` to the message before PSS encoding (`prepared_msg = msg_prefix || msg`).

> **Note**
>
> Persona's protocol does not use `msg_prefix` in production — the nonce in
> `tokenInput` already provides per-request randomness. The `msg_prefix` here is
> included solely to match the RFC 9474 Appendix A.1 test vectors exactly. It is
> straightforward to pass through to your `blind()` call for verification
> purposes.

**Inputs**

```
msg        = 8f3dc6fb8c4a02f4d6352edf0907822c1210a9b32f9bdda4c45a698c80023a
a6b59f8cfec5fdbb36331372ebefedae7d
msg_prefix = 8417e699b219d583fb6216ae0c53ca0e9723442d02f1d1a34295527
e7d929e8b
inv        = 80682c48982407b489d53d1261b19ec8627d02b8cda5336750b8cee332ae26
0de57b02d72609c1e0e9f28e2040fc65b6f02d56dbd6aa9af8fde656f70495dfb723
ba01173d4707a12fddac628ca29f3e32340bd8f7ddb557cf819f6b01e445ad96f874
ba235584ee71f6581f62d4f43bf03f910f6510deb85e8ef06c7f09d9794a008be7ff
2529f0ebb69decef646387dc767b74939265fec0223aa6d84d2a8a1cc912d5ca25b4
e144ab8f6ba054b54910176d5737a2cff011da431bd5f2a0d2d66b9e70b39f4b050e
45c0d9c16f02deda9ddf2d00f3e4b01037d7029cd49c2d46a8e1fc2c0c17520af1f4
b5e25ba396afc4cd60c494a4c426448b35b49635b337cfb08e7c22a39b256dd032c0
0adddafb51a627f99a0e1704170ac1f1912e49d9db10ec04c19c58f420212973e0cb
329524223a6aa56c7937c5dffdb5d966b6cd4cbc26f3201dd25c80960a1a111b3294
7bb78973d269fac7f5186530930ed19f68507540eed9e1bab8b00f00d8ca09b3f099
aae46180e04e3584bd7ca054df18a1504b89d1d1675d0966c4ae1407be325cdf623c
f13ff13e4a28b594d59e3eadbadf6136eee7a59d6a444c9eb4e2198e8a974f27a39e
b63af2c9af3870488b8adaad444674f512133ad80b9220e09158521614f1faadfe85
05ef57b7df6813048603f0dd04f4280177a11380fbfc861dbcbd7418d62155248dad
5fdec0991f
```

Construct `preparedMsg` by concatenating `msg_prefix || msg`, then call:

```
blind(publicKey, preparedMsg, inv)
```

**Expected output**

```
blindedMsg = aa3ee045138d874669685ffaef962c7694a9450aa9b4fd6465db9b
3b75a522bb921c4c0fdcdfae9667593255099cff51f5d3fd65e8ffb9d3b3036252a6
b51b6edfb3f40382b2bbf34c0055e4cbcc422850e586d84f190cd449af11dc65545f
5fe26fd89796eb87da4bda0c545f397cddfeeb56f06e28135ec74fd477949e7677f6
f36cfae8fd5c1c5898b03b9c244cf6d1a4fb7ad1cb43aff5e80cb462fac541e72f67
f0a50f1843d1759edfaae92d1a916d3f0efaf4d650db416c3bf8abdb5414a78cebc9
7de676723cb119e77aea489f2bbf530c440ebc5a75dccd3ebf5a412a5f346badd61b
ee588e5917bdcce9dc33c882e39826951b0b8276c6203971947072b726e935816056
ff5cb11a71ca2946478584126bb877acdf87255f26e6cca4e0878801307485d3b7bb
89b289551a8b65a7a6b93db010423d1406e149c87731910306e5e410b41d4da32346
24e74f92845183e323cf7eb244f212a695f8856c675fbc3a021ce649e22c6f0d053a
9d238841cf3afdc2739f99672a419ae13c17f1f8a3bc302ec2e7b98e8c353898b715
0ad8877ec841ea6e4b288064c254fefd0d049c3ad196bf7ffa535e74585d0120ce72
8036ed500942fbd5e6332c298f1ffebe9ff60c1e117b274cf0cb9d70c36ee4891528
996ec1ed0b178e9f3c0c0e6120885f39e8ccaadbb20f3196378c07b1ff22d10049d3
039a7a92fe7efdd95d
```

---

## Test 2 — `finalize()`

**Inputs**

```
blindSig = 3f4a79eacd4445fca628a310d41e12fcd813c4d43aa4ef2b81226953
248d6d00adfee6b79cb88bfa1f99270369fd063c023e5ed546719b0b2d143dd1bca4
6b0e0e615fe5c63d95c5a6b873b8b50bc52487354e69c3dfbf416e7aca18d5842c89
b676efdd38087008fa5a810161fcdec26f20ccf2f1e6ab0f9d2bb93e051cb9e86a9b
28c5bb62fd5f5391379f887c0f706a08bcc3b9e7506aaf02485d688198f5e22eefdf
837b2dd919320b17482c5cc54271b4ccb41d267629b3f844fd63750b01f5276c79e3
3718bb561a152acb2eb36d8be75bce05c9d1b94eb609106f38226fb2e0f5cd5c5c39
c59dda166862de498b8d92f6bcb41af433d65a2ac23da87f39764cb64e79e74a8f4c
e4dd567480d967cefac46b6e9c06434c3715635834357edd2ce6f105eea854ac126c
cfa3de2aac5607565a4e5efaac5eed491c335f6fc97e6eb7e9cea3e12de38dfb3152
20c0a3f84536abb2fdd722813e083feda010391ac3d8fd1cd9212b5d94e634e69ebc
c800c4d5c4c1091c64afc37acf563c7fc0a6e4c082bc55544f50a7971f3fb97d5853
d72c3af34ffd5ce123998be5360d1059820c66a81e1ee6d9c1803b5b62af6bc87752
6df255b6d1d835d8c840bebbcd6cc0ee910f17da37caf8488afbc08397a1941fcc79
e76a5888a95b3d5405e13f737bea5c78d716a48eb9dc0aec8de39c4b45c6914ad4a8
185969f70b1adf46
inv      = (same as Test 1)
```

Call `finalize` with the same `preparedMsg` from Test 1:

```
finalize(publicKey, preparedMsg, blindSig, inv)
```

**Expected output**

```
sig = 191e941c57510e22d29afad257de5ca436d2316221fe870c7cb75205a6c071
c2735aed0bc24c37f3d5bd960ab97a829a508f966bbaed7a82645e65eadaf24ab5e6
d9421392c5b15b7f9b640d34fec512846a3100b80f75ef51064602118c1a77d28d93
8f6efc22041d60159a518d3de7c4d840c9c68109672d743d299d8d2577ef60c19ab4
63c716b3fa75fa56f5735349d414a44df12bf0dd44aa3e10822a651ed4cb0eb6f47c
9bd0ef14a034a7ac2451e30434d513eb22e68b7587a8de9b4e63a059d05c8b22c7c5
1e2cfee2d8bef511412e93c859a13726d87c57d1bc4c2e68ab121562f839c3a3d233
e87ed63c69b7e57525367753fbebcc2a9805a2802659f5888b2c69115bf865559f10
d906c09d048a0d71bfee4b33857393ec2b69e451433496d02c9a7910abb954317720
bbde9e69108eafc3e90bad3d5ca4066d7b1e49013fa04e948104a1dd82b12509ecb1
46e948c54bd8bfb5e6d18127cd1f7a93c3cf9f2d869d5a78878c03fe808a0d799e91
0be6f26d18db61c485b303631d3568368fc41986d08a95ea6ac0592240c19d7b2241
6b9c82ae6241e211dd5610d0baaa9823158f9c32b66318f5529491b7eeadcaa71898
a63bac9d95f4aa548d5e97568d744fc429104e32edd9c87519892a198a30d333d427
739ffb9607b092e910ae37771abf2adb9f63bc058bf58062ad456cb934679795bbdf
cdfad5e0f2
```

---

## Test 3 — `buildTokenInput()`

Parse this JSON response body and combine with the fixed nonce to produce the expected `tokenInput`.

**Inputs**

The challenge response:

```json
{
  "challenge": "eyJ0b2tlbl90eXBlIjoyLCJpc3N1ZXJfbmFtZSI6IndpdGhwZXJzb25hLmNvbS9hcGkvdjEvcHJpdmFjeS1wYXNzZXMiLCJvcmlnaW5faW5mbyI6Ii9hcGkvcHJpdmFjeS92MS9yZWxheXMiLCJleHBpcmVzX2F0IjoiMjA5OS0wMS0wMVQwMDowMDowMFoiLCJtYWMiOiJiMWU5ZDRjN2EyZjVlOGQzYjZhMWYwZTlkOGM3YjZhNWY0ZTNkMmMxYjBhOWY4ZTdkNmM1YjRhM2YyZTFkMCJ9",
  "token-key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArsTWmt3HC5kOpmpecGA7b-4nqv69CPLZTL4SUMVW4EepKNY1w_Re6bZtG8YooDusm3w_QW_iDavqjz17S79_ljvjNdIyjWfmwT7kqPlV4Foyg3INPh8TnDjkPgM4rQWKlJXFM3f8Nb5k0gj4m0qnIb9_fT_vg3viqA4Pit8LzR7sW7BARDorJ5L9ylIqdHKu108xoevh7rwfQIZgoFQ9_iqFDxBqYX7GaFVzcC6qohpWQKXcr5t045f6OvGKLxt8A7qRpjNhWN5CDWMYjuFDhm7kFXNdFVt8LYVNeVt7wjbP_XFULfNCNCIaBBPhQtjGE1XMRNRb2pQgSXRVesJwTNi1k_A1pXJLGt9ELnjFQs1EFPzm8SmBgvttjlPO8a39LpDh5N7sUpmb3GwpFE6NUqElIyyMbXXHBuo8wGhBx72jNWjGOmwDgX9yK1D8-JgjfXiKRACGnkTZCjAgkj3GRjiKvMkUMVIV_NG64RscdR_VJEOqyPYBCH2NQnN8GKP6EezUEx7K4BeuChSs_E74W4PBn-0zz9HNYp2ixMCeIis5jhjYIvd7s3jeo8s2C2BeWqWLIO3CnQAKZr0XfGgqF-frEqY-98LkGD4NiY89a_VnuoroT4Tx0jv4uOJhw3KeL6bQe4MuB83dHRT1UyXG-SQmeVcSGQLcGbOzKUi96tUCAwEAAQ",
  "token-key-id": "c7a2f1e4d3b6a5f8e7d2c1b4a3f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9"
}
```

Plus the fixed nonce you generate:

```
nonce = a3f8c2d1e5b4a7f0c9d2e1b6a5f4c3d2e1b0a9f8c7d6e5f4a3b2c1d0e9f8a7b6
```

The decoded challenge JSON:

```json
{
  "token_type": 2,
  "issuer_name": "withpersona.com/api/v1/privacy-passes",
  "origin_info": "/api/privacy/v1/relays",
  "expires_at": "2099-01-01T00:00:00Z",
  "mac": "b1e9d4c7a2f5e8d3b6a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0"
}
```

Extract `mac` from the decoded challenge and `token-key-id` from the JSON response body. Build `tokenInput` as:

```
tokenInput = 0x0002 || SHA256(nonce) || SHA256(mac) || SHA256(token-key-id)
```

**Expected output**

```
tokenInput = 000205a11543a705818ed8325787a8d62e3671e7da0d86e7f03be85c3550efa2d3c56bf1d5b977f76c6f352c19125d517666d7e13683c5e6bfd20fda34f9a712367633677cf4111a585c9be12746e1d120b1eef6e2cd8d9faeccb6127510168ac689
```