> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.withpersona.com/2020-05-18/relay-privacy-pass-implementation-verification/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 ``` > Test vectors to verify your blind RSA and tokenInput implementation.