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.23.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| registry_evidence_client-0.23.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.23.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| registry_evidence_client-0.23.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.23.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | registry_evidence_client-0.23.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 |
38f776119763869470364b1cd33a0d4c122892adb4b703414d96a1c5be040f88
|
|
BLAKE2b-256 checksum How to use checksums |
809fc2592efea5323141f4f1b273c9a98f184b7f07aaea6669308d5ac5ac9e36
|
| 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 20, 2026.
Transparency logRelease files / registry_evidence_client-0.23.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | registry_evidence_client-0.23.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 |
ea872de6c53d6991a9239b1d19e6625a55f5de5457da21c6f322ea5217e0e6d3
|
|
BLAKE2b-256 checksum How to use checksums |
3d39f6c23fd2bdae037a81d9eeade99a5fe486c5c0ecb9b7b8883b5819890ca8
|
| 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 20, 2026.
Transparency logRelease files / registry_evidence_client-0.23.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | registry_evidence_client-0.23.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 |
c64e7ad2deeed640cb54f2392ff8cefe1483def2a7fe2e70b4c4ac7380ed34bb
|
|
BLAKE2b-256 checksum How to use checksums |
31ede9c8d3e05dce5d48f3e73e1139447307b47b66f0f2861ac16eccd1fc839f
|
| 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 20, 2026.
Transparency log