WitnessOS-Passport
Verifiable identity and scoped authority for autonomous agents. Give an agent a passport, and anyone can check which key speaks for it and what it is allowed to do — offline, with no account, no API call, and no permission from us.
Verification is free and offline by design. Issuance is the product.
pip install witnessos-passport
The issuer signs; the agent's key is carried:
from witnessos_passport import issue_passport, verify_passport, authorise
# The issuer signs for an agent whose public key it has been given.
passport, _ = issue_passport(
subject="agent-01",
issuer="empire-labs",
subject_public_key=agent_public_key_hex, # the agent keeps its private key
issuer_seed=issuer_seed,
authority={"actions": ["gmail.send"], "resources": ["mailbox:ops"]},
)
verify_passport(passport).grade # E1 — signed, signer unidentified
verify_passport( # E2 — issuer bound out of band
passport, trusted_issuer_keys={"empire-labs": issuer_public_key_hex}
).identity_route # "issuer_attested"
authorise( # no trust policy -> refused
passport, action="gmail.send", resource="mailbox:ops"
).code # passport_invalid
Command line
witnessos-passport keygen --key-out issuer.key
witnessos-passport issue --subject agent-01 --issuer empire-labs \
--subject-key <agent-public-key-hex> \
--issuer-key issuer.key \
--actions gmail.send,http.get --resources 'mailbox:ops' \
--spend 25.00 --out passport.json
witnessos-passport verify passport.json --trusted empire-labs=<issuer-pubkey-hex>
witnessos-passport authorise passport.json --action gmail.send --resource mailbox:ops \
--trusted empire-labs=<issuer-pubkey-hex>
Presentation — the verifier asks, the agent answers, the verifier decides:
```bash
witnessos-passport challenge --action fs.write --resource workdir:/srv/notes \
--ttl 120 --out challenge.json
witnessos-passport prove --passport passport.json --key agent.key \
--challenge challenge.json --out presentation.json
witnessos-passport check --presentation presentation.json --challenge challenge.json \
--trusted-subject agent-01=<agent-pubkey-hex> \
--replay-store ~/.local/state/passport-replay.sqlite
witnessos-passport vectors --out vectors/passport-v2-vectors.json
An agent may instead issue for itself (--issuer equal to --subject, --key-out); a
verifier accepts that with --trusted-subject agent-01=<agent-pubkey-hex>, which is the
pinned_subject route.
Exit codes: 0 verified/allowed, 1 failed/denied, 2 usage error. verify performs no
network access and needs no configuration file. Add --json for machine-readable output —
accepted either before or after the command.
Issuing in an issuer's name without that issuer's key is refused rather than silently self-signed.
Two keys, two roles
| Field | Whose key | What it does |
|---|---|---|
issuer_key |
the issuer | signed this document |
subject.public_key |
the agent | is bound to the agent by that signature; signs the agent's own actions |
Both keys are inside the signed bytes, so editing which key speaks for the agent breaks the signature exactly as editing its authority does. The agent's private key never enters the issuance path when a third party issues.
Presenting a passport
Verification reads a document, and it is offline — so a copy of a passport answers it as well as the original. That is the point (the signed bytes are meant to be shared), but it also means verification alone cannot tell you who is acting. Presentation does: the agent proves it holds the private key its passport carries, over a challenge the verifier issued for that one action.
from witnessos_passport import issue_challenge, prove, check_presentation, SQLiteReplayGuard
challenge = issue_challenge(action="fs.write", resource="workdir:/srv/notes", ttl_seconds=120)
presentation = prove(passport, agent_seed, challenge) # the agent's side
check_presentation( # the verifier's side
presentation,
challenge=challenge, # the one it issued
replay_guard=FileReplayGuard("~/.local/state/passport-replay.json"),
trusted_issuer_keys={"empire-labs": issuer_public_key_hex},
require="E2",
).code # "allowed", or why not
A copy of someone else's passport is not a credential: the proof must be made by the key the
document carries, and the challenge dies with its window and with its nonce. No replay guard
is a refusal (challenge_replay_unverifiable), not an assumption — single use is a property
of the verifier's state, not of the signature. Not anchored, and not a timestamp authority: see
§8.5 of the format spec.
The same flow is on the command line, and check will not run without a replay store: it
refuses rather than assume a challenge is fresh. Two verifiers sharing one store cannot both
accept the same presentation — the claim is a single INSERT against a primary key, so the
store decides who wins, not the reader. That matters because a read-then-write store has a
window in which one claim erases another; FileReplayGuard is kept for exactly the case it
is documented for, one process at a time.
When the action produces data, naming the action and the resource is not enough — an
authorisation to write "the shortlist" is satisfied by any bytes at all. A challenge can bind
the exact bytes instead: payload={"target": "shortlist.json", "sha256": payload_digest(blob)},
where the target is relative to the resource the challenge names. It costs no new signature,
because the proof already covers the whole challenge; what it does require is a runtime that
recomputes the digest over the content it is about to deliver and refuses on mismatch — the
verifier never sees the bytes, and a gate that skips that step has a decorative binding. See
§8.6.
What a passport proves — and what it does not
A grade is a verifier conclusion, never a document's self-description. A passport carrying a
grade field is recorded and ignored.
| Grade | Name | Proven |
|---|---|---|
| E0 | declared | nothing independent — a claim, with integrity at most |
| E1 | observed | an independent party witnessed the key-to-subject binding, through a route outside the document |
| E2 | enforced | the gateway authorised and routed the action; the credential broker enforced policy — engine evidence: this SDK never emits it |
| E3 | corroborated | the destination provider confirmed the outcome — engine evidence: this SDK never emits it |
| E4 | anchored | inclusion in an anchored batch, verified offline against a trusted timestamp |
This is the WitnessOS ladder, and it is canonical on every surface — this repo included. WitnessOS
defined it first; this repo's earlier wording was the
newcomer and has been retired. Anchored is E4. Identity, self-signature and custody are
properties, not rungs: a key carried inside the document signing that document proves possession,
not who holds it; and an independent custodian holding a snapshot proves the record still exists,
which was never proof that it is more true. Custody is reported as an attribute (retained_by)
and never moves the grade.
Identity enters only at E1 (observed), and only through the verifier's out-of-band policy — never because the document claimed it. There are two routes to E1, and both are reported:
issuer_attested— the signing key is bound to an issuer the verifier trusts. This is the route that lets a third party vouch for an agent.pinned_subject— the document is self-issued and the verifier's policy binds that exact key to the document's own subject id. The label is checked, not just the key: a key pinned for one agent can never carry another agent's identity, and a labelless list of keys promotes nothing. Restricted to self-issued documents on purpose: otherwise an untrusted issuer could mint a wildcard grant naming a pinned agent and inherit its trust.
authorise requires E2 by default. A signature only proves someone signed; an
unidentified author's grant can be produced by anyone who can generate a keypair, so
honouring it by default would make every scope check decorative. Pass require="E1" to
accept unidentified documents deliberately.
The ladder is nested: E1 presupposes integrity, E4 the observed rung. A rung is never claimed on a foundation that did not hold, and unprovable components cap the grade; they never inflate it.
Status — read this before depending on it
Phase 1 (core primitive). Implemented and tested:
- Passport issuance: issuer-signed claims with the agent's key bound inside the signature.
- Offline verification with fail-closed grading and explicit identity routes.
- Scoped authorisation with deny-by-default semantics and machine-readable refusal codes.
- Holder-bound presentation: an agent proves possession of the key its passport carries, over a verifier-issued, windowed, single-use challenge — with a refused-by-default replay guard.
- Presentation on the command line end to end (
challenge→prove→check), so the control is reachable from the interface people actually install, not only from the SDK. - An atomic replay store (
SQLiteReplayGuard): a spent challenge is claimed byINSERTagainst a primary key, so concurrent verifiers serialise on the store instead of losing a write. - Byte-level payload binding: a challenge can name the exact content authorised for a target inside its resource, and the proof covers it without any additional signature.
- Anchored receipts (E4). Records are batched into an RFC 6962 Merkle tree and an external RFC 3161 timestamp authority signs a token over the whole receipt, not just the root. A verifier proves inclusion and validates the token against a trust anchor it supplies — no access to our storage, no trust in our clock, no trust in us.
- Python SDK and CLI.
- Binding suite (forgery, borrowed issuer names, trust laundering, key substitution) alongside the tamper suite, each with a positive control, plus published cross-language test vectors.
Retention is supplied, not proven. Retention by an independent custodian stays an attribute
supplied from outside — this code cannot witness somebody else's archive, and it does not pretend to
(§6, §11.5). It is carried and tested: the attribute is recorded, require_retention=True demands
it, and custody never moves the grade. What is missing is a custodian to make the claim, not the
code to carry it.
We are not that custodian — hold our own archive and independence is gone by construction. So custody is the customer's to hold, or a partner's, and ours to prove once they do. Who qualifies, what a receipt must bind and how a custodian is named: docs/CUSTODY.md. Recorded 2026-09-15 as a position, not a gap.
Also absent: a replay store that spans machines. The atomic claim shipped here covers processes that share a filesystem — one host, or hosts whose filesystem implements POSIX locking correctly (a local disk does; some network mounts quietly do not). A verifier fleet spread across machines wants a server-side store; the interface is one method, so that is a constructor argument, not a rewrite. Anchoring is only as good as the target you point it at: the SDK verifies whatever the verifier trusts, and ships an adapter for RFC 3161 only. Declared out of v1.0 — decided, not postponed. Hosted issuance is a service: tenancy, key custody, an uptime promise. No customer is asking for it yet, so it returns when one does (recorded 2026-09-15). Self-serve billing follows hosted issuance and returns with it — there is nothing priced to bill for until then.
Scheduled, Passport v1.1 (Interop): TypeScript parity — the format exists for agent runtimes and most agent code is JS/TS, and the cross-language vectors are already published, so it is bounded work rather than research. Same release, same phase: submission of anchored receipts to a transparency log.
Equivocation is not detected by this code. The chain catches gaps and splices. It cannot catch a sealer anchoring two contradictory batches — that needs third parties able to compare what was published, which is what a transparency log adds. "A third party stood next to this" is what ships; "third parties can catch a liar" is not, yet (§11.7).
Anchored is proven; retention is an attribute. E4 (anchored) is awarded when a verifier walks
the inclusion proof and accepts the token itself. Custody is supplied from outside and is reported
as an attribute, never as a grade: AnchorClaim(anchored=True) — a boolean handed in from outside —
earns a named cap and no grade at all.
Design rules
- Fail closed. Malformed input yields a refusal with a reason, never a lenient default.
- One clock. The verifier's clock decides validity.
issued_atin the verifier's future is refused, so forward-dating is detectable. - Deny by default. Empty scope grants nothing; wildcards must be explicit and may only be trailing.
- Exact money. Amounts are decimal strings parsed as
Decimal; nothing compares in binary floating point. - No hidden state. Every function is pure. The same question always gets the same answer.
- No keys in the library. Seeds are generated and returned; this project never persists, logs or transmits them.
Testing
python3 -m pytest -m "not network" # 336 tests
python3 -m pytest -m "network" # talks to FreeTSA; deselected in CI on purpose
python3 scripts/smoke_cli.py # end-to-end CLI walkthrough
The suite includes a positive control alongside every rejection case — a suite where everything fails would otherwise prove nothing.
tests/fixtures/tsa/ holds a real FreeTSA token (see its PROVENANCE.md) over a receipt
this repository can rebuild. The offline suite verifies that token against the vendored
certificate chain, so the anchoring cryptography is exercised on every run without depending on
a third party being up. The network tests submit a live query to FreeTSA; they are excluded
from CI because a public service must never be able to turn this repository red.
scripts/smoke_cli.py drives the installed CLI the way a user would — keygen, issue (both
routes), verify (untrusted, issuer-trusted, subject-pinned, over-required), authorise, tamper,
expiry — and asserts on the JSON payload as well as the exit code. A step that exits 0 while
reporting the wrong grade is a failure.
Interoperability
vectors/passport-v2-vectors.json is the contract for independent implementations: published
test seeds (never real key material) for both roles, two signed documents — one
issuer-attested, one self-issued — their expected signatures, canonical JSON examples with
SHA-256 digests, and expected verification and authorisation outcomes.
Specifications
docs/FORMAT.md— the normative format specification.
Licence
Apache-2.0. Verification is free and stays free.
Metadata
Release files for witnessos-passport 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| witnessos_passport-0.1.0.tar.gz | 97.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| witnessos_passport-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 158.3 kB
Release files / witnessos_passport-0.1.0.tar.gz
| Download URL | witnessos_passport-0.1.0.tar.gz |
|---|---|
| Size | 97.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
290f69aaf394d538897afda6a5a891a767fb972d35cf0945699ca7d413ef8303
|
|
BLAKE2b-256 checksum How to use checksums |
4f44da372637553e07219b328fc890602e9e6b94e02c0441d22970ed2ad45802
|
| 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 17, 2026.
Transparency logRelease files / witnessos_passport-0.1.0-py3-none-any.whl
| Download URL | witnessos_passport-0.1.0-py3-none-any.whl |
|---|---|
| Size | 61.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f38b1f6a24cd5c293547321b4fe7f12b1c393c117b5e579ee46bab9e91ae4f33
|
|
BLAKE2b-256 checksum How to use checksums |
3502ed46e9a220aa6f5da35933050c16bfa7c0300bb42d4df324a60ae4056fb9
|
| 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 17, 2026.
Transparency logSecurity for witnessos-passport 0.1.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.
Attestation for witnessos_passport-0.1.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 Sep 17, 2026.
Transparency logA 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.