Generic, payload-agnostic IETF SCITT + COSE Receipts substrate for Python (receipts strictly RFC9162_SHA256).
Project description
scitt-cose
A generic, payload-agnostic IETF SCITT + COSE Receipts substrate for Python: build/verify COSE_Sign1 Signed Statements, verify Receipts and RFC 9162 inclusion / consistency proofs, with the Merkle + receipt-signing primitives.
It is NOT a transparency service (operating a log — a hosted registration
endpoint — is a separate concern), and it carries NO application profile —
bring your own statement semantics. The only third-party dependencies anywhere
in the package are cbor2 and
cryptography, plus the standard
library. COSE_Sign1 (RFC 9052) is implemented here from scratch — it does not
use python-cwt or any other COSE library.
Name note.
scitt-coseis the finalized package name, claimed on PyPI and GitHub (action-state-group/scitt-cose) in the same pass.
What this does / does not do
Does:
- Verify a SCITT Signed Statement (
COSE_Sign1) signature against a supplied key — EdDSA and ES256 — and report its issuer / subject / content-type / alg. - Verify a COSE Receipt whose verifiable data structure is
RFC9162_SHA256(vds = 1, the tree algorithm registered by draft-ietf-cose-merkle-tree-proofs): the RFC 9162 SHA-256 inclusion proof and the log's signature over the reconstructed root — i.e. "this statement is provably in the log" — without trusting the log operator. The vds value is read from the protected header only and anything other thanRFC9162_SHA256is rejected, not silently accepted. - Provide the RFC 9162 Merkle primitives (root, inclusion, consistency) and a
build_receiptprimitive.
Does NOT:
- Operate a Transparency Service. It never registers statements, issues receipts, anchors, or stores anything. Running a log is a separate concern with its own operational trust obligations.
- Validate any application profile's payload semantics. The statement payload is treated as opaque bytes. There is no application-profile awareness — SBOMs, agent actions, or anything else; that neutrality is deliberate, and is what makes this reusable by anyone in the SCITT ecosystem.
- Depend on a COSE library for wire values.
COSE_Sign1is implemented from scratch overcbor2+cryptography; code points are pinned to the IANA registries, not to a library's enum.
Why this exists
This is a standalone, profile-opaque, cross-validated generic SCITT/COSE verifier: it verifies anyone's SCITT Signed Statements and COSE Receipts, treats the payload as opaque bytes, and its conformance is checked against independent, external references (the published RFC 6962/9162 Merkle vectors, a third-party COSE library, and a separate Go implementation — see Correctness & cross-implementation evidence).
It is intended to be useful as neutral substrate: the parts that exist today
don't fit that gap cleanly — python-cwt is COSE-only (no SCITT/Receipts), and a
transparency-service emulator is a server, not a verification library you depend
on. So this is the small building block — a second, independent implementation
you can verify against, with no profile baked in. (No primacy is claimed; the
value is neutrality + verifiable conformance, not being "first".)
An agent-action profile is one example consumer that builds its statement/claim semantics on top of this substrate — but nothing profile-specific lives here.
Provenance, neutrality & governance
Three things an adopter should know up front:
- Built by Action State Group. We built this for our own SCITT use and publish it as community substrate under Apache-2.0.
- Neutral by design. No application profile, no vendor coupling,
payload-opaque. The package imports only
cbor2,cryptography, and the standard library — it does not import any Action State code, and a test gate (tests/test_iana_codepoints.py) enforces that this stays true. - Foundation intent. We intend to contribute this project to a neutral open-source foundation so its governance does not rest with any single vendor. Which foundation is deliberately not yet decided; until then it is governed in the open under DCO + Apache-2.0 (see CONTRIBUTING.md).
Scope
In scope (library plumbing):
- Build / parse / verify Signed Statements and Transparent Statements.
- Verify Receipts + RFC 9162 inclusion and consistency proofs.
- RFC 9162 SHA-256 Merkle primitives and the receipt-signing primitive
(
build_receipt) — so you can mint receipts as a building block.
Out of scope (documented, deliberately not built):
- Operating a transparency log / service. A hosted registration endpoint, log storage, monitoring, witnessing, gossip — none of that is here. The primitives needed to build one (Merkle tree, receipt signing) are included.
Install
pip install cbor2 cryptography # runtime deps
# from a checkout (package root):
pip install -e .
Quick start
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
from scitt_cose import (
build_signed_statement, parse_signed_statement,
merkle_root, inclusion_proof, verify_inclusion,
consistency_proof, verify_consistency,
build_receipt, verify_receipt,
attach_receipts, extract_receipts,
)
# --- a key (caller supplies everything) ---
sk = ed25519.Ed25519PrivateKey.generate()
priv = sk.private_bytes(serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption())
pub = sk.public_key().public_bytes(serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo)
# --- Signed Statement (generic; no profile) ---
stmt = build_signed_statement(
b'{"hello":"world"}',
alg="EdDSA", private_key_pem=priv,
issuer="https://issuer.example",
subject="my-artifact",
content_type="application/json",
extra_cwt_claims={"my_claim": "x", 99: 1}, # str- or int-keyed
)
parsed = parse_signed_statement(stmt, public_key_pem=pub)
assert parsed["signature_verified"] is True
assert parsed["issuer"] == "https://issuer.example"
# --- Merkle (RFC 9162 SHA-256; hex in/out) ---
entries = [b"a".hex(), b"b".hex(), b"c".hex(), b"d".hex()]
root = merkle_root(entries)
path = inclusion_proof(entries, 2)
assert verify_inclusion(entries[2], 2, len(entries), path, root)
cproof = consistency_proof(entries, 2, 4)
assert verify_consistency(merkle_root(entries[:2]), root, 2, 4, cproof)
# --- Receipt (primitive) + verify ---
receipt = build_receipt(
leaf_entry_hex=entries[2], leaf_index=2, tree_entries_hex=entries,
alg="EdDSA", log_private_key_pem=priv, # here the "log" key is our key
)
res = verify_receipt(receipt, leaf_entry_hex=entries[2], log_public_key_pem=pub)
assert res.ok and res.root == root
# --- Transparent Statement = Signed Statement + Receipts ---
transparent = attach_receipts(stmt, [receipt])
assert extract_receipts(transparent) == [receipt]
CLI
scitt-cose --statement stmt.cose --statement-pubkey issuer.pem
scitt-cose --receipt receipt.cose --receipt-log-pubkey log.pem --leaf-entry-hex 61
scitt-cose --statement stmt.cose --receipt receipt.cose \
--receipt-log-pubkey log.pem --leaf-entry-hex 61 --json
Public API
| Area | Functions / types |
|---|---|
| COSE_Sign1 | sign_sign1, verify_sign1, Sign1, CoseError |
| Statements | build_signed_statement, parse_signed_statement, attach_receipts, extract_receipts |
| Merkle | leaf_hash, merkle_root, inclusion_proof, verify_inclusion, consistency_proof, verify_consistency |
| Receipts | build_receipt, verify_receipt, ReceiptResult |
| Status | DRAFT_TRACKING_NOTICE, DRAFT_SCITT_ARCHITECTURE, DRAFT_COSE_MERKLE_TREE_PROOFS, SUBSTRATE_RFCS |
Algorithms
sign_sign1 / statements support "EdDSA" (code point −8) and "ES256"
(−7). For ES256, COSE carries the signature as raw r || s (64 bytes), not
DER; the conversion happens at the cryptography boundary. ML-DSA code points
(RFC 9964) are recognized in the status notice but signing is not
implemented here.
Why CWT Claims at label 15 (not 13)
Signed-Statement CWT Claims are placed in the protected header at label 15,
the "CWT Claims" header parameter registered by RFC 9597 §2. Label 13 is
a different parameter (kcwt, RFC 9528) and is sometimes mistakenly used for the
claims map; this library always reads and writes the claims at 15.
Receipt vdp encoding (honest caveat)
The Receipt's verifiable-data-proof shape tracks draft-ietf-cose-merkle-tree-proofs-18:
- protected
1= alg, protected395= vds (1=RFC9162_SHA256); - unprotected
396= vdp map with key-1→ array of inclusion-proof bstrs; - each inclusion-proof bstr =
cbor([tree_size, leaf_index, [audit_path bstrs]]); - payload = the Merkle root (detached by default).
verify_receipt reads vds from the protected header only (it is
security-relevant and must be integrity-protected), reconstructs the root from
the proof, and verifies the COSE_Sign1 over that root with the log key. Because
the underlying documents are drafts, not RFCs, this exact CBOR shape is
validated by round-trip in this library's own tests, not against a frozen
RFC. Treat the wire shape as draft-tracking.
Draft-tracking / standards honesty
This library tracks two IETF documents that are Active Internet-Drafts (Work in Progress) — they have been approved and are in the RFC Editor Queue, but are NOT yet published RFCs (status audited against the IETF Datatracker at ship date; re-verify at publish time, since RFC-Ed-Queue documents can be published at any point):
draft-ietf-scitt-architecture-22— SCITT Architecture (Datatracker: Active Internet-Draft, RFC Ed Queue)draft-ietf-cose-merkle-tree-proofs-18— COSE Receipts / COSE Merkle Tree Proofs (Datatracker: Active Internet-Draft, RFC Ed Queue)
No unassigned RFC number is claimed anywhere (enforced by a test that scans shipped source + docs).
Published RFCs whose mechanisms this library implements / relies on (titles verified against the RFC Editor / IANA registries):
- RFC 9052 — CBOR Object Signing and Encryption (COSE): Structures and Process (COSE_Sign1, Sig_structure).
- RFC 9053 — COSE: Initial Algorithms (EdDSA −8, ES256 −7).
- RFC 9162 — Certificate Transparency Version 2 (SHA-256 Merkle tree; inclusion and consistency proofs; reuses the RFC 6962 tree hash).
- RFC 9597 — CBOR Web Token (CWT) Claims in COSE Headers — the CWT Claims
header parameter at label 15 (IANA COSE Header Parameters registry). Label
13 is
kcwtand 14 iskccs, both from RFC 9528 (EDHOC) — not the CWT Claims map; using 13 for claims is the python-cwt bug this library avoids. - RFC 9964 — ML-DSA for JOSE and COSE (code points recognized; ML-DSA signing not implemented here).
Independence
scitt_cose/ imports only cbor2, cryptography, and the standard library.
It does not import cwt / python-cwt, pycose, or any consuming
product/profile package. Verify:
grep -rnE 'import (cwt|pycose)\b' scitt_cose/ # → nothing
Two tests keep this true: the COSE-library import guard and the
no-downstream-code neutrality gate (both in tests/test_iana_codepoints.py).
Correctness & cross-implementation evidence
For a community verifier, "trust me, it's conformant" is worthless. The correctness story rests on agreement with things outside this library, not on self-consistency:
- Cross-language agreement (in CI). An independent Go verifier
(the
scitt-cose-go-verifydirectory, built onveraison/go-cose+fxamacker/cbor, with a clean-room Merkle fold) verifies the statements and receipts this library emits, agrees on the reconstructed Merkle root, and rejects tampered inputs. CI runs it withSCITT_REQUIRE_GO=1, so the cross-check can never silently skip. (tests/test_crosslang_go.py) - The standard's own test vectors. The Merkle code is checked against the
published RFC 6962 / RFC 9162 reference vectors (8-leaf root
5dc9da79…, the canonical inclusion/consistency proofs) — external values, not ours. (tests/test_rfc9162_vectors.py) - A third-party COSE library.
pycose(independent of us, and notpython-cwt) both verifies our statements and emits statements we verify — so the round-trip isn't self-referential. (tests/test_thirdparty_cose.py) - The COSE standard's own reference-signed vector. We verify the canonical
COSE_Sign1 the COSE WG published (RFC 9052 §C.2.1, ES256, from the
cose-wg/Examplescorpus) — anchoring the signature layer to the spec's reference output, just as the RFC 6962 vectors anchor the Merkle layer. (tests/test_cose_wg_vectors.py) - A real Transparency-Service verifier. A downstream Transparency
Service's own verifier — written independently of this package, on a
different COSE stack (
python-cwt, vs this package's from-scratchcbor2/cryptography) — cross-verifies the same Signed Statements and the same TS-issued Receipts and agrees on the reconstructed Merkle root, in both directions. This exercises the real register→issue→verify shape; the integration test lives in the consuming repo (so this package stays standalone). This is not the hosted verify endpoint: that endpoint runs this package unchanged (a convenience deployment, deliberately the same library), so it is not an independent oracle — the python-cwt verifier is. - IANA values, not library enums. Every wire code point is asserted against
its registry/RFC number (CWT Claims 15, not python-cwt's
13). (tests/test_iana_codepoints.py) - Spec-driven negative suite. The MUST-reject conditions — alg confusion,
critical-header (
crit) handling,vdsdowngrade, wrong-leaf/wrong-log receipts, Merkle edge cases, malformed CBOR — each have a test. (tests/test_negative_conformance.py)
Evaluated and not used: the SCITT API emulator. The
scitt-community/scitt-api-emulatorwas considered as an ecosystem oracle and rejected: it is archived/unmaintained (final "pre-archive" tag, Nov 2024) and emits the obsolete pre-standard receipt format (draft-birkholz-scitt-receipts— string labelsservice_id/tree_alg, not a COSE_Sign1 receipt, not thevds=395 /vdp=396 RFC 9162 structure this library verifies); its statements usepycose, already covered above. Pinning a non-conformant, drift-prone implementation would be a misleading oracle, so the COSE WG spec vector is used in its place.
Hosted verification — a standalone SCITT-only verifier
scitt_cose.hosted is a stateless, read-only wrapper over the same verify
functions, so you can offer verification without anyone installing anything — and
without the submitter having to trust the operator with their data (nothing
stored, nothing logged, payload-opaque; the receipt path needs only the leaf
digest, never the payload). tests/test_hosted_parity.py asserts the hosted
verdict equals the local verdict on a fixture set, including the ASGI path.
This is a SCITT-only verifier, and it is NOT a Transparency Service. It verifies statements and receipts; it never registers, issues receipts, or anchors. Running a Transparency Service is a separate offering with its own operational trust obligations — deliberately out of scope here.
Run it standalone (no other service involved):
# Zero extra deps — stdlib HTTP server:
scitt-cose-serve # 127.0.0.1:8080
# Or under a production ASGI server (pip install "scitt-cose[serve]"):
uvicorn scitt_cose.hosted:make_asgi_app --factory --host 0.0.0.0 --port 8080
# Or containerized (Dockerfile ships with the package):
docker build -t scitt-verifier . && docker run -p 8080:8080 scitt-verifier
GET / -> capabilities (what it does / does not do)
POST /verify -> {valid, statement, receipt, reasons} # JSON body, see below
GET / is content-negotiated: browsers (Accept: text/html) get a static
landing page that renders the verifier-vs-Transparency-Service boundary table
on the page itself — not buried in docs — while API clients get the same data
as JSON (including a boundary field). Both are generated from the same
constants (scitt_cose.hosted.BOUNDARY_TABLE), so page and API can't drift;
tests/test_hosted_page.py pins the table's presence on both.
It can also ride along inside an existing ASGI app via
app.mount("/scitt-verify", scitt_cose.hosted.make_asgi_app()) — same logic,
shared deployment, still standalone code. The full design, submitter-safety
constraints, and proposed deployment shape are in
docs/hosted-verifier-design.md. A hosted
instance is live at https://verify.actionstate.ai — this package unchanged
(the parity-tested wrappers, deployed); its landing page renders the
verifier-vs-Transparency-Service boundary table and states the privacy posture.
You don't need it: the verifier runs anywhere.
Tests
python3 -m pytest -q # unit + conformance suite (package root)
SCITT_REQUIRE_GO=1 python3 -m pytest -q # also forces the Go cross-check
python3 -m ruff check .
License & contributing
Apache-2.0 — see LICENSE and NOTICE. Contributions are
accepted under the Developer Certificate of Origin (sign your commits with
git commit -s); scope rules and the standards-honesty gates are in
CONTRIBUTING.md.
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 scitt_cose-0.0.2.tar.gz.
File metadata
- Download URL: scitt_cose-0.0.2.tar.gz
- Upload date:
- Size: 66.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
53bc7117a90b1f93e72a1dae784006dae4e6f5b11334361b365f6d1dc820b700
|
|
| MD5 |
ca04c78bac4db76b92d7b6f5591fa856
|
|
| BLAKE2b-256 |
36f8786bc2f4ffccef14aa953fa80a7e56b2735c97a38666d6340a7a400192b4
|
File details
Details for the file scitt_cose-0.0.2-py3-none-any.whl.
File metadata
- Download URL: scitt_cose-0.0.2-py3-none-any.whl
- Upload date:
- Size: 40.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc5a387e78a5b7ef690c678d4c5eb20ffa5edcd4ba3f9ea64e9eba1def9efb4b
|
|
| MD5 |
06ed300a45a12c796b385638f2d5b4dc
|
|
| BLAKE2b-256 |
263fdf8ec4bfe359c2b8f54144687fd19e5f269f6a584aa0a0470900a8f9de62
|