Skip to main content

ainra — Python verifier (independent implementation #4)

A verify-only, offline, fail-closed, zero-telemetry Python implementation of the AINRA agent-passport verifier. It is written independently from the AINRA specification (docs/AINRA_I_The_Standard.md, docs/AINRA_Master_Technical_Specification_v1.md) and the CC0 conformance vectors — not transliterated from the Rust core or the TypeScript SDK. It joins the conformance differential as a fourth column: core ↔ sdk ↔ P0 ↔ py, agreeing byte-for-byte on every vector's verdict and reason.

Why a fourth brain? If an independent reimplementation, built to the same spec + vector reference, reaches the same verdict on every vector, that is independent confirmation the standard is unambiguous and the reference is correct. If it disagreed, we would have found something. It agrees on 1153 passport + 17 delta + 9 directory vectors.

Package name

ainra — checked against PyPI on 2026-07-30 (GET /pypi/ainra/json → HTTP 404, i.e. the name is unregistered and available). It is not published to PyPI by this work; the name is reserved by intent and used for local installs only.

Install

pip install -e packages/sdk-py     # editable, from a checkout

Requires Python ≥ 3.10 and cryptography ≥ 44 (already present on most systems; it ships Ed25519 and ML-DSA-65). SLH-DSA-SHA2-128s, SHA-256, and zlib need no extra dependency (see below).

Quickstart

import json, base64
from ainra import Verifier

vec = json.load(open("vectors/v1/valid-0000.json"))

# 1) Build an offline verifier from the trust anchors.
verifier = Verifier(vec["anchors"])

# 2) Verify a bundle. The caller supplies `now`; there is no I/O.
verdict = verifier.verify(vec["presentation"], now=1500)
print("valid :", verdict.valid, "| reason:", verdict.reason)
print("event :", json.dumps(verdict.event()))

# 3) A revoked lineage fails closed with a named reason.
rvk = json.load(open("vectors/v1/revoked-0000.json"))
rv = Verifier(rvk["anchors"]).verify(rvk["presentation"], rvk["presentation"]["now"])
print("revoked ->", rv.valid, "| reason:", rv.reason)

# 4) The verifier owns the clock: forward-dating past `exp` cannot dodge expiry.
exp = json.loads(base64.urlsafe_b64decode(vec["presentation"]["claims"] + "=="))["exp"]
print("at exp ->", verifier.verify(vec["presentation"], exp).reason)

Real output (run from the repo root):

valid : True | reason: None
event : {"status": "valid", "reason": null, "name": "ainra:registrar-01:acme:invoicing@1.0.0", "number": "did:ainra:registrar-01:acme:invoicing", "tier": "L1", "freshness_age_s": 10}
revoked -> False | reason: revoked
at exp -> expired

The verdict event is the M16 shape every AINRA surface emits (docs/PRESENTATION.md): status, reason, name, number (the permanent version-less AINRA Number), tier, freshness_age_s.

ASGI middleware — gate a route, fail closed

Framework-agnostic (Starlette, FastAPI, Quart, any ASGI app). Every gated request must carry a valid passport or it is denied 403 + x-ainra-reason; on allow, the response carries the verdict event in x-ainra-verdict.

from ainra import Verifier, ainra_gate

verifier = Verifier.from_directory(directory, root_ed25519, root_slh)  # None if not authentic
app.add_middleware(ainra_gate(verifier))   # or: AinraGate(app, verifier)

The bundle is read from the x-ainra-passport header (base64url of canonical JSON, or raw JSON for local testing), falling back to the JSON body field ainra_passport. Verification never needs the body, so the header form is streaming-safe.

What it verifies

The frozen nine-step verify, first-failure-wins, mapping to the 20 frozen reasons (docs/reasons.json): AINRA name grammar; canonical JSON (sorted keys, no spaces, rejecting floats / non-ASCII keys / integers beyond 2⁵³); the strict base64url decode gateway (D-029 — every external decode is a canonical round-trip, fail closed); hybrid Ed25519 + ML-DSA-65 (both signatures or alg_downgrade/sig_invalid); the exact validity window (nbf inclusive, exp exclusive — no skew, no grace); scope-ceiling and delegation narrowing; revocation-status freshness classes (F1 ≤ 30 s · F2 ≤ 5 min · F3 ≤ 24 h, fail closed); logged-before-valid RFC 6962 inclusion to a signed checkpoint (root SLH-DSA-SHA2-128s or a scope-limited, in-window, unrevoked delegate); the signed status delta / fresh-head classes; and the dual-root-signed directory. Fail closed everywhere; .verify never raises.

Cryptography — shared primitives, independent logic (state precisely, D-041)

The verification logic is independent; the cryptographic primitives are shared, audited libraries — reimplementing a signature scheme would be less safe, not more independent:

Primitive Sizes (confirmed) Source
Ed25519 32 B key / 64 B sig cryptography (pyca, wraps OpenSSL 3.5+)
ML-DSA-65 (FIPS 204) 1952 B key / 3309 B sig cryptography (pyca / OpenSSL 3.5+)
SLH-DSA-SHA2-128s (FIPS 205) 32 B key / 7856 B sig OpenSSL libcrypto via ctypes (EVP raw-public-key verify) — that primitive only
SHA-256 (RFC 6962 prefixes) — Python stdlib hashlib

The claim, precisely: shared cryptographic primitives (pyca cryptography / OpenSSL 3.5+ for Ed25519 + ML-DSA-65; OpenSSL libcrypto via ctypes for SLH-DSA-SHA2-128s; stdlib for SHA-256), with an independent verification logic — the differential exercises the logic, not the primitives. cryptography 49 does not yet surface SLH-DSA, so it is reached from the same underlying OpenSSL directly; if the OpenSSL SLH-DSA verify path is unavailable, that primitive fails closed (returns False).

Differential — the fourth column

make diff (or node tools/diff-harness/run.mjs) runs the Python verifier over the whole corpus and asserts it agrees with the Rust core's recorded verdict on every vector, including the alg-downgrade-*, noncanon-*, boundary-*, renewal-* classes, plus delta and directory:

(A) verdict diff  core↔sdk : 1153/1153 agree
(B) canon 3-way  core↔sdk↔P0 : 10/10 byte-identical
(C) canon reject core↔sdk : 4/4 both refuse
(D) delta diff   core↔sdk : 17/17 agree
(E) directory diff core↔sdk : 9/9 agree
(F) verdict diff  core↔py : 1153/1153 agree
(F) delta diff   core↔py : 17/17 agree
(F) directory diff core↔py : 9/9 agree

DIFF OK: all implementations agree (core ↔ sdk ↔ P0 ↔ py)

Tests

cd packages/sdk-py && PYTHONPATH=. python3 -m unittest discover -s tests

Covers the whole-corpus agreement (all 20 reasons reachable), the ~5-line Verifier surface (valid / revoked / verifier-owns-the-clock), the ASGI gate (allow on VALID, deny 403 fail-closed on missing/revoked), and the strict base64url / canonical-JSON gateways. Zero telemetry, no network.

License

Apache-2.0 OR MIT (dual). The conformance vectors are CC0 (vectors/LICENSE).

Build it the documented way

v = Verifier.from_directory(directory, roots["root_ed25519"], roots["root_slh"],
                            audience="https://api.example", freshness="F2")

from_directory authenticates the directory against both ceremony roots and carries the registrar's status key, its status URI, and any graduated-distrust cutoff into the verifier — so revocations are authenticated (D-020) and a distrusted registrar is refused (D-044).

The raw Verifier(anchors, …) constructor exists for callers embedding anchors they already trust. It cannot authenticate revocations, because anchors supplied that way carry no status key. Use from_directory in production; the difference is recorded in docs/POLICY-PARITY.md.

Verify a running copy (ADR-019)

Same call; supply your audience when you build the verifier:

v = Verifier.from_directory(directory, roots["root_ed25519"], roots["root_slh"],
                            audience="https://api.example")   # never taken from the bundle
v.verify(bundle, now)     # the instance layer is checked automatically when present

The default empty audience is fail-closed — a service that has not said who it is accepts no instance credential.

Minting lives on the operator's side and takes a signing callback, so no key material enters this package: mint_instance_credential, prove_instance_possession.

Metadata

Release files for ainra 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 ainra 0.4.0
File Size Uploaded
ainra-0.4.0.tar.gz 42.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ainra 0.4.0
File Interpreter ABI Platform
ainra-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 80.6 kB

Release files / ainra-0.4.0.tar.gz

Download URL ainra-0.4.0.tar.gz
Size 42.1 kB
Tags Source
SHA-256 checksum
How to use checksums
74b2ffcba2d9b98d5c3e4f7d36e0d4f63912f92a5aedb88801fc046b5af665ee
BLAKE2b-256 checksum
How to use checksums
c68482b4c92e9b01f69a69173f39fc7cb81d05831cac40750217abcccbcebc71
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 15, 2026.

Transparency log

Release files / ainra-0.4.0-py3-none-any.whl

Download URL ainra-0.4.0-py3-none-any.whl
Size 38.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
185aba822885234f1c0699b05a14e631bf5eb9d1a08681afcdfad9af0bc51ae5
BLAKE2b-256 checksum
How to use checksums
176978c0f881db6b68057de004638ba2f9c54410d599416b042e8884e3f9d239
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 15, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.1

2 release files

This release

0.4.0 This release

2 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