Skip to main content

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-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 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 than RFC9162_SHA256 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).

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

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-22SCITT Architecture (Datatracker: Active Internet-Draft, RFC Ed Queue)
  • 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.

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


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.0.2.tar.gz (66.2 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.0.2-py3-none-any.whl (40.8 kB view details)

Uploaded Python 3

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

Hashes for scitt_cose-0.0.2.tar.gz
Algorithm Hash digest
SHA256 53bc7117a90b1f93e72a1dae784006dae4e6f5b11334361b365f6d1dc820b700
MD5 ca04c78bac4db76b92d7b6f5591fa856
BLAKE2b-256 36f8786bc2f4ffccef14aa953fa80a7e56b2735c97a38666d6340a7a400192b4

See more details on using hashes here.

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

Hashes for scitt_cose-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 dc5a387e78a5b7ef690c678d4c5eb20ffa5edcd4ba3f9ea64e9eba1def9efb4b
MD5 06ed300a45a12c796b385638f2d5b4dc
BLAKE2b-256 263fdf8ec4bfe359c2b8f54144687fd19e5f269f6a584aa0a0470900a8f9de62

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page