Metadata-Version: 2.4
Name: zk-age-verifier
Version: 0.3.0
Summary: Verifier service for EU age-verification proofs (Longfellow ZK over mdoc, W3C Digital Credentials API)
Project-URL: Homepage, https://github.com/pipe23-org/zk-age-verifier
Project-URL: Documentation, https://zk-age-verifier.readthedocs.io/en/stable/
Project-URL: Repository, https://github.com/pipe23-org/zk-age-verifier
Project-URL: Issues, https://github.com/pipe23-org/zk-age-verifier/issues
Author: Nathan Schepers
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: age-verification,digital-credentials-api,eudi,longfellow,mdoc,zero-knowledge
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.11
Requires-Dist: cbor2>=6.1.3
Requires-Dist: cryptography>=46.0.7
Requires-Dist: fastapi
Requires-Dist: granian
Requires-Dist: pydantic-settings
Requires-Dist: pyhpke>=0.6.5
Requires-Dist: pylongfellow<0.6,>=0.5.0
Requires-Dist: structlog
Description-Content-Type: text/markdown

# 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](https://github.com/pipe23-org/zk-age-verifier/actions/workflows/ci.yml/badge.svg)](https://github.com/pipe23-org/zk-age-verifier/actions/workflows/ci.yml)
[![Docs](https://app.readthedocs.org/projects/zk-age-verifier/badge/?version=stable)](https://zk-age-verifier.readthedocs.io/en/stable/)
[![PyPI](https://img.shields.io/pypi/v/zk-age-verifier)](https://pypi.org/project/zk-age-verifier/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)

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

```toml
[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.
- `backend` (default `google-cpp`) — the pylongfellow verifier backend, `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.
