Skip to main content

registry-evidence-client-py

Python binding for registry-evidence-client, the Evidence relying-party client, via PyO3. Every Evidence semantic decision (request preparation, sending, and verification) is the wrapped Rust crate's own; this crate is a thin #[pymodule] surface plus a JSON conversion layer, and re-implements none of it.

The package is named registry-evidence-client and imported as registry_evidence_client (matching the crate's own [lib] name). Starting with Registry Stack v0.22.0, install the exact client version that matches the Evidence deployment:

python -m pip install "registry-evidence-client==<version>"

PyPI carries manylinux wheels requiring glibc 2.17 or newer for Linux amd64 and Linux arm64, plus a macOS arm64 wheel. Earlier versions remain available as wheels attached to their GitHub Releases.

Python surface

Every method below is an ordinary blocking def, never async def. The client owns a private current-thread tokio runtime and blocks on it for every network call, releasing the GIL for the duration (via py.detach) so other Python threads keep running.

from registry_evidence_client import EvidenceClient

client = EvidenceClient(base_url, trusted_jwks, revoked_key_ids, token)

spec = {
    "response_format": "signed-jws",
    # requirement, subjects, and the remaining trusted procedure inputs
}
prepared = client.prepare(spec)              # synchronous, no I/O
definitions = client.discover()
jwks = client.fetch_jwks()
response = client.send(prepared)
verified = client.verify(prepared, response)
verified = client.request_and_verify(prepared)
verified = client.verify_as_of(prepared, response, as_of_unix_seconds)

batch_spec = {
    # requirement, purpose, and shared verification expectations
    "items": [
        {"subjects": subjects_a, "subject_expectations": "accept_first_use"},
        {"subjects": subjects_b, "subject_expectations": pinned_subjects_b},
    ],
}
prepared_batch = client.prepare_batch(batch_spec)  # synchronous, no I/O
raw_batch = client.send_batch(prepared_batch)
verified_batch = client.verify_batch(prepared_batch, raw_batch)
# Or: client.request_and_verify_batch(prepared_batch)

Request-batch results retain request order. Each item is either {"status": "available", "verified": VerifiedEvidence} or exactly {"status": "not_available"}. Rust verifies every available member against the policy and nonce at the same position before the binding returns any item. The prepared and raw batch classes have no public constructors, and a prepared batch can be sent only once.

response_format is required on every request specification. Use "signed-jws" for a flattened JWS JSON response or "sd-jwt-vc" for the keyless SD-JWT VC response. prepare() closes that choice before any I/O, send() uses its corresponding HTTP Accept value, and verification never guesses a format from the returned bytes. The shipped type stub exposes EvidenceResponseFormat and EvidenceRequestSpec for these inputs.

token is either a bare string (a static token) or a mapping with exactly one key, "private_key_jwt". There is no caller-supplied token provider; that is out of scope for this binding, same as the Node binding.

Holder-bound issuance is supported: prepare() accepts public holder_keys, and SdJwtVcBatchResponse parses the ordered credential envelope returned for them. The binding stops at issuance. It exposes no trace_id for a holder to create a selective-disclosure presentation or key-binding proof, and no relying-party trace_id to verify that presentation. Use the registry-evidence-verifier Rust crate or evidence verify-presentation for the verification half of that workflow.

trusted_jwks and revoked_key_ids are both required trust inputs. revoked_key_ids contains current service-key RFC 7638 thumbprints and overrides a matching key even when it remains in trusted_jwks or in an older prepared request's policy.

Design notes

Error mapping

A mapped failure surfaces to a caller as an EvidenceClientError, exported from the package root, with one subclass per stable kind: ConfigurationError, NonceError, TokenError, TransportError, DeniedError, NotAvailableError, ProtocolError, VerificationError. Every instance carries kind; status, code, trace_id, retry_after_seconds, transport_kind (on a TransportError, and on a TokenError whose token_kind is "transport"), and token_kind (only on a TokenError) are set as attributes only when the underlying failure carries them. str(error) is human prose, not JSON: read it, do not parse it.

The denied/protocol split is a hazard worth calling out explicitly: HTTP 401, 403, and 429 all map to denied regardless of the response body's own code, while every other non-2xx status (including 400, 500, and anything else not specifically recognized) maps to protocol. A caller that only checks status without also checking kind can misclassify a 429 rate limit as a generic protocol failure, or vice versa. See registry-evidence-client's problem.rs for the authoritative mapping table.

A response that exceeds its size bound maps to kind: "transport" with transport_kind: "response_too_large", not kind: "protocol", even when the response status itself was a plain 200: the size limit is enforced against the transport, before any attempt to interpret the body as a problem response.

Which bound applies depends on the call. max_response_bytes bounds the signed response body that send() and request_and_verify() read, and its default follows what the verifier will accept as a signed response. max_metadata_bytes bounds the documents discover() and fetch_jwks() read, neither of which is signed or verified. Tightening one does not tighten the other.

No string reaching Python carries unbounded remote text; no exception carries response bytes, a credential, a header value, a selector value, or a subject binding.

Panics

PyO3 already catches panics that cross the FFI boundary: every generated trampoline (behind #[pymethods], #[pyfunction], and #[pymodule]) wraps the call in std::panic::catch_unwind and translates a caught panic into PyO3's own PanicException, before this crate adds anything of its own. See pyo3-0.29.1/src/impl_/trampoline.rs (the trampoline function) in the vendored source for the exact mechanism. The client's own request nonce is generated through getrandom::fill, which reports entropy failure as an ordinary error and cannot panic. Aside from to_py_err's set_attr! calls, which can only panic under allocation failure, the one latent panic path is upstream and left unguarded on purpose: the private-key-JWT token provider's jti claim, generated with Ulid::new(), reaches rand::rng() and panics if OS entropy is unavailable. It is reachable only in a deployment configured for private-key-JWT; short of that same allocation-failure-only path, a static-authorization deployment has no reachable panic at all. This crate adds no guard of its own for it: the trampoline above already turns any such panic into an ordinary Python exception rather than a process abort.

The unsafe_code lint

Unlike the Node binding (registry-evidence-client-node, which opts out of the workspace's unsafe_code = "forbid" because napi-rs's generated glue contains unsafe code attributed to the invoking crate), this crate inherits [lints] workspace = true unmodified. Confirmed by building -p registry-evidence-client-py both without and with the extension-module feature: PyO3's proc macros do not expand to unsafe code inside this crate; the actual FFI calls live inside pyo3/pyo3-ffi's own compiled sources, under their own crate's lint settings, not this one's.

Nonce and the golden fixture

prepare() generates a fresh request nonce on every call; there is no seam to inject a fixed nonce from outside. tests/golden_fixture.rs and its committed fixtures under tests/fixtures/ exist because of this: they pin one specific, already-issued signed response (with its own fixed nonce baked in) so the conversion layer's verification path can be exercised deterministically without needing a live signer. Regenerate the fixture only with:

cargo test -p registry-evidence-client-py --test golden_fixture -- --ignored regenerate_golden_fixture

never by hand-editing the fixture files. The Python test suite deliberately has no signed-response round trip of its own: EvidenceClient::send() never parses or verifies the response body (only verify()/verify_as_of() do), so every Python-level error and construction test can use fake or unsigned response bytes. The one genuine signed round trip against the real compiled Python surface lives in tests/happy_path.rs, which drives it directly through PyO3 rather than through a second, separately-maintained signer.

verify_as_of

verify_as_of(prepared, response, as_of_unix_seconds) judges a response as of an explicit instant rather than the live clock. A past instant is the direction that costs something: naming a stale instant accepts an assertion whose validity interval has since elapsed, because the question asked is whether it was acceptable then, and the answer stays yes forever. A live trust decision should call verify, not this.

Building

Building requires python3 on PATH at build time (PyO3's build script locates the interpreter to configure the target ABI); this is true for both cargo build and maturin.

For local development, install the package into a virtualenv with:

uv run maturin develop

The auto-initialize dev-dependency feature (see Cargo.toml) links every test binary this crate produces directly against libpython, so the dynamic linker has to find it at process startup. build.rs records the build-time interpreter's own library directory as an rpath for that build, which is why cargo test -p registry-evidence-client-py needs no DYLD_LIBRARY_PATH (macOS) or LD_LIBRARY_PATH (Linux) of its own, even for an interpreter outside the linker's default search path. Rebuilding after the interpreter moves is enough to follow it; the rpath is a build-time constant, not a lookup. The shipped wheel never carries that rpath: maturin always builds through this crate's extension-module feature, where libpython is left to the loading process.

Testing

Rust unit tests (cargo test -p registry-evidence-client-py) cover the conversion layer directly, plus a golden fixture and a live happy-path/ request-batch/nonce-mismatch/one-send-guard round trip against the real compiled Python surface (tests/happy_path.rs), driven straight through PyO3 without shelling out to a python3 process.

The Python suite under tests/python/ (python3 -m unittest discover -s crates/registry-evidence-client-py/tests/python) covers construction refusals, error mapping for denied/not-available/protocol/transport failures, discovery (discover/fetch_jwks) against a stub server, singular and batch one-send guards, exact batch wire and malformed-member handling, a GIL-release concurrency proof, and a stub-drift check against the committed .pyi. Every file in that suite imports tests/python/bootstrap.py first, which runs:

cargo build --locked -p registry-evidence-client-py --lib --features registry-evidence-client-py/extension-module

then copies the resulting dylib to a scratch directory as registry_evidence_client.so and puts that directory on sys.path, so the suite never depends on maturin, pip install -e, or any packaging step.

No test path invokes maturin, on a developer's machine or in CI. Two things do: uv run maturin develop above, for local development, and .github/workflows/evidence-dev.yml, which builds the wheel published with each Evidence development prerelease and smokes it from a fresh virtual environment. That workflow stamps the prerelease version into pyproject.toml before building, so this crate's own copy of the workspace version stays the released one.

Release files for registry-evidence-client 0.26.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for registry-evidence-client 0.26.0
File Interpreter ABI Platform
registry_evidence_client-0.26.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
registry_evidence_client-0.26.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
registry_evidence_client-0.26.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details

Total release size: 12.8 MB

Release files / registry_evidence_client-0.26.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL registry_evidence_client-0.26.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 4.5 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
373e6d5b3daf5e7769b44ae57e5f4f40d717e43fdfb4fcba4d08b8e044b1cf96
BLAKE2b-256 checksum
How to use checksums
5faf7b158b1be154725587e62c2a1eed8dfe80c0e435c1ec8430971755afa958
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 Sep 4, 2026.

Transparency log

Release files / registry_evidence_client-0.26.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL registry_evidence_client-0.26.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 4.2 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
4548aa0911c16de12bd4bd316e7c3dda0574d8cd2ede0fc6a4a5cff7f8ddec68
BLAKE2b-256 checksum
How to use checksums
32e80141b99da9da42f9132a4c6fdd28eee01600878ea3ce20807bab46c70567
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 Sep 4, 2026.

Transparency log

Release files / registry_evidence_client-0.26.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL registry_evidence_client-0.26.0-cp310-abi3-macosx_11_0_arm64.whl
Size 4.1 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
4f173ca3f5d885e4371666c80f08b4a00a29c18dd2d8812d8c3ff4dfbb887464
BLAKE2b-256 checksum
How to use checksums
eb0d11f8457a4e86f2d258f4ea30f28690c4311ae8042e12abe092ec070d5f8f
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 Sep 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.26.0 This release

3 release files

0.25.0

3 release files

0.24.0

3 release files

0.23.0

3 release files

0.22.0

3 release files

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