Verifier service for EU age-verification proofs (Longfellow ZK over mdoc, W3C Digital Credentials API)
Project description
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 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"
First start generates the ZK circuit and caches it on disk.
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.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 with a timestamp older than this failstale-proof.cors_allowed_origins(default[]) — origins for which CORS headers are emitted.circuit_cache_dir(default$XDG_CACHE_HOME/zk-age-verifier/circuits) — where the generated circuit is cached.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 URL).
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
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.
- End-to-end testing covers Chrome on one Android 16 device.
- No rate limiting.
- The session store is in-process.
License
Apache-2.0.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zk_age_verifier-0.1.1.tar.gz.
File metadata
- Download URL: zk_age_verifier-0.1.1.tar.gz
- Upload date:
- Size: 522.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58a64285975a169c7556290127936fc6af3d5666ba86406230a0eb1b6bdbe6ab
|
|
| MD5 |
7706fc92e839e82b69ec06caf5a711eb
|
|
| BLAKE2b-256 |
14b2f172dfd57876ed01e2f3ad938b76b42a3f1c8b5224134becf9f3f74d42de
|
Provenance
The following attestation bundles were made for zk_age_verifier-0.1.1.tar.gz:
Publisher:
release.yml on pipe23-org/zk-age-verifier
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zk_age_verifier-0.1.1.tar.gz -
Subject digest:
58a64285975a169c7556290127936fc6af3d5666ba86406230a0eb1b6bdbe6ab - Sigstore transparency entry: 2225774428
- Sigstore integration time:
-
Permalink:
pipe23-org/zk-age-verifier@27b7dba53ea93e230b9e0123b800f0f1c4ccb443 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/pipe23-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@27b7dba53ea93e230b9e0123b800f0f1c4ccb443 -
Trigger Event:
push
-
Statement type:
File details
Details for the file zk_age_verifier-0.1.1-py3-none-any.whl.
File metadata
- Download URL: zk_age_verifier-0.1.1-py3-none-any.whl
- Upload date:
- Size: 37.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38cc119654f16aec268fba70a1ff36ab912ee0745bbd8c40d649b0a11fb17d0c
|
|
| MD5 |
a82aad2347f7060509f882f44a48d3f1
|
|
| BLAKE2b-256 |
1c2af27f6a8095faf0b88a6ce237ecef2901e38861052ce959d3c2bacefa27c7
|
Provenance
The following attestation bundles were made for zk_age_verifier-0.1.1-py3-none-any.whl:
Publisher:
release.yml on pipe23-org/zk-age-verifier
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zk_age_verifier-0.1.1-py3-none-any.whl -
Subject digest:
38cc119654f16aec268fba70a1ff36ab912ee0745bbd8c40d649b0a11fb17d0c - Sigstore transparency entry: 2225774714
- Sigstore integration time:
-
Permalink:
pipe23-org/zk-age-verifier@27b7dba53ea93e230b9e0123b800f0f1c4ccb443 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/pipe23-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@27b7dba53ea93e230b9e0123b800f0f1c4ccb443 -
Trigger Event:
push
-
Statement type: