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.tidis 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"). Applicationclausesuse 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 withkeysfor the backend.keysmay be one key or several (key selection by trial decryption, §16.5);leewayis 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 opaqueObsigilError. Reserved clauses are surfaced under their names (tidas 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 orNone, 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.0mandateform 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
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3647ef4404156c9c757592a1250ec9fd95c327c32464843b3a1bc2cdb056b14b
|
|
| MD5 |
ce5bc6a4e7a14666dffe73eb80453a69
|
|
| BLAKE2b-256 |
e7187f2451da41388c8c1925feff6eb343051141c604c0b5fafa7f35a4b94fec
|
Provenance
The following attestation bundles were made for obsigil-0.1.0.tar.gz:
Publisher:
publish-pypi.yml on deyanovich/obsigil-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
obsigil-0.1.0.tar.gz -
Subject digest:
3647ef4404156c9c757592a1250ec9fd95c327c32464843b3a1bc2cdb056b14b - Sigstore transparency entry: 2026881361
- Sigstore integration time:
-
Permalink:
deyanovich/obsigil-py@5a2e04153bff427ee9fb03b0c741628261c85d18 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/deyanovich
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@5a2e04153bff427ee9fb03b0c741628261c85d18 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29fe2a033c9bcdf49270a212c9d0a4f73d14afe8ea8563852b35e6c104a7ce18
|
|
| MD5 |
533d8ccdc1d93f7eac9b2cdbae38714b
|
|
| BLAKE2b-256 |
c11ebaf595ed9f75069f0189ab4cbb79a35322037c0a1ca3d6e4342e3eb7878e
|
Provenance
The following attestation bundles were made for obsigil-0.1.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on deyanovich/obsigil-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
obsigil-0.1.0-py3-none-any.whl -
Subject digest:
29fe2a033c9bcdf49270a212c9d0a4f73d14afe8ea8563852b35e6c104a7ce18 - Sigstore transparency entry: 2026881451
- Sigstore integration time:
-
Permalink:
deyanovich/obsigil-py@5a2e04153bff427ee9fb03b0c741628261c85d18 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/deyanovich
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@5a2e04153bff427ee9fb03b0c741628261c85d18 -
Trigger Event:
push
-
Statement type: