Skip to main content

Vouchsafe

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

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

pip install vouchsafe

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

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:

import json
import os
from pathlib import Path

from vouchsafe import Identity

# Generate once with: create_vouchsafe_id.py --label alice -o alice.json
# Store the identity JSON and its passphrase securely.
alice = Identity.from_data(
    json.loads(Path("alice.json").read_text()),
    passphrase=os.environ["VOUCHSAFE_IDENTITY_PASSPHRASE"],
)

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:

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:

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.

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.

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.

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.

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:

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

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

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

The resulting JSON contains the identity URN, public key, public-key hash, and specification version. Identity files are encrypted with a passphrase by default using PBKDF2-SHA-256 and AES-256-GCM. Provide the passphrase noninteractively with --passphrase-file, or set VOUCHSAFE_ASKPASS to an SSH_ASKPASS-compatible program. Use --create-unencrypted-identity-file only when an unencrypted private key is required; it is unsafe. --separate writes an unencrypted .urn, .public.pem, and .private.pem set 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.

Usage: create_vouchsafe_id.py [-h] [-l LABEL] [-s] [-q] [-e EXISTING] [-o OUTPUT]
                               [--public PUBLIC_FILE] [--private PRIVATE_FILE]
                               [--passphrase-file FILE] [--create-unencrypted-identity-file]

  -l, --label LABEL        Identity label (required unless --existing is used)
  -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
  --passphrase-file FILE   Read the identity passphrase from a file
  --create-unencrypted-identity-file
                            Create an unencrypted identity file (unsafe)

create_vouchsafe_token.py

# 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
Usage: create_vouchsafe_token.py [-h] -i IDENTITY [-f CLAIMS] [-c KEY=VALUE]
                                  [-p PURPOSE] [-e EXPIRES] [-o OUTPUT]
                                  [-t TOKEN_FILE] [-T TOKEN]
                                  [--passphrase-file FILE]
                                 [--attest | --vouch | --revoke]

  -i, --identity FILE      Identity JSON file (required)
  --passphrase-file FILE   Read the identity passphrase from a file
  -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

# 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
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:

{
  "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.
  • identity.to_identity_file(passphrase=None): export a 1.4.0 identity file; a non-empty passphrase encrypts the private key.

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.
  • load_identity(data, passphrase=None): validate and decrypt an identity file.
  • serialize_identity(identity, passphrase=None): write a validated 1.4.0 identity file.

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

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:

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

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:

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

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

Release files for vouchsafe 2.1.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 vouchsafe 2.1.0
File Size Uploaded
vouchsafe-2.1.0.tar.gz 47.6 kB Details

Built distribution (wheel)

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

Total release size: 84.7 kB

Release files / vouchsafe-2.1.0.tar.gz

Download URL vouchsafe-2.1.0.tar.gz
Size 47.6 kB
Tags Source
SHA-256 checksum
How to use checksums
a55dcc9cbc6f27798a80251f52b2866f086d49efc552b378481ae1a486a93cf3
BLAKE2b-256 checksum
How to use checksums
057ee8b0871a04a3e38381fa165c5479e9e996fe55a8e02184ed4cc7b795ae3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / vouchsafe-2.1.0-py3-none-any.whl

Download URL vouchsafe-2.1.0-py3-none-any.whl
Size 37.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cad9202348f31d0e2851f3bc09639042d652711b9c0fdf5a9efbada10a8f7669
BLAKE2b-256 checksum
How to use checksums
52c1b0f282724d3c96272524fd5dda75bb00fd460e87bf5570955eb927eef246
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

This release

2.1.0 This release

2 release files

1.0.0

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