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

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

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

# 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]
                                 [--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

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

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

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vouchsafe-1.0.0.tar.gz (39.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vouchsafe-1.0.0-py3-none-any.whl (32.7 kB view details)

Uploaded Python 3

File details

Details for the file vouchsafe-1.0.0.tar.gz.

File metadata

  • Download URL: vouchsafe-1.0.0.tar.gz
  • Upload date:
  • Size: 39.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for vouchsafe-1.0.0.tar.gz
Algorithm Hash digest
SHA256 2c201766a268459c667cbb926c7cded076cd64eeec79366a2d18b3505d963f6c
MD5 e307db00c29c019e846feaa32285cd47
BLAKE2b-256 df335738a600821f03d77147a5f98d4c485b466787669d0f9c981a1aad2fdfc9

See more details on using hashes here.

File details

Details for the file vouchsafe-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: vouchsafe-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 32.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for vouchsafe-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dd256a4e55a28e1ebde56d8a75bd2dc6d1cf3565f36eab821a2183a24e656bd0
MD5 04e54c6f232b94c0fa39a521564ddd9a
BLAKE2b-256 d877674c4ff7f10bd7a09edb4844a1d7649f6e940126ac9a7b0e60a573c5273e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page