Skip to main content

scitt-cose

CI

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-cose is 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 carrying either of two verifiable data structures: RFC9162_SHA256 (vds = 1, the tree algorithm registered by draft-ietf-cose-merkle-tree-proofs) or CCF ccf.v1 (vds = 2, Microsoft CCF's Merkle format, used by scitt-ccf-ledger v7+) — 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 than these two registered structures is rejected, not silently accepted.
  • Provide the RFC 9162 Merkle primitives (root, inclusion, consistency) and a build_receipt primitive.

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_Sign1 is implemented from scratch over cbor2 + 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:

  1. Built by Action State Group. We built this for our own SCITT use and publish it as community substrate under Apache-2.0.
  2. 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.
  3. 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).
  4. Development. This project is built and maintained by Action State Group, with AI-assisted engineering (Claude Code) used as a development tool. Every contribution is human-reviewed and DCO-signed. Correctness does not rest on trusting any author — human or otherwise: conformance is enforced in CI by the published RFC 6962/9162 test vectors, an independent clean-room Go implementation, and cross-checks against a third-party COSE library, none of which can be skipped.

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

Failure contract

A verifier's behaviour on hostile and malformed input is part of its API, so it is spelled out here and enforced by tests (tests/test_hardening.py) and by continuous differential fuzzing (see below).

  • parse_signed_statement and verify_receipt never raise on input. Every outcome — bad signature, malformed CBOR, truncation, a hostile length or tree size — is reported in the returned value (signature_verified / ReceiptResult.ok), never as a leaked cbor2 / RecursionError / other exception. A CoseError is reserved for the low-level primitive verify_sign1 (a building block), which raises on failure by design.
  • Identity is authenticated-only. parse_signed_statement populates issuer / subject / content_type / alg / claims only when signature_verified is True. When the signature did not verify (wrong key, bad signature) or no key was supplied, those top-level fields are None, and the structurally-decoded values are fenced under an explicit unverified sub-dict — so an integrator can never mistake an attacker-chosen issuer for a signed one. verified ⇒ trust the fields; not verified ⇒ they are under unverified`, clearly labelled.
  • Strict, non-malleable decoding. Both verify paths reject trailing bytes after the COSE_Sign1, indefinite-length / non-deterministic encodings, and duplicate protected-header keys — so one signature cannot be presented in many byte encodings, and the Python and Go verifiers agree on accept and reject.
  • Bounded cost. Messages are size-capped; tree sizes and inclusion-proof lengths are bounded and length-checked before any hashing or allocation, and no decoded integer is ever coerced to bytes before validation. A tiny hostile receipt cannot trigger a multi-gigabyte allocation or an unbounded loop on either runtime.

See docs/hardening-review.md for the review, the finding-by-finding resolutions, and the consciously-accepted items.

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.

ES256 signatures are accepted in their canonical form as verified by cryptography; s-value malleability (high-s) is not rejected — see the Security Considerations in docs/hardening-review.md for why that is a documented, accepted property rather than a bug.

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, protected 395 = 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.

Standards status / honesty

The SCITT Architecture is now RFC 9943 (published June 2026). This library implements RFC 9943 as its normative SCITT base.

This library also tracks one IETF document that remains an Active Internet-Draft (Work in Progress) in the RFC Editor Queue, NOT yet published as an RFC (status audited at ship date; re-verify at publish time):

  • draft-ietf-cose-merkle-tree-proofs-18COSE 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 9052CBOR Object Signing and Encryption (COSE): Structures and Process (COSE_Sign1, Sig_structure).
  • RFC 9053COSE: Initial Algorithms (EdDSA −8, ES256 −7).
  • RFC 9162Certificate Transparency Version 2 (SHA-256 Merkle tree; inclusion and consistency proofs; reuses the RFC 6962 tree hash).
  • RFC 9597CBOR Web Token (CWT) Claims in COSE Headers — the CWT Claims header parameter at label 15 (IANA COSE Header Parameters registry). Label 13 is kcwt and 14 is kccs, both from RFC 9528 (EDHOC) — not the CWT Claims map; using 13 for claims is the python-cwt bug this library avoids.
  • RFC 9964ML-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-verify directory, built on veraison/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 with SCITT_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 not python-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/Examples corpus) — 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-scratch cbor2/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, vds downgrade, 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-emulator was 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 labels service_id/tree_alg, not a COSE_Sign1 receipt, not the vds=395 / vdp=396 RFC 9162 structure this library verifies); its statements use pycose, 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.

Cross-implementation validation

At IETF 126 (Vienna, Jul 2026), receipt verification was cross-validated: microsoft/scitt-ccf-ledger PR #424 pins the published pyscitt test inputs as vectors-ietf126; ccf.v1 + RFC 9162 SHA-256 receipts were verified under one dispatch path against this library's verify_receipt().

Hosted verification — a standalone SCITT-only verifier

hosted_profiles.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.

Not part of the published scitt-cose package. hosted_profiles/ (the hosted HTTP wrapper plus its application-profile renderers) is excluded from the built wheel — the neutral, pip install-able package is verifier APIs only. hosted_profiles.hosted only runs from a full repo checkout (see the Dockerfile, or clone + run below).

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 from a repo checkout (no other service involved):

# Under a production ASGI server (pip install ".[serve]" from the checkout):
uvicorn hosted_profiles.hosted:make_asgi_app --factory --host 0.0.0.0 --port 8080

# Or containerized (Dockerfile ships in the repo):
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 (hosted_profiles.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", hosted_profiles.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.

Bundle & capsule permalinks — offline, not just hosted

GET /v/<capsule_id> and GET /bundle are the recipient-side viewers behind capsule bundle's permalinks. The record never leaves the browser in either mode — it rides in the URL fragment (the part after #), which HTTP never transmits, so the server serving the viewer's HTML/JS never sees the bytes being verified. Integrity, Sequence and (bundle-only) Completeness/ Cross-check are checkable with zero network: download the self-contained copy from GET /bundle/offline-shell and it still verifies opened straight from file://. Authenticity (the COSE signature) isn't evaluated by this browser page yet — that stage renders an honest skip, never a fabricated pass — but is checkable with zero network too, via the CLI's permalink --check or scitt-cose installed locally. Witness needs a network by definition — it's a claim about a log someone else keeps — and when the anchor is unreachable the viewer renders that stage skipped, not failed. Full trust model, including exactly what a server does learn (a viewed capsule_id, and — until you've downloaded and diffed it — trust in served JS): docs/verification-trust-model.md.

Test vectors (cross-implementation, stable)

test-vectors/ is a frozen, append-only vector set for other SCITT/COSE implementations to check themselves against: two happy-path vectors (EdDSA, ES256) and three negative vectors (tampered inclusion path, unsupported vds, bad statement signature), each with committed bytes, TEST-ONLY keys, and an expected.json documenting every protected-header field a verifier needs. Verify from a clean checkout:

git clone https://github.com/action-state-group/scitt-cose
cd scitt-cose && pip install -e . && python -m scitt_cose.vectors

or from the published package (the runner ships in scitt-cose ≥ 0.1.0): pip install "scitt-cose>=0.1.0", download test-vectors/, then python -m scitt_cose.vectors path/to/test-vectors (add --json for a machine-readable report). The Go clean-room implementation runs the same manifest (go test ./... in scitt-cose-go-verify/). The append-only promise is enforced, not just stated: test-vectors/SHA256SUMS pins every published byte and CI fails on any mutation. A mismatch against your implementation is exactly the report we want — please open an issue. Details and the stability promise: test-vectors/README.md.

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scitt_cose-0.2.2.tar.gz (121.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scitt_cose-0.2.2-py3-none-any.whl (52.7 kB view details)

Uploaded Python 3

File details

Details for the file scitt_cose-0.2.2.tar.gz.

File metadata

  • Download URL: scitt_cose-0.2.2.tar.gz
  • Upload date:
  • Size: 121.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for scitt_cose-0.2.2.tar.gz
Algorithm Hash digest
SHA256 296832276b1b56f5b60a4da9c05fb6893d7b14617d9b82e38e345547e9bdaa01
MD5 57c11142b8947e713318e3e2c9d4865d
BLAKE2b-256 db3100ac2cd8572a17a382f6a073f8c8adf452c33782f691266ee81da7eeb523

See more details on using hashes here.

Provenance

The following attestation bundles were made for scitt_cose-0.2.2.tar.gz:

Publisher: release.yml on action-state-group/scitt-cose

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file scitt_cose-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: scitt_cose-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 52.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for scitt_cose-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 bf0de702db7a3605b1b0130670c7fbc5751b8d907f80f454f0d7d3dde51e8443
MD5 f4c7a0d2cfcb6f35d3f05bf5378333ec
BLAKE2b-256 f81c382ebfbf70c78b2cd8994037004d36d71f305cddad7df5a08263a3006b27

See more details on using hashes here.

Provenance

The following attestation bundles were made for scitt_cose-0.2.2-py3-none-any.whl:

Publisher: release.yml on action-state-group/scitt-cose

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.0

2 files

This release

0.2.2 This release

2 files

0.2.1

2 files

0.1.1

2 files

0.1.0

2 files

0.0.2

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page