Skip to main content

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.

CI Docs PyPI License: Apache 2.0

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 exact scheme://host[:port] origin the presentation asserts.
  • pylongfellow.backend (default google-cpp) — the proof engine, google-cpp or isrg-rust; an unknown or unbuilt name fails startup.
  • session_ttl_seconds (default 300) — session lifetime.
  • session_cap (default 1000) — live-session limit; POST /sessions returns 503 at the cap.
  • timestamp_skew_seconds (default 300) — proofs whose timestamp differs from the current time by more than this fail stale-proof.
  • cors_allowed_origins (default []) — origins for which CORS headers are emitted.
  • trust.sources (required) — non-empty list; each entry sets one of pem (a PEM file or directory of issuer CA certs) or etsi_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)

Source distribution for zk-age-verifier 0.4.0
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)

Table of built distributions (wheels) for zk-age-verifier 0.4.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

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

Select from 2 files

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 log

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

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page