Complete technical documentation for the OpenAgeProof protocol version 0.15.
OpenAgeProof — an open protocol for privacy-preserving age assurance.
Status: Draft. This document describes a protocol design that has not been implemented or audited. Do not deploy.
Changes from 0.14: Consistency pass. §0 first bullet rewritten — previous "no credential-issuance record exists" overclaimed; replaced with accurate "the issuer holds no information that identifies you or your token." §0 second bullet and §1.1 last bullet generalized from "credit card" to "payment card" (per the credit + adult debit acceptance settled in v0.14). §2 issuance description generalized from "small card charge" to "card validation" to cover zero-dollar authorization. §10.8 retitled "Issuer-side absence of token information" and rewritten to reflect that the issuer never had token information rather than having it but unable to link.
Normative keywords: The terms MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as in RFC 2119.
OpenAgeProof is one proposal among several for privacy-preserving age assurance. It differs from the leading alternatives in the choice of anchor and the threat model it prioritizes. Readers evaluating OpenAgeProof should understand how it relates to existing work:
OpenAgeProof's distinctive properties in this landscape are:
These are trade-offs, not strict improvements. OpenAgeProof's age assurance is probabilistic (card-possession correlates with adulthood) rather than cryptographic (government attestation of date of birth). Implementers should choose based on threat model rather than on a single axis.
OpenAgeProof is designed for contexts in which a service must verify that a user is an adult (18+ or the applicable age of majority) before granting access, and where the user prefers not to disclose their identity, date of birth, or biometric data to achieve this.
In scope:
Out of scope:
The legal status of payment-card-based age assurance varies by jurisdiction. Implementers are responsible for determining whether OpenAgeProof meets local requirements. This section summarizes known compatibility as of the document date; implementers must verify current law before deployment.
Strong fit (payment card verification is an accepted method, standalone):
Compatible as one component, not standalone:
Not applicable:
The OpenAgeProof flow has three phases:
┌────────┐ (1) card validation ┌──────────┐
│ Holder │ ─────────────────────────▶ │ Issuer │
│ │ ◀───────────────────────── │ │
│ │ (2) token └──────────┘
│ │ ▲
│ │ │ bulk fetch:
│ │ │ - public keys (§6.2)
│ │ │ (cached, refreshed periodically,
│ │ │ NOT per-token)
│ │ │
│ │ (3) token presentation ┌──────┴──────┐
│ │ ─────────────────────────▶ │ RP │
│ │ │ verifies │
│ │ │ locally │
└────────┘ └─────────────┘
The issuer is contacted only at issuance (step 1) and, separately and non-per-token, for bulk distribution of public keys. The issuer does not participate in individual verifications.
A token is a signed structure containing:
| Field | Type | Description |
|---|---|---|
v | integer | Protocol version. MUST be 1 for this draft. |
iss | string | Issuer identifier (URI). |
iat | integer | Issuance time (Unix seconds). |
exp | integer | Expiration time (Unix seconds). Set per §4.3 (recommended 1 year from issuance, max 2 years). |
jti | bytes | Unique token identifier. MUST be unpredictable (≥128 bits of entropy). |
scope | string | Optional. Assurance level or market. See §3.1. |
sig | bytes | Issuer signature over the above fields. |
Tokens MUST be serialized as compact JSON with fields in the order above, then signed. Signature algorithm is specified in §6.
The scope field carries a machine-readable description of what the token asserts. Implementations MUST support at least:
age-18 — issuer asserts card-issuer underwriting requires holder to be 18+age-21 — issuer asserts card-issuer underwriting requires holder to be 21+ (rare; some US states have 21+ credit products)Additional scopes MAY be defined. Relying parties MUST reject tokens whose scope they do not recognize.
Tokens MUST NOT contain:
Before issuing a token, the issuer MUST:
payment_method.card.fingerprint. This fingerprint MUST be opaque to the issuer in the sense that it cannot be reversed to a PAN.CARD_ALREADY_USED.Before signing a token with the age-18 scope, the issuer MUST verify that the card's product category corresponds to an adult-only profile. This verification MUST use the payment processor's card metadata as the primary source (e.g., Stripe's card.funding and card.brand fields, or the equivalent from other processors). A local BIN database MAY be used as a fallback only when the processor does not expose sufficient metadata; such a database MUST be refreshed at least monthly.
An adult-only profile means a card product whose issuance by the card issuer requires the holder to be at least 18 (or the applicable age of majority in the issuing jurisdiction). This includes standard credit cards, charge cards, and adult debit cards tied to deposit accounts requiring adult account holders. This excludes:
Issuers MUST reject cards falling into these categories for age-18 scope tokens and return CARD_INELIGIBLE_SCOPE.
Some teen/minor debit products are issued under BINs that overlap with adult debit products from the same card issuer. BIN verification alone cannot reliably distinguish these cases. Issuers SHOULD maintain a deny-list of known minor-accessible products and update it as such products are identified. This is an active area of the protocol's threat model and is expected to improve as card networks publish more granular product metadata.
For the age-21 scope, the issuer MUST additionally verify that the card product's underwriting threshold is at least 21. Most consumer cards do not meet this threshold; implementations supporting age-21 should document which card products they accept.
Scopes are to be interpreted as lower bounds on the card's underwriting threshold. A card underwritten at 19 (e.g., a standard credit card issued in a Canadian province where the age of majority is 19) satisfies the age-18 scope. A card underwritten at 18 does not satisfy the age-21 scope.
The issuer needs to confirm two things about the card at issuance time: that it is a real, active card capable of being charged, and that it produces a stable fingerprint for one-token-per-card enforcement (§4.2). Two payment processor mechanisms can produce both:
Either mechanism produces the fingerprint required for protocol enforcement. The choice between them is operational, not protocol-level. RPs accepting tokens cannot tell which mechanism the issuer used.
Issuers SHOULD use a charge rather than zero-dollar authorization, for the reasons in §4.1.3. Zero-dollar authorization is an acceptable alternative for issuers operating under specific economic models (e.g., grant-funded, donation-supported) where charging users is undesirable.
Issuers operate at a non-zero cost: per-validation network fees from the payment processor, infrastructure costs, and operational overhead. Fee structures determine how those costs are recovered.
The recommended structure is a flat per-issuance fee charged at issuance time, sufficient to cover the issuer's operational costs. Realistic minimum fees in 2026 are approximately $1.00–$2.00 USD, dominated by processor fixed fees (typically $0.25–$0.50 per transaction).
Other structures are permitted:
The fee structure does not affect interoperability — RPs accepting tokens do not see how the issuer was funded. Each issuer chooses its own model and discloses it in service documentation.
Beyond cost recovery, charging serves additional purposes:
These considerations push toward charging at the recommended fee level even when zero-dollar authorization is technically sufficient for card validation.
Issuers MAY charge more than cost recovery to build reserves or fund growth, but non-profit reference implementations SHOULD publish a cost breakdown to support auditability and user trust. Implementations that subsidize issuance below cost MUST disclose the subsidy source.
Issuers MUST reject payments that originate from tokenizing wallets such as Apple Pay, Google Pay, Samsung Pay, or other digital wallets that present a Device Primary Account Number (DPAN) or similar token rather than the underlying card.
In Stripe's API, this corresponds to rejecting any payment method where card.wallet is non-null. Equivalent fields exist in other payment processors' APIs.
The reason: tokenizing wallets produce different fingerprints for the same underlying card depending on which device or wallet was used to make the payment. A user with one card and three devices (phone, tablet, watch) could mint three tokens by paying via each. This breaks the protocol's "one card, one token" property — the foundational anti-Sybil guarantee that the protocol depends on.
Direct card entry produces a fingerprint that reflects the underlying card itself, ensuring that one card can produce only one active token regardless of how many devices the holder owns.
When rejecting a wallet payment, issuers SHOULD return error WALLET_NOT_SUPPORTED and direct the user to enter their card details directly. The error message SHOULD explain that the rejection is not a flaw in the user's card but a protocol requirement to ensure each card produces a single token.
This requirement may be relaxed in future versions of the protocol if payment processors expose stable card-level fingerprints for wallet payments. Until that capability is available and verified, direct card entry is required.
The issuer MUST store, for each issued token:
The issuer MUST NOT store:
jti (which the issuer cannot derive from blind issuance)The fingerprint is an opaque token from the processor's side; it cannot be used by an attacker without the corresponding card. Storing it enables one-token-per-card enforcement without creating a PII database. The token's expiration date is metadata about the protocol's operation, not about the user, and is stored to allow re-issuance after expiration.
This minimalism is deliberate. The issuer's database, after issuance, holds two values per token: an opaque fingerprint and a date. Neither identifies the user. Neither links the issuer's records to a specific token in the wild. A subpoena to the issuer reveals only that some card was used to obtain a token expiring on a particular date.
After a token's expiration date passes, the issuer MAY purge the corresponding row to allow re-issuance on the same card. Implementations SHOULD purge expired entries periodically. Implementations operating a "per-card with free renewals" fee model (§4.1.3) MAY retain expired entries to recognize repeat customers.
Issuers SHOULD set token expiration to 1 year from issuance. Issuers MAY set shorter expirations (no less than 6 months) for higher-security contexts or longer expirations (no more than 2 years) where operational constraints justify it. Issuers MUST publish their chosen lifespan in their service documentation.
Token expiration is independent of the card's expiration date. The card's role is to prove adulthood at issuance; once that proof is established, the card's continued validity does not affect the token. Constraining token lifespan based on card expiration would penalize users with older cards without providing meaningful additional protection — the lifespan cap below already bounds the relevant damage.
A 1-year lifespan bounds the impact of several events the protocol cannot detect at issuance time:
The issuer stores the token's expiration date alongside the fingerprint (§4.2) so that re-issuance on the same card can be authorized once the previous token has expired. This is metadata about the protocol's operation, not about the user; it does not weaken the privacy properties in §10.8.
When the previous token expires, the same card MAY be used to obtain a new token. The issuer's record of the previous issuance is purged or replaced with the new one.
Issuers MUST use a blind signature scheme. The recommended scheme is the Privately Verifiable Token issuance from IETF Privacy Pass (RFC 9576).
In blind issuance:
The issuer's database, after issuance, contains only the card fingerprint and the token's expiration date (per §4.2). The blinded value flows through the issuance protocol but is not retained — it has no operational purpose after the signature has been returned to the holder. The unblinded token (containing the actual jti that appears at relying parties) is mathematically unrecoverable from the issuer's records.
This property is what enables the subpoena-resistance claim in §10.8. Implementations that omit blind issuance do not provide this property and MUST NOT claim it.
The holder presents the token to the relying party through an implementation-defined channel (HTTP header, cookie, form field, QR code, etc.). The protocol does not mandate a transport.
For web contexts, a recommended transport is an HTTP header:
OpenAgeProof-Token: <base64url-encoded token>
On receipt of a token, the relying party MUST:
iss value. Keys MAY be cached but caches MUST honor the issuer's published key rotation metadata.iat is in the past and exp is in the future, with an implementation-defined clock skew tolerance (SHOULD be ≤5 minutes).scope meets the relying party's requirement.If all checks pass, the relying party accepts the token as valid age assurance for the scope indicated.
All verification described in §5.2 MUST be performed locally by the relying party, using cached issuer signing keys (§6.2). The relying party MUST NOT contact the issuer on a per-token basis to verify a token.
Issuers MUST NOT offer a remote token-verification endpoint — that is, an endpoint that accepts a full token (or a jti) as input and returns a validity decision. Endpoints that serve key material (§6.2) are permitted because they are bulk, non-token-specific, and cacheable; they do not reveal individual verification events to the issuer.
Rationale. Any remote verification call made by a relying party reveals to the issuer, through standard HTTP request metadata (source IP, TLS handshake, request headers, reverse DNS), which relying party is performing the verification. Over many such calls, the issuer can accumulate a per-token log of which services a token is being presented to and when — effectively a browsing history for the token holder. This reintroduces the surveillance problem that OpenAgeProof is designed to prevent, regardless of any policy commitment by the issuer not to log such data.
The protocol's privacy guarantees are therefore structural, not policy-based: the issuer cannot observe presentations that never reach its servers. Prohibiting the remote verification endpoint is the mechanism that enforces this property at the protocol level rather than the trust level.
Relying parties that cannot perform local cryptographic verification (e.g., severely constrained embedded environments) are not supported in this version of the protocol. Remote verification is not a compliant fallback.
OpenAgeProof does not provide per-token revocation. Tokens expire at the date specified in their exp claim (per §4.3) and become invalid at that point. There is no revocation list.
This is a deliberate design choice. Per-token revocation requires the issuer to maintain a {card, jti} mapping or equivalent linkage, which would defeat the subpoena-resistance property described in §10.8. Operational events that might suggest a need for revocation are handled differently:
exp date. To prevent further abuse, the holder cancels the underlying card, which prevents new tokens being issued on that card (the cancelled card cannot pay).jti values it has seen behaving abusively. This is RP-local and does not require coordination with the issuer.Implementers should understand that this design accepts a trade-off: stolen tokens remain usable until expiration. The trade-off is justified because per-token revocation would create the very {card, jti} linkage that blind issuance is designed to prevent.
Tokens MAY be single-use or multi-use depending on the relying party's requirements. For single-use:
jti set for the token's validity period.jti has been previously accepted.For multi-use:
jti to other RPs.Relying parties implementing OpenAgeProof will choose one of several verification patterns based on their architecture. The protocol does not mandate a pattern; this section describes the common choices and their trade-offs. The choice is invisible to the issuer — none of the patterns require coordination with the issuer or change what the issuer stores.
Stateless verification. The RP verifies the token on each presentation. No state is maintained between requests beyond the seen-jti set required for replay prevention (§5.4). Suitable for content sites without user accounts. The protocol provides no protection against token sharing in this mode; an RP concerned about sharing must implement its own controls (rate limiting, IP-based heuristics, behavioral analysis). Privacy against the RP is bounded by what the RP logs alongside the token presentation — IP addresses, browser fingerprints, and request patterns can produce correlation even without explicit account binding.
Per-session verification. The RP verifies the token at session start. The session itself maintains state (typically a server-side session record or a signed cookie). Tokens are not re-checked within a session but must be re-presented for new sessions. Suitable for sites with login flows but without persistent user accounts. Reduces token presentation frequency without requiring durable account binding.
Account-bound verification. The RP records the token's jti against a user account at first verification. Subsequent logins use account state, not the token. The RP enforces uniqueness on jti within its user base, preventing one token from being claimed by multiple accounts. The token's exp is checked periodically; expired tokens require a new token to be obtained from any compliant issuer. Suitable for platforms with persistent accounts where regulatory compliance requires the age assurance to be associated with the account.
The account-bound pattern is the most resistant to token sharing — once a jti is claimed by Account A, it cannot be claimed by Account B at the same RP. Sharing across different RPs is still possible (a token claimed at RP A can be claimed at RP B), but sharing within an RP is prevented.
The privacy properties differ across patterns:
Implementations MUST support Ed25519 for token signatures. Implementations MAY additionally support ECDSA P-256.
Issuers MUST publish their current signing public keys at a well-known URL:
<iss>/.well-known/openageproof-keys.json
Format:
{
"keys": [
{
"kid": "2026-01",
"alg": "Ed25519",
"public_key": "<base64url>",
"not_before": 1735689600,
"not_after": 1767225600
}
]
}
Issuers SHOULD rotate keys at least annually. Relying parties MUST honor not_before and not_after when verifying signatures.
The recommended blind signature scheme is the Privately Verifiable Token issuance from IETF RFC 9576 (Privacy Pass), which provides:
A concrete profile selection (specific PVT mode, key formats) is required for interoperability between independent implementations. This is noted in §13 as an open issue.
To be considered a compliant OpenAgeProof issuer, an implementation MUST:
Compliant issuers SHOULD:
End of specification draft 0.15.