For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Help CenterOpenAPI SpecStatus
OverviewInquiriesTransactionsRelaysAPI ReferenceChangelog
OverviewInquiriesTransactionsRelaysAPI ReferenceChangelog
  • Overview
    • Relay Overview
    • Getting Started
  • Widget
    • Usage
  • Integration Methods
    • Server SDK
      • Usage
        • Challenge Mechanism
        • Blind RSA
        • Implementation Verification
  • References
    • Relay Widget SDK
    • Server SDK
    • Gateway Service
    • API
    • Schemas
LogoLogo
Help CenterOpenAPI SpecStatus
On this page
  • Test key pair
  • Test 1 — blind()
  • Test 2 — finalize()
  • Test 3 — buildTokenInput()
Integration MethodsAPIPrivacy Pass Protocol

Implementation Verification

Was this page helpful?
Previous

Relay Widget SDK Reference

Next
Built with

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 (RSABSSA-SHA384-PSS-Randomized).

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

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

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 fixed WWW-Authenticate header and combine with the fixed nonce to produce the expected tokenInput.

Inputs

WWW-Authenticate: PrivateToken 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"
nonce = a3f8c2d1e5b4a7f0c9d2e1b6a5f4c3d2e1b0a9f8c7d6e5f4a3b2c1d0e9f8a7b6

The decoded challenge JSON:

1{
2 "token_type": 2,
3 "issuer_name": "withpersona.com/api/v1/privacy-passes",
4 "origin_info": "/api/privacy/v1/relays",
5 "expires_at": "2099-01-01T00:00:00Z",
6 "mac": "b1e9d4c7a2f5e8d3b6a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0"
7}

Extract mac from the decoded challenge and token-key-id from the header. Build tokenInput as:

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

Expected output

tokenInput = 000205a11543a705818ed8325787a8d62e3671e7da0d86e7f03be85c3550efa2d3c56bf1d5b977f76c6f352c19125d517666d7e13683c5e6bfd20fda34f9a712367633677cf4111a585c9be12746e1d120b1eef6e2cd8d9faeccb6127510168ac689