SDKs are out: sigill-python and sigill-dotnet
The earlier posts in this series — the timestamped agent, the CI workflow, the hardening post — all share a structure: here's a pattern, here's a curl call against /tsa/stamp, here are forty lines of glue around it. That's the right shape for a tutorial. It's the wrong shape for a production codebase.
So today we're shipping two SDKs that do all the glue for you:
- sigill-python —
pip install sigill-sdk. Python 3.9 through 3.13. - sigill-dotnet —
dotnet add package Sigill.Sdk. Targetsnet8.0,net9.0, andnetstandard2.1.
Both are Apache-2.0, both are open source on GitHub, and both produce byte-identical output for the same input. An envelope sealed by either SDK verifies with either SDK — the canonical bytes match across languages by construction, and that's enforced by tests on every commit.
What the SDKs actually do
Three things, in roughly increasing depth.
Build the envelope. AiEvidenceEnvelopeV1 is a JSON shape that captures one AI generation: who ran it, why, what model, what prompt, what response, what context. The schema lives at spec/ai-evidence-envelope-v1.schema.json and it's the same schema in both repos, byte-for-byte. Both SDKs ship a fluent builder so you don't write the JSON by hand:
envelope = (
EnvelopeBuilder()
.with_purpose(category="summarization", business_context="ticket-summary")
.with_actor(type="service", id="svc-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 ticket in three bullets.")
.with_output_inline("Customer reports login fails after password reset.")
.build()
)
The .NET API is identical in shape, just PascalCased.
Seal it. client.seal(envelope) does the work the hardening post called for: canonicalize the envelope to RFC 8785 JCS bytes, hash those bytes with SHA-256, send the canonical bytes to Sigill's /tsa/stamp, attach the returned RFC 3161 token. What comes back is a sealed envelope with integrity.envelopeHash and proofs[] populated. This is the part that's annoying to implement by hand — JCS canonicalization has subtle rules about number formatting and key ordering, and a single byte difference between producer and verifier means the timestamp doesn't match. The SDKs handle it; you don't.
Verify it later. client.verify(sealed_envelope) recomputes the canonical bytes, recomputes the hash, parses the TSR, confirms the message-imprint binds to the envelope, and reports every problem it finds. Tampering, missing payloads, corrupt timestamps, unsupported proof types — all surface as structured VerificationIssue entries on the result, with the kind of kind field you can route on (hash_mismatch, invalid_proof, timestamp_unavailable, canonicalization_failed).
Keeping PII out of the envelope
The non-trivial feature is the hash-reference model. For sensitive prompts and responses you don't want to store inline, the SDK hashes the bytes you supply and records only the hash in the envelope. Original bytes are yours to keep, redact, or delete:
sealed = client.seal(
envelope, # uses with_prompt_ref / with_output_ref
external_payloads={
"prompt": prompt_bytes,
"output": response_bytes,
},
)
# envelope contains SHA-256("prompt bytes") under prompt.hash; the bytes
# themselves are NOT stored in the envelope, but they are bound to it.
When the right-to-erasure email arrives, you delete the bytes. The sealed envelope still proves the call happened, just not what was in it. When you need to audit, you supply the bytes again and verify() confirms they hash to the registered values. If the bytes have been deleted or modified, verification reports exactly which ref is missing or wrong — not a generic "verification failed."
This is the pattern the hardening post called for and the timestamped-agent example couldn't easily express. The SDKs make it the natural path.
Why two repos and not a monorepo
A monorepo would have made the cross-language byte-compatibility guarantee mechanical: one source of truth, one CI pipeline, both SDKs failing together if anything drifts. We went with two repos anyway, because that's the shape every language-specific SDK ships in — stripe-python / stripe-go, openai-python / openai-dotnet, aws-sdk-*. People searching NuGet for a .NET package expect a .NET-shaped repo behind it, with .NET-shaped issues, .NET-shaped releases, and contributors who don't have to install the other ecosystem's tooling to file a bug.
The interop guarantee is preserved by vendoring the same spec/ directory in both repos. Both test suites read the same spec/test-vectors/01-complete-ai-call/canonical.json and envelope-hash.txt and assert their canonical output matches byte-for-byte. CI fails in either repo if its vendored copy drifts. Schema changes are coordinated PRs across both — slightly more friction than a monorepo, but the same friction we'd impose on any external contributor anyway.
If you want a single read of the underlying contract — what's in an envelope, what gets hashed in what order, what "valid" means — spec/README.md is the canonical doc. Same content in both repos.
What's standard, what's Sigill
The same disclaimer that applied to the CI post applies here. The cryptographic primitives are open: RFC 8785 has been an IETF standard since 2020, RFC 3161 since 2001, SHA-256 since the late 1990s. You could implement everything in these SDKs against any RFC 3161 TSA — DigiCert, Sectigo, GlobalSign — and skip Sigill entirely. The schema, the canonicalization rules, the hash binding, the verification logic: none of it is proprietary, and the spec doc walks through every choice.
What Sigill adds is the hosted layer the timestamped-agent post described: round-robin failover across multiple TSAs, the lookup endpoint, verified-domain attribution. The SDKs target Sigill's API by default because that's what we operate, but the envelope format is open and the verification path doesn't require Sigill at all — once an envelope is sealed, anyone with the bytes can verify it offline.
What's not in v1
A few things on the list that didn't make this release:
- CAdES, ASiC, JWS proofs. v1 supports RFC 3161 only. The
proofs[]array is shaped to allow other proof types in v2 without breaking compatibility — sealed v1 envelopes will continue to verify with v2 SDKs unchanged. - Offline TSA chain validation. The SDK confirms the TSR's message-imprint matches your envelope, but does not validate the TSA's certificate chain back to a trust anchor. Sigill's
/tsa/verifyendpoint does that server-side; v2 will add a pluggable trust policy for fully offline validation. - Async Python client. The Python SDK is sync-only for now. If you need async, open an issue — it's a thin wrapper away.
- Other languages. Go, Java, and TypeScript are likely candidates if there's demand. The byte-compatible test-vector approach makes adding a language relatively straightforward; what takes time is the per-language ergonomics.
Try them
# Python
pip install sigill-sdk
# .NET
dotnet add package Sigill.Sdk
The READMEs walk through the 30-second example, the PII path, and the error-handling table. The spec doc is for when you want to know exactly what the bytes mean. And both repos have the test vectors committed under spec/test-vectors/ — if you're implementing your own client in a third language, those are your conformance fixtures.
The free Sigill tier is enough to seal a few hundred envelopes and decide whether the pattern fits. If you build something interesting on top of the SDKs, we'd like to hear about it.
Open source under Apache 2.0. Issues, PRs, and bug reports welcome on either repo.