zk-age-verifier
zk-age-verifier is a verifier service for EU age verification, accepting Longfellow zero-knowledge proofs over mdoc through the W3C Digital Credentials API. It runs as a sidecar HTTP service beside a consumer backend. A verified verdict contains one boolean per requested check. The service has no authentication and is intended to be reachable only from the consumer backend, not the browser or internet. It is experimental and unstable.
Installation
pip install zk-age-verifier
uv add zk-age-verifier
docker pull ghcr.io/pipe23-org/zk-age-verifier:latest
Usage
The verifier needs a configured origin and at least one trust source.
[service]
expected_origin = "https://av.example"
[[trust.sources]]
pem = "/etc/zk-age-verifier/anchors"
python -m zk_age_verifier --config config.toml
POST /sessions opens a session and returns the navigator.credentials.get() argument
under transports.dc.
$ curl -X POST http://127.0.0.1:8000/sessions \
-H 'content-type: application/json' -d '{"checks": ["age_over_18"]}'
{"session_id": "tmcdOPmo7oCgd4AmmMFyYg",
"transports": {"dc": {"digital": {"requests": [{"protocol": "org-iso-mdoc",
"data": {"deviceRequest": "omd2ZXJzaW9u…", "encryptionInfo": "gmVkY2FwaaJ…"}}]},
"mediation": "required"}},
"expires_at": "2026-07-20T09:34:22.089682Z"}
The consumer backend relays the wallet response to POST /sessions/{session_id}/presentation. A
verified and a failed verification both return 200.
POST /sessions/{session_id}/presentation
{"response": "<wallet response, base64url>"}
{"state": "verified", "result": {"age_over_18": true}, "verified_at": "<iso8601>"}
{"state": "failed", "reason": "decrypt-failed"}
GET /health returns {"status": "ok"} while the process is up.
GET /debug/transcript/{session_id} returns the transcript inputs stored for a session — the
origin and the encryptionInfo string — with the handover hash and session-transcript bytes
reconstructed from them, hex-encoded. It is a development route, unauthenticated like the rest
of the service.
Configuration
Two TOML tables, [service] and [trust], passed with --config.
expected_origin(required) — the exactscheme://host[:port]origin the presentation asserts.pylongfellow.backend(defaultgoogle-cpp) — the proof engine,google-cpporisrg-rust; an unknown or unbuilt name fails startup.session_ttl_seconds(default 300) — session lifetime.session_cap(default 1000) — live-session limit;POST /sessionsreturns 503 at the cap.timestamp_skew_seconds(default 300) — proofs whose timestamp differs from the current time by more than this failstale-proof.cors_allowed_origins(default[]) — origins for which CORS headers are emitted.trust.sources(required) — non-empty list; each entry sets one ofpem(a PEM file or directory of issuer CA certs) oretsi_xml(an ETSI trusted-list https URL or file path).
A presented document-signer certificate must carry the keyUsage extension asserting digitalSignature; an anchor accepted as the issuer of a chained leaf must assert keyCertSign.
Configured trust sources merge into one anchor set. Any anchor in that set can vouch for a certificate that signs age credentials. The certificate layer carries no required marker restricting what an anchor's certificates may sign. ETSI TS 119 412-6 clause 6 places no type indicator on EAA signing certificates. The trust.sources list must name only anchors intended to vouch for age credentials. A mixed-purpose or broad list authorizes every CA on it as an age-credential issuer.
Environment variables ZK_AGE_VERIFIER_<SECTION>__<KEY> override scalar values; lists and
nested tables come from the TOML file only. Environment variables take precedence over the
TOML file, which takes precedence over the defaults.
Documentation
Full documentation: https://zk-age-verifier.readthedocs.io/
Development
uv sync
uv run pytest
uv run ruff check
uv run ruff format --check
uv run mypy
make test-live runs the suite against a running server over HTTP. make test-container
runs it against the built container image.
Status
You should not rely on this code.
- No rate limiting.
- The session store is in-process.
- The proof is verified against an empty device-namespace map; the ZK response format carries no field holding the map the wallet signed.
License
Apache-2.0.
Metadata
Release files for zk-age-verifier 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| zk_age_verifier-0.4.0.tar.gz | 1.1 MB | With provenance | Details for zk_age_verifier-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zk_age_verifier-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for zk_age_verifier-0.4.0-py3-none-any.whl |
Total release size: 1.4 MB
Release files / zk_age_verifier-0.4.0.tar.gz
| Download URL | zk_age_verifier-0.4.0.tar.gz |
|---|---|
| Size | 1.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3066716bfc7163cd3e41c803bf7f3de4d7474c555b60efc9a5cf912bc3b5c9b9
|
|
BLAKE2b-256 checksum How to use checksums |
da7595144f64500159410f722c0d763d72b8ec98eaf08792ce7151843f14e29b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logRelease files / zk_age_verifier-0.4.0-py3-none-any.whl
| Download URL | zk_age_verifier-0.4.0-py3-none-any.whl |
|---|---|
| Size | 337.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0ca6646ba4ff48b61ec20b23a81d8657bb134c1a45639f3969ede7c034fb59a5
|
|
BLAKE2b-256 checksum How to use checksums |
7195c9ead896aa4fc2508523c18ce85f2764802f4502e9c12fd8d09fffe21a7c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logSecurity for zk-age-verifier 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for zk_age_verifier-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.