Skip to main content

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 by INSERT against 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

  1. Fail closed. Malformed input yields a refusal with a reason, never a lenient default.
  2. One clock. The verifier's clock decides validity. issued_at in the verifier's future is refused, so forward-dating is detectable.
  3. Deny by default. Empty scope grants nothing; wildcards must be explicit and may only be trailing.
  4. Exact money. Amounts are decimal strings parsed as Decimal; nothing compares in binary floating point.
  5. No hidden state. Every function is pure. The same question always gets the same answer.
  6. 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

Licence

Apache-2.0. Verification is free and stays free.

Metadata

Release files for witnessos-passport 0.1.1

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

Source distribution (sdist)

Source distribution for witnessos-passport 0.1.1
File Size Uploaded Provenance
witnessos_passport-0.1.1.tar.gz 97.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for witnessos-passport 0.1.1
File Interpreter ABI Platform
witnessos_passport-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 158.5 kB

Release files / witnessos_passport-0.1.1.tar.gz

Download URL witnessos_passport-0.1.1.tar.gz
Size 97.5 kB
Tags Source
SHA-256 checksum
How to use checksums
fbd2c54de5662c8b5029b1a7027a711a654d2ac4bfa916e108c31e05faeae33d
BLAKE2b-256 checksum
How to use checksums
db000bfdb28b1c7ca29b416f04f7c2170ca096cef4e1bec6efef80b5656a52e2
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 log

Release files / witnessos_passport-0.1.1-py3-none-any.whl

Download URL witnessos_passport-0.1.1-py3-none-any.whl
Size 61.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fd57d1a2047650ff5cb9b0a55c5813ca35ff24b4a323acbb55722479ea6bac39
BLAKE2b-256 checksum
How to use checksums
419a7053a4a4d3e2fb3798f37c9d02bbb3af79d82f191f92594cef34ca80c95f
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 log

Security for witnessos-passport 0.1.1

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.

Select from 2 files

Attestation for witnessos_passport-0.1.1.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 log

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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

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