AI evidence envelopes (AiEvidenceEnvelopeV1) — canonicalize, seal with RFC 3161 timestamps via Sigill, and verify.
Project description
sigill-sdk (Python)
Tamper-evident AI evidence envelopes for Python. Build an AiEvidenceEnvelopeV1
record of any AI generation, seal it with an RFC 3161 timestamp via Sigill,
and verify it offline at any later point.
The cryptographic primitives — RFC 8785 canonical JSON, SHA-256 hash binding, RFC 3161 timestamp parsing — are all handled inside the SDK. You hand it your prompt, response, and metadata; you get back a signed envelope. Apps don't need to implement canonicalization, hash binding, or timestamp protocol logic themselves.
For the underlying spec — what's in an envelope, what gets hashed in what order, what
"valid" means — see spec/README.md.
The same spec ships in this repo's sibling: the .NET SDK at sigill-dotnet.
Identical test vectors, byte-compatible output.
Install
pip install sigill-sdk
Python 3.9+. The only runtime dependencies are httpx, jcs (the reference RFC 8785
implementation), and asn1crypto.
30-second example
from sigill_sdk import SigillClient, EnvelopeBuilder
client = SigillClient(api_key="sigill_...") # from Settings → API Keys at sigill.ai
envelope = (
EnvelopeBuilder()
.with_purpose(category="summarization", business_context="support-ticket-summary")
.with_actor(type="service", id="svc-support-summarizer", tenant_id="tenant-acme")
.with_activity(name="ticket.summarize", correlation_id="trace-abc-123")
.with_model(provider="anthropic", name="claude-opus-4-7",
parameters={"max_tokens": 1024, "temperature": 0.2})
.with_prompt_inline("Summarize the following support ticket in three bullet points.")
.with_output_inline("Customer reports login fails after password reset.")
.build()
)
sealed = client.seal(envelope)
# sealed["integrity"]["envelopeHash"] ← SHA-256 of canonical JSON
# sealed["proofs"][0]["tsrBase64"] ← RFC 3161 timestamp from Sigill
# ...persist sealed somewhere durable (DB, S3, your audit log)...
# Later — re-verify cryptographically. Anyone with the sealed envelope can do this:
result = client.verify(sealed)
assert result.is_valid
print("Stamped at:", result.timestamps[0]["gen_time"], "by", result.timestamps[0]["tsa_name"])
That's the whole hot path. Everything below is detail you only reach for when you need it.
Keeping PII out of the envelope
For sensitive prompts and responses, store hash references in the envelope instead of the content itself. The SDK hashes the bytes you supply, records the hash in the envelope, and the original bytes are yours to keep, redact, or delete.
prompt_bytes = "Classify identity doc. Subject: Jane Doe, born 1985-03-14.".encode()
response_bytes = b'{"document_type":"passport","confidence":0.97}'
envelope = (
EnvelopeBuilder()
.with_purpose(category="classification", regulatory_basis=["EU-AI-Act:Annex-III"])
.with_actor(type="user", id="user-9b2f1a", tenant_id="tenant-acme")
.with_activity(name="kyc.classify")
.with_model(provider="anthropic", name="claude-opus-4-7")
.with_prompt_ref("prompt", content_type="text/plain")
.with_output_ref("output", content_type="application/json")
.with_policy_metadata(redactionApplied=True, redactionPolicy="pii-redaction-v3")
.build()
)
sealed = client.seal(
envelope,
external_payloads={"prompt": prompt_bytes, "output": response_bytes},
)
# The envelope now contains SHA-256("prompt bytes") and SHA-256("response bytes")
# under prompt.hash and output.hash. The bytes themselves are NOT stored.
When you later need to audit, supply the bytes again — verify confirms they hash to the same registered values:
result = client.verify(
sealed,
external_payloads={"prompt": prompt_bytes, "output": response_bytes},
)
assert result.is_valid
If the bytes have been deleted or modified, verification reports exactly which ref
is missing or wrong:
result = client.verify(sealed, external_payloads={"prompt": prompt_bytes})
# result.is_valid -> False
# result.issues[0].kind -> VerificationIssueKind.HASH_MISMATCH
# result.issues[0].target -> "output"
# result.issues[0].message -> "payload_not_supplied: external bytes for ref 'output' …"
Error handling
Producer-time errors raise; verification errors are collected. This split is deliberate: when sealing, you have a single in-flight operation that either works or doesn't. When verifying, an audit UI wants every problem at once, not just the first.
| When | Surface | Spec §7 kind |
|---|---|---|
seal() — every TSA Sigill tried failed |
TimestampUnavailable (with failures: list) |
timestamp_unavailable |
seal() — caller pre-declared a hash that doesn't match supplied bytes |
HashMismatch |
hash_mismatch |
seal() — input contains values JCS rejects (NaN, Infinity) |
CanonicalizationFailed |
canonicalization_failed |
verify() — anything wrong |
result.issues[], result.is_valid == False |
per-issue kind field |
A typical seal-with-fallback:
from sigill_sdk import SigillClient, TimestampUnavailable
try:
sealed = client.seal(envelope, external_payloads=payloads)
persist(sealed)
except TimestampUnavailable as e:
# All TSAs in our rotation failed. Persist the envelope unsealed and seal it later.
log.warning("TSA outage: %d attempts, failures=%r", e.attempts, e.failures)
persist_for_async_sealing(envelope, payloads)
Cross-language interop
This SDK and the .NET SDK at sigill-dotnet share the same spec, JSON Schema, and test vectors. An envelope sealed by either SDK verifies with either SDK — the canonical bytes are byte-identical.
The interop guarantee is enforced by tests: both test suites read the same files
under spec/test-vectors/
and assert that their canonical output matches the committed reference bytes. The spec/ directory
in this repo is a vendored copy; the canonical source lives under spec/ in
sigill-dotnet too, and the bytes are
byte-identical between the two.
Pinning a specific TSA
By default, seal() uses Sigill's auto mode — round-robin across the TSAs you have
enabled, with automatic failover. That's the recommended setting for production: you
get redundancy at no cost.
If you need to record that a specific TSA produced the timestamp (compliance reason, specific policy OID), pass it explicitly:
sealed = client.seal(envelope, tsa_slug="digicert") # SHA-256, US TSA
sealed = client.seal(envelope, tsa_slug="sectigo") # SHA-512
sealed = client.seal(envelope, tsa_slug="skid-ecc", # eIDAS Qualified
qualified=True)
Available slugs and their properties: see Sigill's TSA documentation.
Async / context manager
SigillClient is a sync client built on httpx.Client. Wrap it in with to ensure
the underlying HTTP connection pool is closed when you're done:
with SigillClient(api_key="...") as client:
sealed = client.seal(envelope)
If you need an async API, open an issue — it's a thin wrapper away.
Lower-level surface
The SDK exposes its primitives in case you need them outside the seal/verify flow:
from sigill_sdk import canonicalize, compute_envelope_hash
canonical_bytes = canonicalize({"b": 2, "a": 1}) # → b'{"a":1,"b":2}'
digest_hex, canonical_bytes = compute_envelope_hash(envelope)
This is what every test vector is built from, and it's what the cross-language interop guarantee comes down to.
What this SDK is not
It is not a substitute for TSA chain validation. The SDK confirms the TSR's
embedded message-imprint matches your envelope, but it does not — by design in v1 —
validate the TSA's certificate chain back to a trust anchor. Sigill's
POST /tsa/verify endpoint does that server-side; for offline trust-anchor
validation, use a dedicated library like
sigstore-python or shell out to
openssl ts -verify. v2 of this SDK will provide a pluggable trust policy.
Development
git clone https://github.com/sigill-ai/sigill-python.git
cd sigill-python
pip install -e ".[dev]"
pytest
The 39-test suite runs offline in <1s. CI runs against Python 3.9 through 3.13.
License
Apache 2.0 — see LICENSE.
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 sigill_sdk-0.1.4.tar.gz.
File metadata
- Download URL: sigill_sdk-0.1.4.tar.gz
- Upload date:
- Size: 30.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
190641c90f514a43de0b813209bdc4a5930bb10ad15c990ca1eeb66ff2cddb46
|
|
| MD5 |
7dd6d6837842a6d67a60843aea04fe33
|
|
| BLAKE2b-256 |
eaa25b3d8f8f79a8932caaa441ecb108adeaefa20d490cc8b81cb22278480bc9
|
Provenance
The following attestation bundles were made for sigill_sdk-0.1.4.tar.gz:
Publisher:
release.yml on sigill-ai/sigill-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sigill_sdk-0.1.4.tar.gz -
Subject digest:
190641c90f514a43de0b813209bdc4a5930bb10ad15c990ca1eeb66ff2cddb46 - Sigstore transparency entry: 1498505463
- Sigstore integration time:
-
Permalink:
sigill-ai/sigill-python@8429c7b4dd7eaac7f4058289978998652765072e -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/sigill-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8429c7b4dd7eaac7f4058289978998652765072e -
Trigger Event:
push
-
Statement type:
File details
Details for the file sigill_sdk-0.1.4-py3-none-any.whl.
File metadata
- Download URL: sigill_sdk-0.1.4-py3-none-any.whl
- Upload date:
- Size: 23.5 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 |
3b56a14a073e6cf0673732c4b86ee0e65dfbc5a4aef941964c4676de67fdcc8e
|
|
| MD5 |
ad5979210481391e8ef631efd2f37c15
|
|
| BLAKE2b-256 |
ecb2719de78997668763d72d4924d945f7e2ee78ba25b7f0a06da0b5844745c9
|
Provenance
The following attestation bundles were made for sigill_sdk-0.1.4-py3-none-any.whl:
Publisher:
release.yml on sigill-ai/sigill-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sigill_sdk-0.1.4-py3-none-any.whl -
Subject digest:
3b56a14a073e6cf0673732c4b86ee0e65dfbc5a4aef941964c4676de67fdcc8e - Sigstore transparency entry: 1498505547
- Sigstore integration time:
-
Permalink:
sigill-ai/sigill-python@8429c7b4dd7eaac7f4058289978998652765072e -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/sigill-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8429c7b4dd7eaac7f4058289978998652765072e -
Trigger Event:
push
-
Statement type: