Privacy Pass is an IETF protocol family that lets a client prove it passed some check (a solved CAPTCHA, a genuine unlocked device, a valid subscription) without telling the website who it is. A trusted issuer signs a blinded token, the client unblinds it, and the website verifies the signature but cannot link it back to the issuance.
RFC 9576 (the architecture), RFC 9577 (the PrivateToken HTTP authentication scheme) and RFC 9578 (two issuance protocols, one on a verifiable oblivious pseudorandom function and one on RSA blind signatures) were published together in June 2024. This post extends our explainer on browser fingerprinting. Fingerprinting recognises you without cookies; Privacy Pass tries to let sites stop abuse without recognising you at all.
Key takeaways
- Privacy Pass separates "this client passed a check" from "this is who the client is". The website (the Origin in RFC terms) learns only that a trusted Issuer signed a token, not which attestation was used or when.
- The architecture has four roles: Client, Attester, Issuer and Origin. Clients trust Attesters with private information but assume Issuers and Origins are malicious, so which roles one company may combine is the heart of the privacy analysis.
- RFC 9577 adds an HTTP authentication scheme: the server sends
WWW-Authenticate: PrivateToken challenge="...", token-key="..."and the client retries withAuthorization: PrivateToken token="...". IANA lists two token types:0x0001(VOPRF P-384) and0x0002(Blind RSA 2048). - Type
0x0001tokens can only be verified with the issuer's private key. Type0x0002tokens can be verified by anyone holding the public key, so any website can accept them without asking the issuer. - Apple shipped Private Access Tokens in iOS 16 and macOS Ventura (announced at WWDC22), using the
PrivateTokenscheme and RSA blind signatures, with an iCloud attester checking the device. Chrome's Private State Tokens are a separate JavaScript API that its specification calls a web platform implementation of a variant of Privacy Pass. - Tokens are not identity, not proof of a human, and not a full CAPTCHA replacement. RFC 9577 tells sites to keep working for clients that send no token and to skip challenging some clients at random so that fallback stays healthy.
- Privacy depends on anonymity set size. Extra metadata bits, per-user issuer keys, many issuers, and tight timing between challenge and redemption all shrink the crowd a client hides in.
What problem does Privacy Pass solve?
Websites facing credential stuffing, scraping and fake sign-ups need to tell likely-honest clients from automated abuse, and the usual tools all cost something. Interactive CAPTCHAs interrupt everyone, and the W3C's 2021 draft note on the inaccessibility of CAPTCHA says the interactive task "inherently excludes many people with disabilities". IP reputation punishes everyone behind a shared address. Fingerprinting based bot checks work by recognising browsers, which is the tracking capability privacy-focused browsers are trying to remove.
Privacy Pass reframes the problem. RFC 9576 describes it as "authorization based on privacy-preserving authentication mechanisms". Instead of a cookie saying "this browser solved a challenge yesterday" (which is linkable state), the client presents an unlinkable proof: a token that a trusted issuer signed after some check, constructed so that the token presented later cannot be matched to the moment it was issued.
According to RFC 9576, the original motivating case was rate limiting abusive traffic from services such as Tor without punishing every honest Tor user. The 2018 paper that introduced the protocol described users of Tor, VPNs and I2P being shown a disproportionate number of challenges by content delivery networks acting as "catch-all" filters. Its answer: solve one challenge, receive a batch of blinded tokens, and spend them later instead of solving more.
RFC 9576 lists two broad use cases today:
- A "low-quality, anti-fraud signal" for open internet services: the client is "likely not a bot", based on a solved CAPTCHA or "evidence of a valid, unlocked device in good standing". Note the words "low-quality": a token is one signal among several, not a verdict.
- Privacy-preserving authentication for proprietary services: the client is a valid subscriber, for example to an Oblivious HTTP deployment, without the service learning which subscriber made which request.
From a 2018 paper to IETF standards in June 2024
| Date | Milestone | Source |
|---|---|---|
| 2018 | Davidson, Goldberg, Sullivan, Tankersley and Valsorda publish the Privacy Pass paper in PoPETs volume 2018, issue 3: a one round trip protocol on an oblivious pseudorandom function, shipped as a browser extension with server components at a CDN. | PoPETs |
| 2020 | The IETF privacypass working group (Security Area) is chartered, starting from draft-davidson-pp-protocol, draft-davidson-pp-architecture and draft-svaldez-pp-http-api. | IETF Datatracker |
| 25 Oct 2021 | draft-private-access-tokens-01 is posted; it is later replaced by working group drafts. | IETF Datatracker |
| 16 Dec 2021 | W3C "Inaccessibility of CAPTCHA" Group Draft Note discusses Privacy Pass tokens as a way to reduce interactive challenges. | W3C |
| June 2022 | Apple's WWDC22 session "Replace CAPTCHAs with Private Access Tokens", new in iOS 16 and macOS Ventura. | Apple Developer |
| Oct 2023 | RFC 9474, RSA Blind Signatures, published by the IRTF Crypto Forum Research Group (Informational). | RFC Editor |
| Dec 2023 | RFC 9497, Oblivious Pseudorandom Functions (OPRFs) Using Prime-Order Groups, published by the CFRG (Informational). | RFC Editor |
| June 2024 | RFC 9576 (architecture, Informational), RFC 9577 (HTTP authentication scheme, Proposed Standard) and RFC 9578 (issuance protocols, Proposed Standard) published. | RFC Editor |
| 18 July 2024 | Latest Draft Community Group Report of the Private State Token API, which uses "the Privacy Pass protocol as an underlying primitive". | WICG |
| 17 Oct 2025 | Google's Privacy Sandbox update retires several APIs but says Chrome will "maintain Private State Tokens". | Privacy Sandbox |
| 2026 | Drafts on public metadata, an extensions parameter and token expiration are active. Batched token issuance (revision 09, 15 October 2026) is in IETF Last Call until 29 October 2026. | IETF Datatracker |
The working group charter states the core cryptographic goal in one sentence: an issuer "cannot link a redeemed token to one of N previously created tokens using the same key with probability non-negligibly larger than 1/N". Everything else in the architecture exists to stop the surrounding system from undoing that property.
Who does what: client, attester, issuer and origin
RFC 9576 defines four logical entities. They can be run by one company or by four, and the choice changes what each party can learn.
- Client. The device or app that wants access. In the vocabulary of the IETF Remote Attestation Procedures architecture (RFC 9334), the client plays the RATS Attester role: it produces evidence about itself.
- Attester. The party that checks something about the client: a solved CAPTCHA, a hardware-backed device attestation, an account in good standing, a geographic region. In RATS terms it is the Verifier that appraises evidence.
- Issuer. The party that holds the signing key and produces tokens for clients that a trusted attester has vouched for. Issuers publish their keys and are identified by a hostname.
- Origin. The website or API that challenges clients for tokens, verifies them and decides what to allow.
In the basic flow (RFC 9576, Section 3.1), the origin answers a request with a token challenge naming an issuer. A client with a matching cached token redeems it. Otherwise it runs the attestation the issuer requires, sends a blinded token request (usually through the attester), finalises the response into a token and retries the request with it. The token "can be validated by anyone with the per-Issuer key but cannot be linked to the content of the token request or token response".
Three contexts and four unlinkability goals
The architecture's privacy analysis rests on three bundles of information, which it calls contexts:
| Context | Who shares it | What it contains |
|---|---|---|
| Redemption context | Client and origin | Timestamp, IP address and other client-visible information, the origin name |
| Issuance context | Client, attester and issuer | Timestamp, IP address, the origin name if revealed during issuance (but not the full challenge, which stays hidden from the issuer) |
| Attestation context | Client and attester only | Whatever the attestation needs: device evidence, account identifiers, IP address |
From these, RFC 9576 defines four goals:
- Origin-Client unlinkability. Given two redemptions, the origin cannot tell whether they came from the same client. The set of clients sharing a redemption context is the redemption anonymity set.
- Issuer-Client unlinkability. The same property for the issuer across issuance events.
- Attester-Origin unlinkability. The attester cannot tell which origin a client was trying to reach.
- Redemption context unlinkability. Even if the issuer and attester help, the origin cannot connect a redemption to the issuance and attestation that produced it.
The trust model is asymmetric and worth stating precisely. Origins trust specific issuers. Issuers trust specific attesters to run attestation correctly. Clients trust attesters not to leak private information, but, in the RFC's words, "Clients assume that Issuers and Origins are malicious". That assumption is why the cryptography must hide the client's input from the issuer, and why the deployment model matters so much.
Why the attester and issuer must be separated, or trusted in specific ways
The attester is the party most likely to learn who the client is. A device attester sees hardware evidence; an account attester sees an account. If the same company also ran the origin, it could join "this account attested at 10:02" with "a token was redeemed at 10:02" and the privacy property would be gone. RFC 9576, Section 4, therefore treats operators drawn in the same box as able to collude, and analyses four deployment models.
| Deployment model | Who is combined | Attestation that is safe | Main constraint |
|---|---|---|---|
| Shared Origin, Attester, Issuer | All three run by one entity | Only non-identifying attestation (no account login) | Issuance and redemption must be separated over time (cached tokens with an empty redemption context) or over space (an anonymising service). RFC 9576 calls this the initial deployment of Privacy Pass. |
| Joint Attester and Issuer | Attester and issuer together, origin separate | Any attestation the joint party is trusted with | The attester and issuer must not learn the origin, so issuance protocols that reveal the origin to the issuer are not appropriate. |
| Joint Origin and Issuer | Origin and issuer together, attester separate | Any type, including account-based attestation | Needs more than one origin behind the issuer; an issuer serving a single origin lets the attester infer the origin from the token request. |
| Split Origin, Attester, Issuer | All three separate | Any type | The most general model. Per-origin tokens are not appropriate if the issuer serves only one origin. |
Two practical rules fall out of this table. First, if you want to use identifying attestation such as an account check, the attester must be a different party from the origin and the issuer must serve many origins. Second, if one company runs everything, the attestation must not identify anyone, and the client must put distance (time or network) between getting tokens and spending them.
Apple's WWDC22 session makes the same point in plain terms: it says each token issuer "needs to be a large service" working with "at least hundreds of servers", because otherwise the choice of issuer would reveal which website a client was visiting.
What an attester does, and what happens when attestation breaks
RFC 9576's example attestation procedures are solving a CAPTCHA, presenting device validity evidence from trusted hardware, and proving client state such as region or a valid account. Requiring several at once shrinks the anonymity set. The RFC is also frank about failure: if an attestation procedure is compromised, it "should be untrusted until the exploit is patched", and because an origin usually cannot tell which attester stood behind a token, every token from an issuer with even one compromised attester becomes suspect. Privacy Pass inherits the strength of whatever check it wraps.
How the PrivateToken HTTP authentication scheme works (RFC 9577)
RFC 9577 is the part a web developer touches. It reuses the standard HTTP authentication framework (RFC 9110, Section 11) with a new scheme name, PrivateToken, registered with IANA.
The challenge
An origin that wants a token replies with a header like this (Apple's documentation describes it arriving on a 401 response):
WWW-Authenticate: PrivateToken challenge="AAIAEmlzc3Vlci5leGFtcGxlLm5ldCCQ_mHF...", token-key="MIIBUjA9BgkqhkiG9w0BAQow...", max-age=300
The parameters are:
challenge(required): a base64url encodedTokenChallengestructure, with padding.token-key: the base64url encoded issuer public key for the token type. It may be omitted if clients can get the key some other way.max-age(optional): how many seconds the origin will accept this challenge.realm(optional), the standard HTTP parameter.
Clients must ignore parameters they do not understand. An origin can send several PrivateToken challenges in one header, for example one per token type or issuer, and the client should answer exactly one of them. RFC 9577 says origins should only offer alternatives with "functionally equivalent properties" and can sort a preferred option first.
The TokenChallenge structure
The default challenge is a small binary structure in TLS presentation language:
struct {
uint16_t token_type;
opaque issuer_name<1..2^16-1>;
opaque redemption_context<0..32>;
opaque origin_info<0..2^16-1>;
} TokenChallenge;
token_typeselects the issuance protocol. Clients must ignore challenges with token types they do not support.issuer_nameis a server name such asissuer.example.netorissuer.example.net:8443, with an implied port of 443 and no userinfo.redemption_contextis either empty or exactly 32 bytes. Any other length must be ignored.origin_infois empty (cross-origin), a single origin name (per-origin), or a comma-separated list with no spaces. If it is not empty, the challenging origin must include its own name, and the client must check that it does, case-insensitively, before fetching or redeeming anything.
Redemption context: choosing how fresh a token must be
The redemption context trades latency against replay control and privacy. RFC 9577 describes these options, the last three built with a pseudorandom function F:
| Redemption context | Construction | Effect |
|---|---|---|
| Empty | zero bytes | Tokens can be pre-fetched and cached; double-spend state must be shared across every origin server that accepts them until key rotation |
| Per request | 32 fresh random bytes | No caching; minimal replay state; issuance and redemption close in time |
| Time window | F(current time window) | Tokens reusable within a window |
| Client network | F(client ASN) | Tokens tied to the client's autonomous system |
| Window and network | F(time window, client ASN) | Both constraints |
Clients that cache tokens bound to a non-empty context should throw them away when they flush cookies or change networks, because otherwise the context value can act like a cookie that survives a reset. RFC 9577 lists this as the cross-context linkability attack.
The token and the Authorization header
After issuance the client retries the request with:
Authorization: PrivateToken token="AAJKiHk6DFEc..."
The default token structure is:
struct {
uint16_t token_type;
uint8_t nonce[32];
uint8_t challenge_digest[32];
uint8_t token_key_id[Nid];
uint8_t authenticator[Nk];
} Token;
The nonce is 32 random bytes chosen by the client. The challenge_digest is SHA-256 of the exact challenge bytes, which binds the token to that challenge. The token_key_id identifies the issuer key, and the authenticator is the issuer's contribution (a MAC-like PRF output or a signature) over the preceding four fields. For both registered token types, Nid is 32 bytes.
Worked example: how big are these messages?
Sizes matter for header limits and logging, so it helps to compute them once.
Challenge. Take issuer issuer.example.net (18 bytes), a fresh 32-byte redemption context and origin_info of www.example.com (15 bytes). The structure is 2 (type) + 2 + 18 (issuer name with length prefix) + 1 + 32 (context with length prefix) + 2 + 15 (origin info with length prefix) = 72 bytes. Base64 turns every 3 bytes into 4 characters, so 72 bytes become exactly 96 characters with no padding.
Type 0x0002 token (Blind RSA 2048). 2 + 32 + 32 + 32 + 256 = 354 bytes. 354 divided by 3 is 118, so the base64url value is 118 × 4 = 472 characters. Add the scheme name and parameter and the Authorization header is close to 500 bytes.
Type 0x0001 token (VOPRF P-384). 2 + 32 + 32 + 32 + 48 = 146 bytes. 146 is not a multiple of 3, so it rounds up to 49 groups, giving 196 characters including padding. Private verification costs the origin a call to the issuer's key, but the token is less than half the size.
What clients and origins are told to do
RFC 9577, Sections 3 and 4, set behaviour expectations that matter as much as the wire format:
- A client spends a given token only once; spending it twice would let origins link the requests.
- Clients should limit how many tokens an origin can make them fetch, since a hostile origin could send endless unique challenges to exhaust issuance or flood issuers.
- For open services, capable clients may ignore challenges "with some non-trivial probability", so services never depend on every client having a token.
- Origins should send random "greased" token types from 17 reserved values (such as
0x02AAand0xF057) so clients stay tolerant of unknown types. - Where tokens replace CAPTCHAs, origins should randomly skip challenging some clients so the no-token path keeps being exercised.
- Origins should prevent double spending by tracking token nonces within each redemption context.
Attacks the scheme anticipates
RFC 9577's security section names the attacks a deployment must design around:
| Attack | What happens | Mitigation in the RFC |
|---|---|---|
| Replay | A token is presented more than once | Constrain tokens to a single origin unless replay is harmless; anti-replay state if redemption has side effects (with extra care for 0-RTT data) |
| Reflection | A third party relays an origin's cross-origin, empty-context challenge to a victim client and forwards the resulting token | Per-origin challenges or non-empty redemption contexts |
| Token exhaustion | Any origin in a cross-origin set drains a client's cached tokens | Scope tokens to one origin with non-empty origin_info, or client-side limits per time window |
| Timing correlation | A challenge at T1 followed by issuance at a unique T2 lets a colluding issuer and origin link events | Clients may add delay within max-age; larger issuance volume |
| Cross-context linkability | A cached context-bound token acts like a cookie | Discard such tokens on cookie flush or network change |
How the two issuance protocols work (RFC 9578)
RFC 9578 defines how a client turns a challenge into a token. Both protocols are one round trip: an HTTP POST of application/private-token-request to the issuer and an application/private-token-response reply.
The idea: blind signing
Picture a notary signing the outside of a carbon-lined envelope without seeing the document inside. The document ends up signed, but the notary cannot later match it to the envelope. Blind signatures, which RFC 9474 traces to David Chaum's work on untraceable payments, are the mathematical version.
In Privacy Pass the document is the token input: token type, the client's random nonce, the SHA-256 digest of the challenge and the issuer key ID. The client blinds it, the issuer signs or evaluates the blinded value, and the client unblinds a valid authenticator over an input the issuer never saw. RFC 9576 requires this "unconditional input secrecy" regardless of hardness assumptions, plus one-more forgery security (N tokens cannot become N+1) and safety under concurrent use.
Type 0x0001: privately verifiable tokens with a VOPRF
The first protocol uses the verifiable oblivious pseudorandom function defined in RFC 9497, with the P-384 curve and SHA-384 (P384-SHA384).
An OPRF, in RFC 9497's description, is a two-party protocol where "the client learns the PRF output without learning anything about the PRF private key, and the server learns neither the PRF input nor output". The verifiable variant adds a proof that the server used the specific key it committed to, which stops an issuer from quietly using a different key for one client to tag it.
The steps in RFC 9578, Section 5, are:
- The client builds
token_inputas the 2-byte type0x0001, a fresh 32-byte nonce, the SHA-256 challenge digest and the 32-byte key ID, then blinds it. - It sends a
TokenRequestcontaining the token type, a one-byte truncated key ID and the 49-byte blinded element. The key ID is truncated to its last byte on purpose, "so that Issuers cannot use token_key_id as a way of uniquely identifying Clients". - The issuer checks the type, the truncated key ID and the size, returning HTTP 422 if any check fails. Otherwise it evaluates the blinded element with its private key and returns the 49-byte evaluated element plus a 96-byte proof (two 48-byte scalars).
- The client verifies the proof and finalises, producing a 48-byte authenticator.
- To verify a redeemed token, the issuer (or whoever holds the private key) recomputes the PRF over the token fields and compares.
The key ID is SHA-256 of the serialised public key. RFC 9578 says both protocols work with any deployment model, but because only the private key holder can verify, this type fits most naturally where the verifying party already holds the key.
Type 0x0002: publicly verifiable tokens with RSA blind signatures
The second protocol uses RSA blind signatures (RFC 9474) with a 2048-bit key, SHA-384 and RSA-PSS, in the RSABSSA-SHA384-PSS-Deterministic and RSABSSA-SHA384-PSSZERO-Deterministic variants. RFC 9474 says variants without randomised message preparation are safe when input messages have high entropy, and every token input carries a fresh 32-byte nonce.
The client blinds the token input with the issuer public key, sends a 259-byte request (type, truncated key ID, 256-byte blinded message), receives a 256-byte blind signature and unblinds it into a standard RSASSA-PSS signature. Section 6.4 specifies verification with SHA-384, MGF1 with SHA-384 and a 48-byte salt. The public key travels as a DER SubjectPublicKeyInfo with the id-RSASSA-PSS identifier, and the key ID is SHA-256 of that encoding.
The difference that matters, in the RFC's words: "any Origin can select a given Issuer to produce tokens, as long as the Origin has the Issuer Public Key, without explicit coordination or permission from the Issuer". A website can accept a platform issuer's tokens without a contract or a verification call per request.
Comparing the two token types
| Property | Type 0x0001 VOPRF(P-384, SHA-384) | Type 0x0002 Blind RSA (2048-bit) |
|---|---|---|
| Underlying primitive | Verifiable OPRF, RFC 9497 | RSA blind signature, RFC 9474 |
| Who can verify | Only the holder of the issuer private key | Anyone with the issuer public key |
| Authenticator size (Nk) | 48 bytes | 256 bytes |
| Token size | 146 bytes | 354 bytes |
| Token request size | 52 bytes | 259 bytes |
| Token response size | 145 bytes (element plus proof) | 256 bytes |
| Public or private metadata | No | No |
| Fits naturally with | Origin and issuer run by the same party | Many independent origins trusting one issuer |
Key encoding in token-key | Serialised P-384 element | DER SubjectPublicKeyInfo with RSASSA-PSS OID |
The issuer directory and key rotation
Issuers publish configuration at a well-known URL, /.well-known/private-token-issuer-directory, with media type application/private-token-issuer-directory. The JSON object has two fields: issuer-request-uri (where to POST token requests) and token-keys, a list of objects each with token-type, a base64url token-key and an optional not-before UNIX timestamp for staged rotations.
Clients use the first key whose not-before is absent or past; origins can try each listed key in order to absorb clock skew. RFC 9578 recommends HTTP caching of the directory (its example is Cache-Control: max-age=86400) and regular key rotation.
Rotation is a privacy issue as well as a security one. RFC 9576 explains that every active key configuration splits clients into groups, so "key rotations represent a trade-off between Client privacy and Issuer security". The worst case is an issuer that hands one user a unique key: every token that user spends would then be recognisable. That is why clients are told to bound the number of active configurations and to use a consistency mechanism to check they see the same keys as everyone else.
What is still being standardised
The IANA "Privacy Pass Token Types" registry, checked for this post, still lists only 0x0001 and 0x0002 as assigned, plus the 17 reserved grease values. Work in progress includes:
- Batched token issuance: many tokens per request; revision 09 is in IETF Last Call until 29 October 2026.
- Public metadata issuance, which binds public values to tokens. RFC 9576 warns every metadata bit can partition anonymity sets.
- An
extensionsparameter for thePrivateTokenscheme and a token expiration extension. - Rate-limited tokens: the working group draft has expired, but RFC 9576 still cites it as a protocol that needs the split deployment model.
- A related draft on Privacy Pass authentication for Media over QUIC.
Until published, wire formats in these drafts can change.
Where Privacy Pass tokens are deployed
Apple: Private Access Tokens
Apple's WWDC22 session "Replace CAPTCHAs with Private Access Tokens" (new in iOS 16 and macOS Ventura) describes the deployment:
- The server sends a
PrivateTokenchallenge naming an issuer it trusts. - The device sends a blinded token request to an iCloud attester, which performs device attestation "using certificates stored in the device's Secure Enclave", checks the account is in good standing and can rate-limit to spot compromised devices or device farms.
- The attester forwards the request to the issuer, which "doesn't know anything about the client" but signs because it trusts the attester. Tokens use RSA blind signatures, matching token type
0x0002. - Tokens need iOS 16 or macOS Ventura with an Apple ID signed in; the Apple ID "is only used for attestation, and is not shared with the servers that receive tokens".
- Safari, WebKit and
URLSessionanswer challenges automatically for foreground apps; if no token can be fetched, the app receives the original 401 response. - On the web, the challenge must come from a first-party domain, and Apple says it "should not block your main page load" because legacy clients will not respond.
This maps onto RFC 9576's split model: the platform attester sees the device and account, a separate issuer signs blindly, and the origin sees only the token.
Chrome: Private State Tokens, a related but different design
Google's Privacy Sandbox documentation describes Private State Tokens (formerly Trust Tokens) as letting trust in a user's authenticity "be conveyed from one context to another" to help sites fight fraud without passive tracking: an issuer site gives tokens to a browser it trusts, and another site redeems them. Google stresses they convey trust rather than establish it, and "are not a replacement" for mechanisms that do.
The WICG specification (18 July 2024) calls the API "a web platform implementation of a variant of Privacy Pass". It is driven by JavaScript and Sec-Private-State-Token headers rather than the PrivateToken scheme, uses protocol version PrivateStateTokenV1VOPRF, lets issuers hold up to six keys that act as labels, allows at most two issuers per top-level site, and permits a third redemption per origin only after an implementation-defined period, "usually 48 hours". Google's documentation records a closed origin trial (Chrome 84 to 101), and its 17 October 2025 update says Chrome will "maintain Private State Tokens".
Challenge pages run by large networks and hosting providers
The 2018 paper's server side ran at a content delivery network, and RFC 9576 calls that shared Origin, Attester and Issuer arrangement the initial deployment of Privacy Pass. Apple expects issuers to be run by "your existing CAPTCHA provider, your web hosting service, or your content delivery network". If you use such a provider, its own documentation is the authority on which token types and issuers it accepts and what happens when no token arrives.
Subscriber access and anonymising services
RFC 9576's second use case is authenticating paying subscribers without identifying them, with Oblivious HTTP (RFC 9458, January 2024) as its example. Here tokens are mandatory rather than a CAPTCHA substitute, and the joint origin and issuer model fits well: the service issues tokens to subscribers through an account-checking attester, then accepts them through a relay that hides client addresses. Our explainer on Oblivious HTTP relays and gateways covers that half of the system.
What are the privacy limits?
Privacy Pass guarantees one link (issuance to redemption) inside a system that can leak elsewhere. RFC 9576 says the value of unlinkability depends on anonymity set size: a set of two is technically unlinkable and practically useless.
Anonymity set size
Every token type, issuer, key, metadata value and redemption context divides clients into groups. Count them in bits, as fingerprinting research does: telling apart 8 configurations is log2(8) = 3 bits, and combined with IP address and timing a few bits go a long way.
Metadata bits
RFC 9576's blunt example: an issuer wanting to track one client "could add a single metadata bit" to its tokens, and one key per metadata value gives log2(N) bits for N keys. The RFC 9578 token types carry no metadata; extensions that add it need a budget.
Issuer key rotation as a tracking channel
Each key configuration is a partition, so an issuer that rotates too often or serves different keys to different clients sorts them into small buckets. The defences are consistency checks (clients confirm they see the same directory as everyone else) and limits on active keys per issuer.
Many issuers
An origin that accepts many issuers can challenge for each in turn and learn which ones a client satisfies, an "exponential loss in privacy relative to the number of Issuers" in RFC 9576's words. Clients should bound active configurations; Chrome's two-issuer limit per site is one example.
Rate limiting by attesters
Rate limiting needs per-client state at the attester. That is acceptable only while the attester is separate from the origin; combine them and the rate-limit counters become a tracking database.
Timing and side channels
With a fresh per-request context, the issuance anonymity set is only the clients that started issuance between challenge and redemption, which may be small for a quiet issuer. RFC 9577 lets clients add delay within max-age.
Discrimination and centralisation
RFC 9576, Section 5, raises two ecosystem risks. If an open website accepts only tokens from device attestation, people with older devices, other operating systems or no account are pushed to the back of the queue. The RFC calls diversity in attestation "critically important" for open services and gives the example of an issuer also supporting CAPTCHA-based attesters, while acknowledging that the token is then only as strong as the weakest accepted attestation. And because privacy improves with fewer, larger issuers, the protocol pushes toward concentration. The RFC suggests multi-stakeholder governance or distributing attestation across many parties.
Hoarding
Tokens are not bound to a device after issuance, so many clients can collect cacheable tokens for one abuser. RFC 9576 suggests rate-limiting issuance and comparing issuance and redemption context distributions.
CAPTCHAs, tokens, device attestation and fingerprinting compared
| Approach | What the site learns | Privacy | Accessibility | Abuse resistance | Works for clients without support |
|---|---|---|---|---|---|
| Interactive CAPTCHA | That someone solved a puzzle in this session | Moderate on its own, but often paired with IP reputation and tracking | Poor; the W3C note documents how interactive tests exclude many disabled users | Declining; the same W3C note says many popular techniques are no longer particularly effective | Yes, by design |
| Privacy Pass tokens (RFC 9577 and 9578) | That a trusted issuer signed a token for this challenge | Strong if anonymity sets are large and deployment rules are followed | Good for supported clients; depends on fallback for everyone else | Bounded by the weakest attester the issuer trusts; vulnerable to hoarding without issuance limits | Only through a fallback path, which RFC 9577 says to keep exercised |
| Direct device attestation to the site | Device model or integrity verdict, often with a stable identifier | Weak to moderate; the site sees attestation details directly | Excludes unsupported or modified devices | High for supported hardware until the attestation is broken | No |
| Fingerprinting based bot checks | A detailed profile of the browser and device | Poor; it is the tracking technique privacy browsers fight | Invisible to users, but misfires on privacy tools and assistive setups | Moderate; arms race with automation tools | Yes, but with false positives |
| Account login | Who the user is | None toward the site | Depends on login design | Good, but costly for open content | No |
The conclusion is layered: use Privacy Pass to remove friction for clients that can present a token, and keep a fallback (a CAPTCHA, a rate limit, a login) for the rest. As a hard gate on open content it becomes the discriminatory treatment RFC 9576 warns about.
Step by step: inspect a PrivateToken challenge, decode it and check the issuer directory
These steps are for operators and developers checking their own sites and their own staging environments. They inspect what a server sends and what an issuer publishes. They do not generate tokens and do not get around any check.
Step 1: see whether your site sends a PrivateToken challenge
On a staging host you control, request a page your bot protection challenges and print the headers:
curl -s -o /dev/null -D - https://staging.example.com/login \
| grep -i '^www-authenticate'
In browser developer tools, open the Network panel, reload and look for WWW-Authenticate: PrivateToken on the 401 response. A supporting browser then retries with an Authorization: PrivateToken header. A challenge with no retry means the client could not get a token, so your fallback is what that user sees.
Step 2: decode the challenge
Save the challenge value to a file and decode it with this standard-library script, which parses the RFC 9577 TokenChallenge and names the token type:
#!/usr/bin/env python3
# decode_challenge.py: parse a PrivateToken TokenChallenge (RFC 9577)
import base64, struct, sys
TYPES = {0x0001: "VOPRF(P-384, SHA-384), privately verifiable (RFC 9578 s5)",
0x0002: "Blind RSA (2048-bit), publicly verifiable (RFC 9578 s6)"}
GREASE = {0x0000, 0x02AA, 0x1132, 0x2E96, 0x3CD3, 0x4473, 0x5A63, 0x6D32,
0x7F3F, 0x8D07, 0x916B, 0xA6A4, 0xBEAB, 0xC3F3, 0xDA42, 0xE944, 0xF057}
def b64url(s):
s = s.strip().strip('"')
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
raw = b64url(open(sys.argv[1]).read())
(ttype,) = struct.unpack_from("!H", raw, 0); off = 2
(ilen,) = struct.unpack_from("!H", raw, off); off += 2
issuer = raw[off:off + ilen].decode("ascii"); off += ilen
clen = raw[off]; off += 1
ctx = raw[off:off + clen]; off += clen
(olen,) = struct.unpack_from("!H", raw, off); off += 2
origins = raw[off:off + olen].decode("ascii"); off += olen
label = TYPES.get(ttype, "RESERVED (grease)" if ttype in GREASE else "unknown")
print(f"token_type 0x{ttype:04x} {label}")
print(f"issuer_name {issuer}")
print(f"redemption_ctx {'empty (cacheable)' if clen == 0 else ctx.hex()} ({clen} bytes)")
print(f"origin_info {origins or '(empty: cross-origin)'}")
print(f"total length {len(raw)} bytes, trailing bytes: {len(raw) - off}")
if clen not in (0, 32):
print("WARNING: redemption_context must be 0 or 32 bytes; clients will ignore this challenge")
Run it:
python3 decode_challenge.py challenge.txt
Check the output against your intent: the right token type and issuer, an empty or 32-byte context as designed, and an origin_info that includes the challenging host. If it does not, RFC 9577 requires clients to discard the challenge and every visitor silently falls back.
Step 3: fetch and read the issuer directory
The issuer name in the challenge tells you where its directory lives:
curl -s -D directory.headers \
-H 'Accept: application/private-token-issuer-directory' \
https://issuer.example.net/.well-known/private-token-issuer-directory \
-o directory.json
grep -iE '^(content-type|cache-control):' directory.headers
python3 -m json.tool directory.json
You should see issuer-request-uri and a token-keys array. Note each entry's token-type, any not-before and the order (clients use the first currently valid key), and compare Cache-Control with the issuer's rotation schedule.
Step 4: confirm the challenge key matches the directory and compute the key ID
For type 0x0002, the token-key is a DER SubjectPublicKeyInfo. Decode it, inspect it and compute the key ID that will appear in tokens:
python3 - <<'EOF' > spki.der
import base64, json, sys
key = json.load(open("directory.json"))["token-keys"][0]["token-key"]
sys.stdout.buffer.write(base64.urlsafe_b64decode(key + "=" * (-len(key) % 4)))
EOF
openssl asn1parse -inform DER -in spki.der | head -20
openssl dgst -sha256 spki.der
The asn1parse output should show rsassaPss, sha384 and a salt length of 30 hex (48), as in RFC 9578's example. The SHA-256 hash is the 32-byte token_key_id; its last byte is the truncated ID clients send to the issuer, and RFC 9578 asks issuers to avoid two active keys sharing it. Then compare your challenge's token-key with the directory: a hard-coded key the issuer has rotated out silently breaks the friction-free path.
Step 5: verify a test token in staging
In staging, capture one Authorization: PrivateToken header from a test client. For type 0x0002, the first 98 bytes are the signed input and the last 256 bytes the authenticator:
python3 - <<'EOF'
import base64
t = open("token.txt").read().strip().strip('"')
raw = base64.urlsafe_b64decode(t + "=" * (-len(t) % 4))
assert len(raw) == 354 and raw[:2] == b"\x00\x02", "not a type 0x0002 token"
open("token_input.bin", "wb").write(raw[:98])
open("authenticator.bin", "wb").write(raw[98:])
print("challenge_digest", raw[34:66].hex())
print("token_key_id ", raw[66:98].hex())
EOF
openssl pkey -pubin -inform DER -in spki.der -out issuer.pem
openssl dgst -sha384 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:48 \
-sigopt rsa_mgf1_md:sha384 -verify issuer.pem \
-signature authenticator.bin token_input.bin
These commands need OpenSSL 3.0 or later; we checked them against a locally signed RSA-PSS test token, not a production issuer. Also confirm challenge_digest equals SHA-256 of the challenge you issued and token_key_id matches Step 4. In production, do this in code with a maintained library.
Step 6: test the behaviour around the token
Signature checks are not enough. In staging:
- Replay the same
Authorizationheader twice. The second attempt should be rejected by your double-spend store. - Send the request with no token at all. The page should still work through your fallback.
- Send a token minted for a different challenge. The
challenge_digestcheck should reject it. - Rotate a test key and confirm both old and new keys verify during the overlap window.
- Confirm your logs do not store raw tokens or nonces alongside IP addresses and account IDs. That join is exactly what the protocol was designed to prevent.
Common mistakes
- Treating a token as user identity. It says "some client a trusted issuer vouched for", nothing more. Never attach it to an account or log it next to identifiers.
- Treating a token as proof of a human. Device attestation shows a genuine device in good standing; a device can still run automation. RFC 9576 calls this a "low-quality" anti-fraud signal. Combine it with rate limits and business logic.
- Using a hard gate on open content. Requiring a token, with no fallback, discriminates against users of older devices, other platforms and assistive setups, which RFC 9576, Section 5.1, warns against. Apple's own guidance is that the check should not block the main page load.
- Single issuer key per user, or too many keys. Unique or frequently rotated keys partition clients into small anonymity sets. Keep the set of active keys small, public and identical for everyone.
- Forgetting double-spend prevention. With an empty redemption context, replay state must be shared across every server that accepts tokens until key rotation. Teams often implement it per server and leave a gap.
- Getting
origin_infowrong. Leaving your own hostname out of a non-empty list makes every compliant client discard the challenge. - Hard-coding the issuer key. Read the directory, respect
not-beforeand caching, and try each listed key in order. - Ignoring accessibility for users without supported devices. Your fallback is the experience for a meaningful share of users. Test it with screen readers and keyboard-only navigation, not just with a supported phone.
- Accepting many issuers "for coverage". Each extra issuer is another bit an origin can learn. Choose few, large issuers.
- Building on drafts. Batched tokens, metadata and expiration are still Internet-Drafts.
FAQ
What is Privacy Pass in simple terms?
A trusted party vouches for your device or browser without telling the website who you are. You pass a check, get blinded tokens, and present one when a site asks. The site verifies the issuer's signature but cannot link the token to you.
Is Privacy Pass the same as Private Access Tokens?
Private Access Tokens are Apple's deployment of the Privacy Pass PrivateToken scheme on iOS 16, macOS Ventura and later, with an iCloud attester and RSA blind signatures. Privacy Pass is the general IETF protocol family defined in RFCs 9576, 9577 and 9578.
Do Private Access Tokens replace CAPTCHAs completely?
No. They let supporting devices skip a CAPTCHA, but sites must keep a fallback for clients that cannot or do not present a token. RFC 9577 even tells origins to skip challenging some clients at random so the fallback keeps working.
Do Privacy Pass tokens work in apps, or only in browsers?
Both, where the platform supports them. Apple's WWDC22 session says apps that use URLSession or WebKit answer PrivateToken challenges automatically while in the foreground, and receive the original 401 response if a token cannot be fetched, so the app can fall back.
Can a website track me with Privacy Pass tokens?
Not through the token itself, if the deployment follows the RFCs. Sites can still use IP addresses, cookies and fingerprinting, and a badly run issuer could split users with per-user keys or metadata.
What is the difference between RFC 9576, RFC 9577 and RFC 9578?
RFC 9576 is the informational architecture: roles, privacy goals and deployment models. RFC 9577 is the HTTP authentication scheme that carries challenges and tokens. RFC 9578 defines the two issuance protocols that turn a challenge into a token.
What is the difference between publicly and privately verifiable tokens?
Privately verifiable tokens (type 0x0001, VOPRF) can only be checked by the issuer's private key holder. Publicly verifiable tokens (type 0x0002, Blind RSA) can be checked by any server with the issuer's public key, so any website can accept them without coordinating with the issuer.
Is Chrome's Private State Tokens API the same as Privacy Pass?
Related but different. Its specification calls it a variant of Privacy Pass, driven by JavaScript and its own headers instead of the PrivateToken scheme.
Can tokens be shared or resold?
Yes. RFC 9576 calls this a "hoarding attack". Defences include issuance rate limits at the attester, per-origin or per-context challenges, and watching for mismatches between issuance and redemption patterns.
Where Safeguard fits
Safeguard's fit with Privacy Pass is indirect, and it is worth being exact about it. Safeguard does not run attesters or issuers, does not detect bots, and does not evaluate whether a deployment's anonymity sets are large enough. Those are design decisions for the teams operating origins and issuers.
What Safeguard does cover is the software those teams build and ship. An origin that verifies tokens depends on an HTTP stack, a cryptography library for RSA-PSS or P-384, a Privacy Pass implementation and often a container image. Safeguard scans repositories, container images and SBOM files (safeguard scan --dir ., safeguard scan --image <name>, safeguard scan --sbom <file>), matches components against vulnerability databases, ranks findings with reachability analysis, shows which projects contain a given component version, and can open fix pull requests through autonomous remediation, which ships switched off. When a flaw lands in a blind signature or elliptic curve library, the useful question is which services carry that version, and that is the question Safeguard answers.
Token verification is only as trustworthy as the cryptography and HTTP libraries underneath it, and those arrive as open source dependencies in your origin servers, issuers and client apps. When one of them needs a fix, the gap between a patch existing and your services running it is the part you control.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. It asks the assistant to fix a lodash CVE in a repository, reads the finding and advisory, asks before each command, edits package.json to a fixed version, and runs npm install and npm test with approval, pushing nothing. The same flow applies when a scan of your token-verifying service flags an outdated cryptography or HTTP library: you ask for the upgrade and approve each step.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with a device code and then scans from the terminal. It suits a team that wants an inventory of the libraries inside an issuer or origin service before a privacy or security review, starting from a machine it controls.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. It is useful when you want to ask an assistant which of your projects carry a particular version of a blind signature or elliptic curve library while you work through the checks above.
Click through it at your own pace, or open it full screen.
To try it on your own projects, download Safeguard and follow the CLI documentation.
Quick check
Your origin sends a PrivateToken challenge with origin_info set to "a.example.com" from the host b.example.com. What happens?
RFC 9577 requires clients to check that a non-empty origin_info list includes the name of the origin that sent the challenge. b.example.com is not in the list, so compliant clients discard the challenge and never fetch or redeem a token. Every visitor falls through to your fallback path.
Why can any website accept type 0x0002 tokens from a platform issuer without asking that issuer to verify them?
Type 0x0002 uses RSA blind signatures, so the finished token is an ordinary RSA-PSS signature that anyone holding the issuer public key can verify. The issuer never learns which origin the token was for. Type 0x0001 tokens, built on a VOPRF, can only be verified with the issuer private key.
Sources
- RFC 9576: The Privacy Pass Architecture (June 2024)
- RFC 9577: The Privacy Pass HTTP Authentication Scheme (June 2024)
- RFC 9578: Privacy Pass Issuance Protocols (June 2024)
- RFC 9474: RSA Blind Signatures (October 2023)
- RFC 9497: Oblivious Pseudorandom Functions (OPRFs) Using Prime-Order Groups (December 2023)
- RFC 9110: HTTP Semantics, Section 11 (Authentication)
- RFC 9334: Remote ATtestation procedureS (RATS) Architecture (January 2023)
- RFC 9458: Oblivious HTTP (January 2024)
- IETF Datatracker: Privacy Pass working group charter
- IETF Datatracker: Privacy Pass working group documents
- IANA: Privacy Pass Token Types registry
- Davidson, Goldberg, Sullivan, Tankersley, Valsorda: Privacy Pass: Bypassing Internet Challenges Anonymously (PoPETs 2018, issue 3)
- Apple Developer: Replace CAPTCHAs with Private Access Tokens (WWDC22)
- Privacy Sandbox: Private State Tokens
- WICG: Private State Token API (Draft Community Group Report, 18 July 2024)
- Privacy Sandbox: Update on Plans for Privacy Sandbox Technologies (17 October 2025)
- W3C: Inaccessibility of CAPTCHA (Group Draft Note, 16 December 2021)
Related reading: browser fingerprinting explained, Oblivious HTTP explained and is it safe to self-host apps on the internet.
Last reviewed: 24 October 2026. Draft extensions to Privacy Pass are still moving through the IETF; check the working group's document list for the latest status.