Skip to main content

Pure-Python implementation of the obsigil mandate-token format (AES-SIV / AES-GCM-SIV)

Project description

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()                      # 64 CSPRNG bytes

token = Obsigil.mint(
    clauses={"role": "admin"},                    # opaque application data
    mandate_key=key,
    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, 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.

Project details


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.1.0.tar.gz (29.9 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.1.0-py3-none-any.whl (31.1 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for obsigil-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3647ef4404156c9c757592a1250ec9fd95c327c32464843b3a1bc2cdb056b14b
MD5 ce5bc6a4e7a14666dffe73eb80453a69
BLAKE2b-256 e7187f2451da41388c8c1925feff6eb343051141c604c0b5fafa7f35a4b94fec

See more details on using hashes here.

Provenance

The following attestation bundles were made for obsigil-0.1.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.1.0-py3-none-any.whl.

File metadata

  • Download URL: obsigil-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 29fe2a033c9bcdf49270a212c9d0a4f73d14afe8ea8563852b35e6c104a7ce18
MD5 533d8ccdc1d93f7eac9b2cdbae38714b
BLAKE2b-256 c11ebaf595ed9f75069f0189ab4cbb79a35322037c0a1ca3d6e4342e3eb7878e

See more details on using hashes here.

Provenance

The following attestation bundles were made for obsigil-0.1.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.

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