Metadata-Version: 2.4
Name: vouchsafe
Version: 1.0.0
Summary: Vouchsafe - Self-verifying identity and offline trust verification
Author-email: jk0ne <jayk@ionzero.com>
License: BSD-3-Clause
Project-URL: Homepage, https://getvouchsafe.org/
Project-URL: Repository, https://github.com/ionzero/vouchsafe-py
Project-URL: Specification, https://github.com/ionzero/vouchsafe
Keywords: vouchsafe,identity,trust,jwt,jsonwebtoken,verification,security,vouchsafe,decentralized,decentralization,authentication,authorization,webhooks,webhook,zi-cg,zero-infrastructure,capability-graph,offline,offline-first,zicg,ed25519,attestation,delegation,revocation,pki,self-sovereign,capabilities,web-of-trust
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41
Requires-Dist: PyJWT>=2.8
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Dynamic: license-file

# Vouchsafe

[Vouchsafe](https://getvouchsafe.org/) is a JWT that proves who sent it,
without key distribution, registries, or a callback to an identity provider.

A normal JWT proves the claims weren't tampered with, but you still need a
separate, pre-shared way to know *whose* key signed it — an API key, a shared
secret, an OIDC handshake, something. A Vouchsafe token carries that proof
**inside itself**: the issuer's identity, their public key, and the signature
are all bound together in one package. If a Vouchsafe token validates, you
already know exactly who issued it, and that nothing has changed, with no
lookups and no infrastructure.

```python
trusted_issuers = {
    "urn:vouchsafe:alice.tp5y...vhsq": ["webhook:order_placed"],
}
```

Trust is configured locally and explicitly: you declare who you trust and
for what, instead of provisioning API keys or registering callback endpoints.

That alone covers most "is this JWT really from who it claims to be from"
use cases. But identity-proof is really just the foundation. Vouchsafe tokens
can also **vouch for each other**, forming chains of delegated trust: Alice
can vouch for Bob's claim, someone who trusts Alice can transitively trust
Bob through her, and any statement in the chain can later be revoked. That
turns Vouchsafe from "a JWT that verifies itself" into a small, portable
trust and authorization system: offline credentials, delegated permissions,
multi-party attestations, expressed entirely as data, with no contact with
a server or service required to evaluate it.

This package implements Vouchsafe for Python.

---

## Installation

```bash
pip install vouchsafe
```

The package is imported as `vouchsafe` and uses normal Python `snake_case`
names:

```python
from vouchsafe import (
    Identity,
    VOUCHSAFE_SPEC_VERSION,
    create_vouchsafe_identity,
    create_vouchsafe_identity_from_keypair,
    verify_urn_matches_key,
    create_jwt,
    verify_jwt,
    decode_jwt,
    get_app_claims,
    create_attestation,
    create_vouch_token,
    revoke_vouch_token,
    create_revoke_token,
    create_burn_token,
    validate_vouch_token,
    verify_vouch_token,
    hash_jwt,
    is_burn_token,
    is_revocation_token,
    decode_token,
    validate_trust_chain,
    verify_trust_chain,
)
```

---

## Quickstart (Identity Interface)

Most applications should start with `Identity`. It covers the common path:
create an identity, issue a token, and validate a trust chain.

### Example: Sending and Verifying a Webhook

The sender creates an attestation containing the webhook data:

```python
import json
from pathlib import Path

from vouchsafe import Identity

# Generate once with: create_vouchsafe_id.py --label alice -o alice.json
# Store identity JSON securely.
alice = Identity.from_data(json.loads(Path("alice.json").read_text()))

webhook_token = alice.attest({
    "purpose": "webhook:order_placed",
    "order_id": "12345",
    "amount": 4999,
})

# Send webhook_token in the body or header required by your application.
```

The receiver validates the token and evaluates local trust policy:

```python
from vouchsafe import get_app_claims, validate_trust_chain

trusted_issuers = {
    "urn:vouchsafe:alice.tp5y...vhsq": ["webhook:order_placed"],
}
subject_token = received_webhook_token

result = validate_trust_chain(
    [subject_token],
    subject_token,
    trusted_issuers,
    ["webhook:order_placed"],
)

if not result["valid"]:
    raise PermissionError("Untrusted webhook source")

subject = result["subjectToken"]
app_claims = get_app_claims(subject["decoded"])
print("Trusted webhook from:", subject["decoded"]["iss"])
print("Webhook data:", app_claims)
```

There is no required callback to the issuer.

---

## Token Delivery and Trust Material

Vouchsafe makes trust decisions using only the tokens provided and your local
configuration. No external service or state is required beyond the token set
being evaluated. This is what makes local, deterministic verification possible.
Vouchsafe validates cryptographic statements, not transport mechanisms: it does
not require any particular API, header format, database, message bus, or
identity provider to deliver tokens. You decide how to obtain tokens and which
trust material to keep available, based on what your application needs.

For trust-chain evaluation, provide the subject token, the related tokens you
have available, and your local trusted-issuer policy. That token set can be
assembled from more than one source:

* tokens presented by the actor in a request, webhook, or message;
* tokens bundled alongside the subject token;
* tokens your application has cached or synchronized previously;
* revocation and burn tokens from an application-managed revocation feed; and
* tokens supplied through an offline import, QR code, file, or any other
  application-specific channel.

For example, an application can merge caller-provided tokens with a local
revocation corpus before validation:

```python
from vouchsafe import validate_trust_chain

presented_tokens = request.tokens
revocation_tokens = load_local_revocation_tokens()
tokens = [*presented_tokens, *revocation_tokens]

result = validate_trust_chain(
    tokens,
    request.subject_token,
    trusted_issuers,
    required_purposes,
)
```

The delivery model does not dilute Vouchsafe's security properties. Every token
is still validated for its signature, issuer-to-key binding, token structure,
and role in the trust graph; local policy still decides which issuers and
purposes are trusted. Because Vouchsafe has no infrastructure dependency, the
application is free, in a way most authorization systems do not allow, to choose
the delivery model that fits its own constraints. A high-stakes financial system
might use high-availability infrastructure or a blockchain ledger, while a
simple chat app might rely on opportunistic sync over BLE (Bluetooth Low
Energy) or even printed QR codes. The choice is yours.

---

## What Vouchsafe Tokens Can Represent

Vouchsafe tokens are simple types that combine into richer trust relationships:

* **Attestations**: "I assert this fact."
  Example: Alice attests that an order was created or that she controls an email
  address.
* **Vouches**: "I vouch for someone else's token."
  Example: if you trust Alice for email confirmation, you can transitively
  trust Bob's email attestation through Alice's vouch.
* **Revocations**: "I withdraw a previous attestation or vouch I issued."
* **Burn tokens**: "I am permanently destroying this identity."
  A burn causes future trust evaluation to reject tokens from that issuer when
  the burn token is included in the evaluation set.

Each token is self-contained and cryptographically bound to its issuer. By
combining tokens, you can represent signed webhooks, delegated permissions,
revocable credentials, and offline-verifiable statements. You decide **who**
to trust through issuer URNs and **for what** through purposes; the evaluator
handles the graph traversal and purpose attenuation.

---

## Examples

### Example 1: Attestation

An attestation is the simplest Vouchsafe token: a signed statement whose issuer
and public key can be verified from the token itself.

```python
from vouchsafe import Identity, get_app_claims, validate_vouch_token

alice = Identity.create("alice")
email_attestation = alice.attest({
    "purpose": "email-confirmation",
    "email": "alice@example.com",
})

decoded = validate_vouch_token(email_attestation)
print(get_app_claims(decoded))
# {"email": "alice@example.com"}
```

### Example 2: Attestation, Vouch, and Trust Check

Bob attests to an email address, Alice vouches for that attestation, and a
verifier trusts Alice for `email-confirmation`.

```python
from vouchsafe import Identity, get_app_claims, validate_trust_chain, validate_vouch_token

bob = Identity.create("bob")
email_attestation = bob.attest({
    "purpose": "email-confirmation",
    "email": "bob@example.com",
})

alice = Identity.create("alice")
vouch = alice.vouch(email_attestation, {"purpose": "email-confirmation"})

trusted_issuers = {alice.urn: ["email-confirmation"]}
print(get_app_claims(validate_vouch_token(email_attestation)))

result = validate_trust_chain(
    [email_attestation, vouch],
    email_attestation,
    trusted_issuers,
    ["email-confirmation"],
)
assert result["valid"]
print("Email is trusted via:", result["trustRoot"])
```

### Example 3: Revoking a Vouch

When Alice revokes her vouch, including that revocation in the token set causes
the trust chain to fail.

```python
from vouchsafe import Identity, validate_trust_chain

bob = Identity.create("bob")
alice = Identity.create("alice")
email_attestation = bob.attest({"purpose": "email-confirmation"})
vouch = alice.vouch(email_attestation, {"purpose": "email-confirmation"})
trusted_issuers = {alice.urn: ["email-confirmation"]}

assert validate_trust_chain(
    [email_attestation, vouch], email_attestation, trusted_issuers,
    ["email-confirmation"],
)["valid"]

revoke = alice.revoke(vouch)
assert not validate_trust_chain(
    [email_attestation, vouch, revoke], email_attestation, trusted_issuers,
    ["email-confirmation"],
)["valid"]
```

### Example 4: Delegation with Constraints

Vouch tokens may carry application claims that constrain how a delegated
permission is used. Vouchsafe verifies the identity, delegation, purpose, and
revocation status; your application enforces its own domain constraints.

```python
import time

from vouchsafe import Identity, get_app_claims, validate_trust_chain

alice = Identity.create("alice")
bob = Identity.create("bob")
now = int(time.time())

upload_request = bob.attest({
    "purpose": "file:write",
    "filename": "report.pdf",
    "size": 3 * 1024 * 1024,
    "exp": now + 600,
})
constrained_vouch = alice.vouch(upload_request, {
    "purpose": "file:write",
    "max_uses": 1,
    "max_size": 5 * 1024 * 1024,
    "exp": now + 300,
})

result = validate_trust_chain(
    [upload_request, constrained_vouch],
    upload_request,
    {alice.urn: ["file:write"]},
    ["file:write"],
)
if not result["valid"]:
    raise PermissionError("Upload not authorized")

claims = get_app_claims(result["subjectToken"]["decoded"])
if claims["size"] > 5 * 1024 * 1024:
    raise ValueError("Upload exceeds the application limit")
```

---

## Validation API

Vouchsafe has two validation layers:

1. **Token-level validation** asks whether a token is a properly formed,
   correctly signed Vouchsafe token.
2. **Trust-chain validation** asks whether that subject token is trusted for
   required purposes by one of your configured trust roots.

### Token-level validation

Use `validate_vouch_token` when you need a verified Vouchsafe token:

```python
from vouchsafe import get_app_claims, validate_vouch_token

decoded = validate_vouch_token(compact_jwt)
# Success means the token has valid structure, signature, URN/key binding,
# timestamps, and token-kind-specific fields.
app_claims = get_app_claims(decoded)
```

Use `verify_vouch_token(vouch_jwt, subject_jwt)` to confirm a vouch references
the intended subject. `decode_token(raw_token)` returns a trust-evaluation
object containing the raw token, decoded claims, and compact-JWT hash.

### Trust-chain validation

```python
result = validate_trust_chain(
    tokens,
    subject_token,
    trusted_issuers,
    required_purposes=None,
    options=None,
)
```

* `tokens`: all available tokens, including the subject. Combine
  actor-provided tokens with locally managed revocation or burn material here.
* `subject_token`: the token being evaluated for trust.
* `trusted_issuers`: a mapping of issuer URNs to permitted purposes.
* `required_purposes`: purposes required by this operation. Omit it or pass an
  empty list when any surviving purpose is acceptable.
* `options["max_depth"]`: optional maximum number of vouch hops.
* `options["return_all_valid_chains"]`: return every valid chain rather than
  stopping at the first match.
* `options["strict"]`: raise `TrustChainValidationError` for invalid ancillary
  token material. By default, invalid ancillary tokens are ignored and reported
  in `validation_errors`.

On success, the result includes `valid`, `subjectToken`, `trustRoot`, `chains`,
`effectivePurposes`, and `validation_errors`. Each item in `chains` contains the ordered `chain`,
the surviving `purposes`, and the `trustRoot` URN. On failure, `valid` is false
and the chain list is empty.

`validate_trust_chain` raises `TrustChainValidationError` when the subject token
itself cannot be evaluated safely, or when the token bundle contains conflicting
tokens with the same issuer and JTI.

The evaluator cleans the graph before traversal: it decodes and validates
tokens, deduplicates them, processes revocations and burns, and only then
evaluates delegation and purpose attenuation. `verify_trust_chain` is the
legacy compatibility wrapper; use `validate_trust_chain` for new code.

---

## CLI Tools

The Python package installs shell tools for identities and tokens. Their `.py`
suffix is intentional: it prevents collisions with identically purposed
commands installed by the Node package.

* `create_vouchsafe_id.py`: generate a Vouchsafe identity and keypair.
* `create_vouchsafe_token.py`: mint attestations, vouches, and revocations.
* `verify_vouchsafe_token.py`: validate tokens and evaluate trust chains.

### `create_vouchsafe_id.py`

```bash
create_vouchsafe_id.py --label alice -o alice.json
```

The resulting JSON contains the identity URN, base64-encoded Ed25519 keypair,
public-key hash, and specification version. Use `--separate` to write a `.urn`,
`.public.pem`, and `.private.pem` file instead.

`--existing` reuses the keypair from an existing identity JSON file and derives a
new identity document for the label you provide. The command refuses to
overwrite the original identity file in place.

```text
Usage: create_vouchsafe_id.py [-h] -l LABEL [-s] [-q] [-e EXISTING] [-o OUTPUT]
                              [--public PUBLIC_FILE] [--private PRIVATE_FILE]

  -l, --label LABEL        Identity label (required)
  -s, --separate           Output URN and PEM files separately
  -q, --quiet              Suppress status output
  -e, --existing FILE      Load an existing identity JSON file
  -o, --output FILE        Output filename or prefix
  --public FILE            Existing public key PEM file
  --private FILE           Existing private key PEM file
```

### `create_vouchsafe_token.py`

```bash
# Attestation with a purpose
create_vouchsafe_token.py -i alice.json -p msg-signing > token.jwt

# Vouch for an existing token
create_vouchsafe_token.py -i alice.json --vouch -t subject.jwt \
    -p email-confirmation -o vouch.jwt

# Revoke a previous attestation or vouch
create_vouchsafe_token.py -i alice.json --revoke -t vouch.jwt -o revoke.jwt
```

```text
Usage: create_vouchsafe_token.py [-h] -i IDENTITY [-f CLAIMS] [-c KEY=VALUE]
                                 [-p PURPOSE] [-e EXPIRES] [-o OUTPUT]
                                 [-t TOKEN_FILE] [-T TOKEN]
                                 [--attest | --vouch | --revoke]

  -i, --identity FILE      Identity JSON file (required)
  -f, --claims FILE        Claims JSON object
  -c, --claim KEY=VALUE    Additional claim (repeatable; JSON values accepted)
  -p, --purpose PURPOSE    Purpose (repeatable; attest/vouch)
  -e, --expires SECONDS    Expiration (default 86400; 0 disables expiration)
  -q, --quiet              Suppress warnings and status output
  -v, --verbose            Emit additional status output
  -t, --token-file FILE    Subject token for vouch/revoke
  -T, --token TOKEN        Subject token string for vouch/revoke
```

The CLI intentionally creates attestations, vouches, and revocations. Use
`create_burn_token` from the functional API to issue a burn token.

### `verify_vouchsafe_token.py`

```bash
# Validate token structure, signature, and URN binding
verify_vouchsafe_token.py -t token.jwt

# Evaluate a chain with a local trust policy and emit shell variables
verify_vouchsafe_token.py -E -p email-confirmation \
    --trusted trusted.json -t chain.txt -O unix
```

```text
Usage: verify_vouchsafe_token.py [-h] [-t TOKEN_FILE] [-T TOKEN]
                                 [-O {json,unix}] [-f FIELD] [-E]
                                 [-P PREFIX] [--trusted FILE]
                                 [--trusted-issuer URN=purpose[,purpose...]]
                                 [-p PURPOSE]

  -t, --token-file FILE    File with one or more tokens (first is subject)
  -T, --token TOKEN        Token string (first is subject)
  -O, --output FORMAT      json or unix
  -f, --field DOTPATH      Output only this field (repeatable)
  -E, --extended           Require trust for the supplied purpose(s)
  -q, --quiet              Suppress warnings and status output
  -v, --verbose            Emit additional status output
  -p, --purpose PURPOSE    Required purpose (repeatable)
  --trusted FILE           Trusted issuers JSON or text file
  --trusted-issuer VALUE   Inline URN=purpose[,purpose...] entry
```

Trusted issuers JSON:

```json
{
  "urn:vouchsafe:alice...": ["email-confirmation", "webhook:order_placed"],
  "urn:vouchsafe:bob...": ["email-confirmation"]
}
```

The text form has one `URN purpose1 purpose2` entry per line. Token files may
contain one token per line, or a single whitespace-delimited line. In either
case, the first token is the subject and the rest are additional trust material.

---

## Identity Class API (High-level)

`Identity` is the recommended entry point unless you need low-level control.

* `Identity.create(label)`: generate a new identity with URN and keypair.
* `Identity.from_data({"urn": urn, "keypair": keypair}, verify=True)`: rehydrate
  stored identity material and verify the URN/key binding by default.
* `Identity.from_keypair(label, keypair)`: build an identity from an existing
  keypair.
* `identity.urn`: the self-verifying issuer URN; safe to share.
* `identity.attest(claims=None)`: issue an attestation.
* `identity.vouch(subject_token, options=None)`: issue a vouch.
* `identity.revoke(token, options=None)`: revoke an attestation or vouch.
* `identity.verify(token)`: validate one Vouchsafe token.
* `identity.to_dict()`: export JSON-ready identity data for storage, including
  `urn`, `keypair`, `publicKeyHash`, and `version`.

---

## Functional API (Low-level)

Use these building blocks when the `Identity` facade is not the right fit.

### Identity helpers

* `create_vouchsafe_identity(label)`: generate `{ "urn", "keypair", ... }`.
* `create_vouchsafe_identity_from_keypair(label, keypair)`: derive an identity
  from an existing base64-encoded Ed25519 keypair.
* `verify_urn_matches_key(urn, public_key)`: return whether the URN matches the
  public key.

### JWT helpers

* `create_jwt(issuer, issuer_key, private_key, claims=None, options=None, *,
  exclude_iss_key=False)`: create an EdDSA JWT. Set `exclude_iss_key` only for
  non-Vouchsafe compatibility cases.
* `verify_jwt(token, *, public_key_override=None, verify_issuer_key=True)`:
  verify a JWT signature and, by default, the issuer-key relationship.
* `decode_jwt(token, *, full=False)`: decode without signature verification.
  With `full=True`, return header and payload.
* `get_app_claims(decoded_token)`: remove JWT and Vouchsafe housekeeping claims.

### Token creation

* `create_attestation(issuer, issuer_keypair, claims=None)`
* `create_vouch_token(subject_jwt, issuer, issuer_keypair, claims=None)`
* `revoke_vouch_token(token, issuer_keypair, claims=None)`
* `create_revoke_token(claims, issuer, issuer_keypair)`
* `create_burn_token(issuer, issuer_keypair, claims=None)`
* `hash_jwt(token)`: calculate the compact JWT hash used for linkage and
  revocation.

### Token validation and inspection

* `validate_vouch_token(token)`: full Vouchsafe structure, URN-binding, and
  signature validation.
* `verify_vouch_token(vouch_jwt, subject_jwt)`: verify a vouch's linkage to its
  subject token.
* `is_revocation_token(decoded_token)` / `is_burn_token(decoded_token)`: classify
  a decoded token and raise if the matching token is malformed.
* `decode_token(raw_token)`: create the decoded trust-evaluation token object.

### Trust-chain evaluation

* `validate_trust_chain(tokens, start_token, trusted_issuers, purposes=None,
  options=None)`: validate delegation, revocations, burns, and purpose
  attenuation.
* `verify_trust_chain(subject_token, trusted_issuers, options=None)`: legacy
  compatibility wrapper. Prefer `validate_trust_chain` in new code.

---

## Interoperability Test Corpus

The repository includes helper scripts under `tests/helpers/` for cross-language
interoperability testing. They are used to prove two directions:

* Python can generate Vouchsafe identities and tokens that another
  implementation, such as JavaScript, validates correctly.
* Python can validate a corpus generated by another implementation, as long as
  that corpus follows the same manifest format.

The corpus is file-based so it can move cleanly between repositories and CI
jobs. A generated corpus contains:

* `manifest.json`: the test plan and expected outcomes;
* `identities/`: exported identity JSON files;
* `tokens/`: individual attestation, vouch, revocation, burn, and negative-test
  JWTs;
* `bundles/`: newline-delimited token sets for trust-chain tests; and
* `trusted/`: trusted-issuer policy JSON files.

### Generate a Python-produced corpus

```bash
python3 tests/helpers/generate_interop_assets.py
```

The script prints the output directory path. Pass a directory argument to choose
where the corpus is written:

```bash
python3 tests/helpers/generate_interop_assets.py /tmp/vouchsafe-interop-py
```

The current Python corpus includes cases for:

* identity round-tripping;
* valid attestation and vouch verification;
* valid multi-hop trust chains;
* rejection after revocation;
* rejection after an identity burn;
* rejection of a tampered `vch_sum`;
* purpose attenuation across delegated vouches;
* rejection of purpose expansion;
* rejection of malformed revocation tokens; and
* rejection of non-Vouchsafe external JWT subjects.

The generated `manifest.json` includes a `producer` field. Python writes
`"producer": "py"`. Another implementation can write its own producer label and
reuse the same asset layout.

### Validate a corpus with Python

```bash
python3 tests/helpers/validate_interop_assets.py /tmp/vouchsafe-interop-py
```

The validator reads `manifest.json`, resolves the referenced assets, and runs
the appropriate Python library checks for each case type. By default it prints a
JSON summary and exits non-zero if any case fails.

Optional flags:

* `--tap`: continue through all cases and emit TAP output instead of stopping at
  the first failure.
* `--skip-unknown-types`: skip manifest case types that this Python validator
  does not implement yet.

Example:

```bash
python3 tests/helpers/validate_interop_assets.py /tmp/vouchsafe-interop-js \
    --skip-unknown-types --tap
```

This is the intended cross-language workflow:

1. Generate a corpus in Python and validate it in JavaScript.
2. Generate a corpus in JavaScript and validate it with
   `tests/helpers/validate_interop_assets.py`.
3. Keep both implementations aligned by adding new manifest case types only when
   the other language can either validate them or explicitly skip them.

The automated Python test `tests/test_interop.py` covers the local half of this
workflow by generating a Python corpus and validating it with the Python
validator.

---

## Learn More

* [getvouchsafe.org](https://getvouchsafe.org): conceptual overview and use cases.
* [Vouchsafe Specification](https://github.com/ionzero/vouchsafe): token format,
  URN rules, and trust-chain semantics.
* ["Vouchsafe: A Zero-Infrastructure Capability Graph Model for Offline Identity and Trust"](https://arxiv.org/abs/2601.02254) - The formal model and security analysis behind Vouchsafe

Vouchsafe is designed to be self-contained, zero-infrastructure, and
human-scale: identity you can prove, trust you can carry.

---

## License

BSD 3-Clause License
