Skip to main content

obsigil

Homepage: https://obsigil.org

Pure-Python implementation of obsigil, a mandate-token format and shared-secret JWT alternative: a token split into a public, advisory manifest and a secret-sealed, authoritative mandate. Each half is an authenticated, deterministically-sealed ciphertext — AES-SIV (RFC 5297) or AES-GCM-SIV (RFC 8452) — in compact text.

Each half's fields are a single canonical CBOR map (RFC 8949 §4.2): reserved fields take negative integer keys (tid, exp, aud, sub, iss), application data takes non-negative integer or text-string keys. obsigil owns the canonical encoding, so the same fields under the same key seal to byte-identical tokens with no shared serializer.

Verification is symmetric: the verifier holds the same key that mints, so obsigil fits shared-secret (HS256-style) JWT and JWE use cases, not public-key verification.

The AEAD primitives come from cryptography (OpenSSL-backed, audited) and CBOR decoding from cbor2; the canonical CBOR encoder and the obsigil logic are small, readable Python. No compiled extension to build — it installs as a universal wheel and you can read the whole format end to end.

import obsigil
from obsigil import Obsigil

key = obsigil.generate_key()                      # 128-char hex, store as a secret

token = Obsigil.mint(
    clauses={"role": "admin"},                    # opaque application data
    mandate_key=key,                              # hex by default (or 64 raw bytes)
    exp=4_000_000_000,
    aud=["api"],
    sub="u42",
    manifest={"iss": "auth.example"},             # optional public half
)

# Front end (advisory — manifest is non-authoritative, §16.7):
front = Obsigil(token.token())
claims = front.claims()                           # dict | None
header = front.authorization_header()             # "Bearer .0…" — send this

# Backend (authoritative):
mandate = Obsigil(token.token(), keys=key, audience="api", now=1)
role = mandate.clause("role")                     # raises ObsigilError if invalid
when = mandate.exp()

API

A single Obsigil type views a token in three roles (API conformance, §12):

  • Obsigil.mint(*, clauses, mandate_key, exp, tid=None, aud=None, sub=None, iss=None, alg="0", encoding="b64", manifest=None) — the issuer: seal a mandate (and an optional keyless manifest) and return the view. tid is generated (a fresh UUIDv7) unless supplied; a supplied one must be a well-formed UUIDv7. Defaults are AES-SIV (alg="0") and base64 (encoding; also "hex"). Application clauses use non-negative integer or text keys; reserved fields are set via their keyword arguments.
  • Obsigil(token, *, keys=None, audience=None, leeway=0, now=None, max_decoded_len=65536, on_reject=None) — keyless for the front end, or with keys for the backend. keys may be one key or several (key selection by trial decryption, §16.5); leeway is clamped to a fixed maximum (Limits and robustness, §16.10).

Each half is reachable at three fidelities (the three fidelities of API conformance, §12.2):

manifest mandate
wire string manifest() mandate()
plaintext (CBOR octets) manifest_plaintext() mandate_plaintext()
parsed claims() clauses()
  • clauses() authenticates and enforces policy (exp, aud, tid, types), returning a dict or raising one opaque ObsigilError. Reserved clauses are surfaced under their names (tid as text); application fields keep their wire keys. Accessors: exp(), tid(), issued_at(), sub(), iss(), aud(), clause(key).
  • clauses_unchecked() authenticates and decodes the canonical CBOR but skips the value checks; mandate_plaintext() authenticates only and returns the raw CBOR octets. Both are backend-internal — keep them non-bearer-facing (authentication vs policy layers, §16.3).
  • claims() opens the keyless manifest for display — advisory, returns a dict or None, never raises (the manifest is non-authoritative, §16.7).
  • mandate() / manifest() — each half as a standalone token; authorization_header(scheme="Bearer") gives the manifest-absent .0mandate form to forward to the backend (Audiences, §9).

The free functions mint, clauses, claims, mandate, manifest, clauses_unchecked, mandate_plaintext, manifest_plaintext, and authorization_header wrap the same core, plus generate_key (128-char hex) / generate_key_bytes (64 raw bytes), generate_uuid7, is_uuid7, is_uuid7_bytes, uuid7_time, MANIFEST_KEY, ObsigilError, Reason. The granular Reason is delivered to on_reject for internal logging only — never to the bearer (the uniform-failure rule, §16.6).

Install

pip install obsigil

Requires Python ≥ 3.9. AES-GCM-SIV (algorithm code 1) needs OpenSSL 3.2+, which modern cryptography wheels bundle; AES-SIV (code 0, the mandatory default) has no such floor.

Conformance

obsigil implements canonical CBOR (RFC 8949 §4.2) and the validation rules of Reserved fields §8 and Limits and robustness §16.10. The bundled test suite covers round-trip, the verification ladder, and the negative cases (non-canonical CBOR, duplicate keys, wrong-typed reserved fields, expiry, audience, size and leeway bounds):

pip install -e .[test]
pytest

Cross-language byte-for-byte known-answer vectors are tracked separately in the obsigil-test-vectors suite, kept in step with the canonical-CBOR format.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Download files

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

Source Distribution

obsigil-0.2.0.tar.gz (32.0 kB view details)

Uploaded Source

Built Distribution

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

obsigil-0.2.0-py3-none-any.whl (32.1 kB view details)

Uploaded Python 3

File details

Details for the file obsigil-0.2.0.tar.gz.

File metadata

  • Download URL: obsigil-0.2.0.tar.gz
  • Upload date:
  • Size: 32.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for obsigil-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e2bbab33a98537d689c7a20c4223f51013f7861e8ead3200ef35517d2f320855
MD5 7e3be17fc41d8aeae3b21dc930b8b327
BLAKE2b-256 4e82f7b90324f4fcd1de389c8eefe02a6e9451561e48a69bfca896744395d1a8

See more details on using hashes here.

Provenance

The following attestation bundles were made for obsigil-0.2.0.tar.gz:

Publisher: publish-pypi.yml on deyanovich/obsigil-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file obsigil-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: obsigil-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 32.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for obsigil-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d4fe5dcb11e1fa816a3a3d2cf9d63467816ece90d09f2841ec5a79043c2be0a5
MD5 ffe5fe23eb1c8acd052dd3f0e738a147
BLAKE2b-256 a2ad2125f2c2cfa718f001bf1e032bd085cabf474d041e1af2dab10c2817d5fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for obsigil-0.2.0-py3-none-any.whl:

Publisher: publish-pypi.yml on deyanovich/obsigil-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.0

2 files

This release

0.2.0 This release

2 files

0.1.0

2 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