Skip to content

Latest commit

 

History

94 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CryptoValid Open Core

DOI

verify-evidence

Verifiable compliance evidence for internet services — free software.

This directory is the AGPL-3.0 carve-out of the OMEGA Ecosystem, decided by the author (Roberto Locatelli) on 2026-08-08.

Licensing architecture (dual license, honest and explicit)

Component License
this repository (cryptovalid-opencore) — evidence format spec, standalone verifier, and every deliverable funded by open-source grants AGPL-3.0-or-later (see LICENSE in this directory)
Everything else in the OMEGA Ecosystem repository BSL 1.1 (source-available; see repository root)

Copyright for both sides: Roberto Locatelli, 2026. The author licenses the contents of this directory under the GNU Affero General Public License v3.0 or later. Contributions to this repository are accepted under the same license.

Commercial license — for AGPL-averse enterprises. The AGPL-3.0 (esp. the §13 network clause) deliberately prevents cloud-stripping, but many corporate legal policies forbid AGPL to avoid copyleft contagion. If that is your case, a commercial license of this code is available from the author (roberto.locatelli.81@gmail.com): the same code, without the AGPL network/copyleft obligations. This is the standard open-core arrangement — AGPL for the community, a commercial license for enterprises that need it — so the copyleft protection is a positioning choice, not an adoption dead-end.

Patents / freedom-to-operate (honest — no professional FTO was done)

Stated plainly so no one relies on a guarantee that does not exist:

  • Measured (low patent exposure): the primitives here are open standards or public prior art — SHA-256/SHA3 (FIPS 180-4/202, royalty-free), Ed25519 (published patent-free by its authors), RFC 3161 timestamping and RFC 6962 Merkle trees (open IETF standards), and hash-of-document timestamping + hash-chaining, whose foundational patents (Haber–Stornetta, US 5,136,646/647) expired over a decade ago. There are no third-party dependencies, so no patent or licence travels in from an external library.
  • Not established (residual risk — needs a lawyer, not this file): a professional freedom-to-operate search has not been performed. Patent risk in software is rarely in the algorithms; it is in combinations and business methods (e.g. "verifying regulatory compliance using a blockchain anchor"), and it varies by jurisdiction — US software/business-method patents are broader than the EU (Art. 52 EPC). AGPL-3.0 §11 grants a patent licence only from this project's contributors; it does NOT shield you from a third party's patent.
  • Post-quantum layer (0.12.0 → 0.13.0), checked on 16/09/2026 on primary sources: ML-DSA is FIPS 204, a public NIST standard; NIST's submission rules required signed IP statements from every submitter, and NIST's two royalty-free patent licence agreements (a US portfolio and the CNRS / Université de Limoges portfolio) concern CRYSTALS-Kyber / ML-KEM, which this project does not use; no patent licence was needed for CRYSTALS-Dilithium / ML-DSA, whose submitters declared none. The hybrid here is two independent signatures (Ed25519 and ML-DSA-65 over the same bytes, separate keys, no combined key or OID): it does NOT implement the IETF LAMPS "composite signatures" draft (draft-ietf-lamps-pq-composite-sigs, RFC Editor queue, one IPR disclosure listed on the datatracker). AWS KMS is used through its public API under AWS's terms. The Java verifier uses only JDK APIs (GPLv2 + Classpath Exception runtime; this file is AGPL-3.0). None of this is an FTO opinion.
  • Your responsibility: this project is provided as-is, with no warranty of non-infringement. Anyone deploying it — especially commercially or in the United States — should conduct their own patent due diligence / FTO. The author has not, and this document is not legal advice.

What this is

A self-hosted toolkit that turns compliance activities into evidence anyone can re-execute:

  • append-only hash-chained ledgers (canonical JSON, SHA-256);
  • Ed25519-signed records (optionally hybrid Ed25519 + ML-DSA-65, FIPS 204, since 0.12.0);
  • RFC 3161 independent timestamps;
  • a standalone verifier (verifier.py, in this directory): one command, no server, no vendor — a third party replays the chain and reaches the same hashes, or the verification fails. Trust is replaced by verification.

Relevance to the Sustainable Development Goals

CryptoValid is submitted as a candidate Digital Public Good. Its relevance to the SDGs is institutional transparency and accountability, not (yet) financial inclusion:

  • SDG 16 — Peace, Justice and Strong Institutions. Verifiable, tamper-evident compliance evidence directly supports target 16.6 (effective, accountable and transparent institutions) and target 16.5 (reduce corruption): any third party can re-execute verifier.py and reach the same hashes, so an institution's records can be checked without trusting the institution — accountability by verification, not by assertion.

  • SDG 9 — Industry, Innovation and Infrastructure. It is open, dependency-free (dependencies = []) digital infrastructure for trustworthy record-keeping, usable by any organisation at no licence cost.

  • SDG 1 / 8 / 10 — NOT claimed. They were withdrawn from the DPG nomination on 2026-08-22 after the 4-mind review (see DPG_NOMINATION.md, «SOLO 16 + 9»): the microfinance module is a real capability, but a proof-of-integrity tool does not by itself advance poverty or inequality targets. What it does: microfinance.py applies the same primitives to microcredit transparency: a microfinance institution's loan portfolio is canonicalized (same portfolio → same digest, re-derivable by a donor or regulator), its internal consistency is attested (disbursed = outstanding + repaid + written-off), and standard Portfolio-at-Risk (PAR30/PAR90) is computed. On the borrower-protection side, over_indebtedness() detects over-indebtedness — a borrower carrying loans across multiple institutions (a debt-trap signal) — using a hashed borrower reference so exposure is aggregated without any PII.

Honest scope (no overclaim): the microfinance module is a real capability but is currently validated on synthetic loan data (unlike the fund side, which is validated on real SEC filings) — MFIs do not publish portfolios the way funds do, so real-data validation awaits an operator's data. The confine is unchanged: proof-of-integrity, not proof-of-veracity (it attests that records are internally consistent and tamper-evident, not that the loans are real or well-underwritten). No PII by design (see PRIVACY.md).

Where this fits (honest scope of the demand)

The broad compliance-automation market (evidence-collection dashboards) is owned by SaaS incumbents and is not the target — their model is trust the vendor's database. CryptoValid targets the narrower, regulation-driven case where evidence must survive without trusting any vendor:

  • EU AI Act high-risk systems must keep automatic logs (Art. 12) for ≥ 6 months (Art. 19); the high-risk obligations for Annex III systems apply from 2 December 2027 (deferred by the Digital Omnibus, Reg. (EU) 2026/1744 — the original 2 August 2026 date no longer holds; see spec/regulatory_profiles.json, which carries the dated source).
  • For records to be admissible in judicial or regulatory proceedings, each event should be timestamped with an eIDAS qualified timestamp and made immutable by a third party independent of both provider and deployer — cryptographic measures, not access controls.

That is exactly this format's shape: offline third-party verification (verifier.py), a qualified-TSP-ready Signed Tree Head (see Merkle proofs below), and tamper-evidence that fails loudly. Honest limits: this is an emerging segment (not proven revenue); the qualified timestamp needs a real QTSP integration; and general-purpose "crypto-ledger" demand is genuinely weak (cf. AWS retiring QLDB). The wedge is court-admissible AI/records evidence, not a database.

Status (honest)

Delivered and tested — the CI badge above is green on every push:

  • append-only hash-chained ledgers + a standalone stdlib verifier, defined by conformance test vectors (spec/vectors/) so any language can prove interoperability;
  • Ed25519-signed records (signer.py) and a signed, auditor-ready evidence pack (evidence_pack.py) that a third party verifies offline, vendor-free;
  • RFC 3161 timestamping — cryptographically verified against a real public TSA;
  • a self-updating regulatory profile (MiCA / EU AI Act / DORA / GDPR) that carries provenance and flags stale mappings for human review;
  • adversarially hardened: red-team passes found and closed real gaps (truncation and manifest re-forge at the signed pack layer, timestamp forgery, cross-language JSON canonicalisation; and on 2026-09-11, a bare-ledger verifier that answered PASS on an empty file) — each with a regression test. Know what verifier.py proves: a bare hash chain is internally consistent evidence — it cannot see truncation or a re-chained suffix written by someone with write access to the file. The receipt says so in a scope field. Since 0.11.0 the signed chain tip (cryptovalid_tip.py, sidecar <ledger>.tip.json, written after every append — O(1) for the writers that already know their tail, Ingestor and Go AppendSigned; cryptovalid_tip.py sign reads the file, O(n)) moves that limit: with the tip and the trusted log key, verifier.py --trusted-pubkey <hex> [--require-tip] names tail_truncated, tail_rewritten and unsealed_tail; the JS and Go verifiers check it too (Rust/Swift: declared no). Without the trusted log key the tip is not checked at all (tip_untrusted): the key inside a tip proves nothing, so there is no "PASS but untrusted" for an automation to misread. What remains, stated in the receipt: a holder of the log key can truncate and re-sign (key custody: HSM/KMS); a rollback to an older genuine tip passes unless you pass --tip-not-before or compare with the monitor state / receipts / an anchor; and when one log key signs several ledgers, a whole pair file+tip of another ledger is caught only with --expect-ledger-id (the tip carries the chain's identity, ledger_id = self_hash of entry 0). The production writer keeps the tip itself: Ingestor(tip_keyfile=…) signs it at every flush under its lock. Measured on the arc (#703) bench: 6002/6003 tamperings without the tip, 6003/6003 with it, and an attacker re-signing the tip with a key of their own is refused;
  • high-frequency ingestion (cryptovalid_ingest.py): segmented hash-chained ledgers with Merkle-STH sealing (chained across segments, KMS/HSM-signable), batched fsync, fail-closed crash recovery — throughput measured by the bench, never quoted as a fixed claim;
  • KMS/HSM signing backends (cryptovalid_kms.py): the Ed25519 private key can live in a PKCS#11 HSM (tested end-to-end against SoftHSM2; YubiHSM 2 exposes the same mechanism), AWS KMS (ECC_NIST_EDWARDS25519, exercised against the API contract — a live signature needs a real AWS account), or HashiCorp Vault Transit (protocol-tested against a stub) — instead of a local key file. Signatures stay standard Ed25519, so the stdlib verifier is unchanged.

Honest caveats, unchanged: no users yet; this is not an HSM itself (it talks to one — key custody is only as strong as the token/KMS policy behind it), not an accredited certification, and not legal advice — it proves what / when / order / who-signed + non-alteration, not the truth of the recorded facts. Broader packaging and an independent security audit are the object of a pending NLnet application.

Quick start

python3 verifier.py <ledger.jsonl>     # verifies a hash-chained ledger

Exit code 0 = chain intact; non-zero = the file tells you where it broke.

Signed evidence (the enterprise edge)

Hash-chaining proves a ledger was not altered. Signing proves who sealed it — and lets a third party verify authorship with nothing but a public key. This is what closed, cloud GRC evidence tools do not give you: their evidence lives in a vendor database ("trust us"); CryptoValid evidence is signed, self-hosted, and verifiable offline by anyone, forever.

python3 signer.py keygen  signer.key                          # Ed25519 keypair (seed, chmod 600)
python3 signer.py sign    ledger.jsonl  ledger.signed.jsonl  signer.key
python3 verifier.py       ledger.signed.jsonl                 # hash chain still PASS (stdlib-only)
python3 signer.py verify  ledger.signed.jsonl                 # signatures PASS (content->self_hash->signature)
  • The signature commits to each entry's self_hash; signature/signer (and the hybrid signature_pq/ signer_pq) are attestation fields excluded from the content hash, so a signed ledger still passes the stdlib hash verifier unchanged — in all five verifiers.
  • Hybrid post-quantum (0.12.0, 0.13.0): keygen signer.key --pq also writes signer.key.pq (ML-DSA-65, FIPS 204, PKCS#8, 0600); sign … --pq-key signer.key.pq — or --pq-kms <AWS KMS key, KeySpec ML_DSA_65> to sign inside KMS — signs every self_hash with BOTH keys (re-signing without a PQ key strips stale PQ fields); verify … --pq-pubkey <b64> requires the layer and exits 1 unless every entry carries a valid ML-DSA-65 signature by THAT key (--pq-pubkey needs --pubkey, and --require-pq alone is refused: a layer checked against the key inside the file is self-declared). The tip: --trusted-pq-pubkey on verifier.py and on the Go cvverify (implies a required tip and needs --trusted-pubkey). Per-entry Ed25519 + ML-DSA-65 are verified by Python (signer.py verify) and Go (cvverify -pubkey <hex> -pq-pubkey <b64>); the tip's by both. The context string is EMPTY (FIPS 204 default) so the JDK, OpenSSL 3.5, .NET and Go verify the same bytes. Needs cryptography ≥ 50 (Python) or Go ≥ 1.27; where a REQUIRED layer cannot be verified the result is pq_unverifiable with ok: false / exit 1, never a green.
  • signer verify re-derives self_hash from the content too, so it catches content tampering on its own: the full chain is content → self_hash → signature.
  • Optional layer, honest scope: the core hash verifier stays stdlib-only; signatures need the cryptography package. Absence of signatures never weakens the hash chain. By default keys are software keys on a file — production should use a KMS/HSM backend (below). Tests: python3 test_signer.py.

KMS/HSM key custody (no private key in process memory)

cryptovalid_kms.py delegates the signature to a backend where the key is non-exportable; the evidence format and the verifier do not change. URI-style selection:

# PKCS#11 HSM (YubiHSM 2, SoftHSM2, smartcard) — PIN via env, never on argv
export CRYPTOVALID_PIN=****
python3 cryptovalid_kms.py keygen-hsm --backend \
  "pkcs11:module=/usr/lib/softhsm/libsofthsm2.so;token=cryptovalid;key=evidence;pin=env:CRYPTOVALID_PIN"
python3 signer.py sign ledger.jsonl ledger.signed.jsonl --backend "pkcs11:module=...;token=...;key=evidence"

# AWS KMS (KeySpec ECC_NIST_EDWARDS25519, EdDSA supported since 2025-11; needs boto3+credentials)
python3 signer.py sign ledger.jsonl out.jsonl --backend "awskms:key_id=alias/cryptovalid;region=eu-south-1"

# HashiCorp Vault Transit (key type ed25519; token from $VAULT_TOKEN; stdlib-only client)
python3 signer.py sign ledger.jsonl out.jsonl --backend "vault:url=https://vault:8200;key=cryptovalid"

Honest bench per backend (test_kms.py): PKCS#11 is tested end-to-end against a real SoftHSM2 token (non-exportable key, tamper ⇒ FAIL); AWS KMS is exercised against the exact API contract (MessageType RAW + ED25519_SHA_512) with an injected client — a live signature requires a real account; Vault Transit is protocol-tested against a local stub. Removing the key from process memory does not protect against a compromised host asking the HSM to sign attacker-chosen data — pair it with KMS policies/audit and HSM touch-policies.

Auditor-ready evidence pack

Assembling evidence for an auditor is the regtech time-sink. evidence_pack.py turns one or more ledgers into a self-verifying bundle (MANIFEST with per-file SHA-256 + per-ledger hash/signature verdicts + signer keys + an optional RFC 3161 timestamp, a human SUMMARY.md, and the ledgers). A third party re-checks everything with nothing but this repository:

python3 evidence_pack.py build  pack_dir/  ledger.signed.jsonl  --subject "audit CUST-001"
python3 evidence_pack.py verify pack_dir/

verify returns valid: true only if every file digest matches the manifest, the manifest is self-consistent, and every ledger passes its hash chain and (if signed) its signatures — no server, no account, no vendor. Tamper any file and it drops to valid: false. RFC 3161 anchoring is optional (needs openssl + a TSA); its absence never invalidates the pack. Tests: python3 test_evidence_pack.py.

This is the difference from closed GRC evidence tools: your evidence is a bundle anyone can re-execute to verify — forever, offline, without trusting us.

Build a pack with --sign-key <keyfile> to authenticate the manifest (recommended): it binds the subject, the file digests, and each ledger's entry count + head hash. verify_pack reports manifest_authenticated.

Dispute evidence for agentic-payment mandates (AP2 / SD-JWT)

The AP2 spec tells implementers what to keep for dispute resolution (the SD-JWTs with their disclosures) but not how — no key snapshotting, no tamper-evidence, no long-term validation. ap2_evidence.py turns a set of SD-JWT mandates into one self-contained evidence file that verifies offline years later: it validates each ES256 signature at build time, snapshots the key material with an explicit provenance class (x5c_header / jwk_header / supplied / jwks_fetched — declared, never dressed up), recomputes cross-mandate hash bindings from the exact compact serializations, and seals everything under a SHA-256 digest with an optional RFC 3161 timestamp:

python3 ap2_evidence.py build ev.json intent=intent.sdjwt cart=cart.sdjwt --tsa http://tsa.izenpe.com
python3 ap2_evidence.py verify ev.json     # offline, fail-closed

Honest scope: proves these exact artifacts verified with this key material at build time (and existed at the TSA's time, if stamped). It does not confer eIDAS art. 45j qualified-archive legal presumption and does not validate x5c chains to a trust anchor. ES256 only, loudly. Tests: python3 test_ap2_evidence.py. Also exposed read-only as the MCP tool verify_ap2_evidence.

Auditor-facing report (PDF/HTML)

An ISO inspector or an EU regulator expects a formal document, not JSON on a terminal. cryptovalid_report.py renders an evidence pack into a typographic audit report — front page, global status, per-ledger detail, signer keys, RFC 3161 / eIDAS anchoring, honest scope, and the exact vendor-free re-verify command:

python3 cryptovalid_report.py pack_dir/                      # -> report.html + report.pdf
python3 cryptovalid_report.py pack_dir/ --lotl --lotl-ms ES  # opt-in eIDAS check (network)

Design rules (they ARE the security model of this layer): the report never recomputes a verdict — every light comes verbatim from the same fail-closed verify_pack() an auditor runs by hand, so a tampered pack renders RED, always. The report is a rendering, not evidence: the authoritative artifacts remain MANIFEST.json + the ledgers, and the document says so on its face. HTML needs nothing but the Python stdlib; PDF uses WeasyPrint only if it is already installed (honest degrade to HTML otherwise). The eIDAS light has three honest states — not checked (default) / qualified / not qualified — never a fabricated green. Tests: python3 test_report.py (tamper→RED proven before the positive path).

MCP server — agents that can prove what they did

cryptovalid_mcp.py exposes CryptoValid to any MCP client (Claude Code, Claude Desktop, other agents) over stdio — zero dependencies: the MCP JSON-RPC transport is implemented with the Python stdlib, same ethos as the rest of this repo.

// client config
{"command": "python3", "args": ["/path/to/cryptovalid_mcp.py"]}

Read-only tools, always available, every answer carries provenance (source, SHA-256, UTC): verify_ledger, verify_pack, verify_archive (with optional trusted-signer set and opt-in eIDAS/LOTL check). Write tools — append_event, seal_segment — let an agent seal its own actions into a tamper-evident, signed, RFC 3161-anchorable archive that anyone re-verifies offline; they are human-gated twice (env CRYPTOVALID_MCP_ALLOW_WRITE=1 and a per-call confirm_token matching CRYPTOVALID_MCP_CONFIRM) and disabled by default. Signing uses the same backend URIs as everywhere else (file: / pkcs11: / awskms: / vault: / nethsm:) — with the HSM/KMS backends the private key never enters the server process.

Tests: python3 test_mcp.py — over the real stdio transport, gate refusals and tampered-ledger failure proven before the positive path. Honest scope: sealing proves what/when/order/who-signed — never the truth of the recorded facts.

DORA ICT-incident evidence (EU, evidence-driven enforcement)

Under EU DORA, supervisors moved to evidence-driven enforcement in 2026: they want timestamps, classification rationales and log trails — not policy PDFs. Major ICT incidents follow a hard reporting lifecycle: initial notification within 4h of major classification (24h backstop), intermediate within 72h, final within 1 month.

dora_incident.py makes that lifecycle tamper-evident and reproducible: each phase (detected → classified → initial/intermediate/final) is a canonical, hash-chained record, and the DORA deadlines are checked against the recorded timestamps — a regulator recomputes the same verdicts from the records alone, then re-verifies with the unified verifier. Seal the chain head with a qualified eIDAS timestamp (cryptovalid_tsa) for legal-grade time. python3 dora_incident.py attest phases.json --incident-id INC-1 / verify att.json phases.json.

Honest scope (the boundary never moves): proof-of-integrity + timeline + deadline-check, NOT a DORA compliance certificate and NOT proof that the major/significant classification is correct (the entity's judgement, reviewed by its auditor). Complements web-evidence anchoring tools by covering the part they leave open: the incident lifecycle and its deadlines.

Transaction evidence for tax/audit (IRS Form 1099-DA era)

From 2026 US brokers must report crypto cost basis (Form 1099-DA) — but NOT for assets acquired before 2026, nor for non-custodial/DeFi activity: the taxpayer must track and, on audit, prove their own transactions. Tax software computes the numbers; it does not give evidence that survives an audit without trusting the software.

tx_evidence.py fills exactly that gap: it canonicalizes each transaction deterministically, hash-chains the set (tamper-evident), and re-derives the cost basis with a declared, deterministic FIFO method — so a third party (an auditor, the IRS) recomputes the same numbers from the same records. python3 tx_evidence.py attest transactions.json / verify attestation.json transactions.json.

Honest scope (the boundary never moves): proof-of-integrity + proof-of-reproducibility, NOT tax advice and NOT proof of tax-correctness — it does not pick your accounting method, apply wash-sale/jurisdiction rules, or certify compliance. Those remain the taxpayer's and their advisor's.

Long-term evidence defenders (WORM · re-anchor · heterogeneous anchors)

Three capabilities that keep evidence sound over years, each a runnable tool now:

  • cryptovalid_worm.py — WORM escrow of the record (not just its hash): append-only, overwrite refused, GDPR crypto-shredding of the key. Meets immutable-retention rules (SEC Rule 17a-4, MiCA 5–7 years). python3 cryptovalid_worm.py put <store> <key> <file>.
  • cryptovalid_reanchor.py — public-RPC anchors lose witnesses as nodes prune history (an "auto-DoS" of the evidence, measured). This assesses decay and flags when to re-anchor before the evidence becomes unreachable. python3 cryptovalid_reanchor.py <retention.json> --age-days N. Run it on a timer; re-anchoring itself is an on-chain tx and stays human-gated.
  • cryptovalid_heterogeneous.py — real fault-independence: N distinct trust domains (Solana + Bitcoin/OTS + eIDAS QTSP), not replicas of one chain. Same-domain replicas count once. python3 cryptovalid_heterogeneous.py <sha3_hex> <attestations.json> --min-domains 2.

Honest scope: these harden availability, retention and robustness of the evidence — not the truth of its content (that boundary never moves).

Threat model (honest — hardened after adversarial review)

Adversarial testing (NEMESIS + an independent LLM red-team) found and CLOSED real gaps:

  • Truncation / branching: a bare hash chain does not prevent dropping trailing entries or forking a new history after any point. A signed manifest commits each ledger's entry count and head hash, so verify_pack detects truncation. Bare ledgers still need a signed manifest (or an external anchor) for this — documented, not hidden.
  • Manifest re-forge: an unsigned manifest is integrity-checked, not authenticated — its metadata is trustworthy only if manifest_authenticated is true. A tampered signed manifest → valid: false.
  • RFC 3161: the timestamp token is now cryptographically verified (status Granted + message imprint == manifest digest), proved against a real DigiCert TSA. A forged token → rfc3161.verified: false.
  • Cross-language integrity: canonicalisation is pinned (ASCII-escaped, no floats, unique keys — see §3 of the spec) with a dedicated conformance vector, so a Go/Rust/JS verifier computes the same hashes.
  • Replay across ledgers: carry a ledger_id in data (see spec §3) to bind entries to one chain.

Declared limits (from a further independent adversarial review, 2026-08-17 — stated, not hidden):

  • Split-view / equivocation — declared "future work" here from 2026-08-17 to 2026-09-19; since 0.14.0 it is a measured feature, see Checkpoints, witnesses, split-view below: a signer-key holder can still write two internally valid chains, but a checkpoint of either is cosigned by a witness only if it extends what that witness already cosigned, so a witness that has cosigned one view refuses the other — and, when the two views have the same size, returns a pair of log-signed checkpoints that is proof by itself. What remains: a log that feeds DISJOINT subsets of the trusted witnesses different views gets both cosigned without any refusal whenever a relying party's quorum N fits inside one subset (M trusted witnesses, M ≥ 2N) — which is why witness policies fix the set and demand N > M/2, and why the monitoring side of the protocol (not implemented here) exists; a log colluding with enough witnesses to reach the quorum; a relying party that trusts no witness is where it was.
  • Rollback / freshness — the same section: the witness cosignature carries a timestamp inside the signed bytes, and verify_witnessed(min_witnesses=N, max_age_s=T) requires N trusted witnesses each with a cosignature younger than T on this exact checkpoint. A witness that has cosigned a newer tree never cosigns the older one again, so a stale view stays acceptable only for as long as the log can keep N witnesses from seeing the newer tree. What remains: the witnesses' clocks are the reference (not the log's), a stale view younger than T passes, and a log that starves N witnesses of its newer checkpoints keeps them re-cosigning the old one (the spec allows re-cosigning the same checkpoint). Without a witness, the anti-truncation HEAD still protects only relative to a known HEAD and TSA anchors give no freshness.
  • Pre-anchor window: between an event and its sealed anchor, a root-level attacker can rewrite and re-seal silently. The anchor proves "existed by T"; the per-entry timestamp comes from the local clock and is not independently attested.
  • Selective omission: an append-only log proves what it contains, never the completeness of what should have been recorded.
  • Signing-channel governance: "non-exportable key" ≠ "custody service incompressible": whoever controls the KMS/HSM policy or unseal material can authorize signatures. Policy governance, rotation and revocation are part of the security perimeter.
  • Post-quantum: hybrid, opt-in, pinned. Since 0.12.0 ledger entries and the signed chain tip MAY carry an ML-DSA-65 (FIPS 204, pure mode, empty context since 0.13.0 so that the JDK 24+ and AWS KMS can produce and verify it) signature next to Ed25519 over the same bytes (signer.py keygen --pq, sign --pq-key, cryptovalid_tip.py sign --pq-key). The layer counts only when the relying party PINS the ML-DSA-65 key (--pq-pubkey, --trusted-pq-pubkey, both of which REQUIRE the layer): then a missing, foreign, malformed or invalid PQ signature is a FAIL, and pq_protected: true also needs every Ed25519 signature to hold. Without the pinned key a present layer is reported as null (verified against the key inside the file, which anyone can put there), never true; an Ed25519-only ledger is still accepted, so stripping the PQ fields is a downgrade the relying party's requirement refuses, not the file. Both keys must be pinned (the library and the CLI refuse a PQ key without the Ed25519 key): with only the PQ key required, a re-signed Ed25519 layer by a foreign key would still verify. Who checks what: per-entry Ed25519 + ML-DSA-65 — Python and Go (cvverify -pubkey … -pq-pubkey …, crypto/mldsa, Go ≥ 1.27); the tip's ML-DSA-65 — Python and Go; JS verifies the hash chain and per-entry Ed25519, Rust and Swift the hash chain only, and all three say the PQ layer is unchecked. The evidence pack is not quantum-resistant on its own: its manifest records pq_protected per ledger with the keys it was built against, but the manifest signature is Ed25519 only, so verify_pack confirms the layer only against keys the verifier pins (the ledger keys, or the manifest signer); with nothing pinned it reports null, never true. The PQ private key is a file (PKCS#8, 0600) or, since 0.13.0, an AWS KMS ML-DSA-65 key (--pq-kms: signed in a FIPS 140-3 Level 3 HSM, the key never in process memory — verified live). Cost: about 4.4 KB of base64 signature and 2.6 KB of base64 public key per entry. Already-published TSA anchors (RSA/ECDSA) do not survive a quantum adversary either, which keeps hash-only anchor coverage part of the security perimeter.

Honest scope unchanged: the format proves what/when/order/who-signed + non-alteration, not the truth of the recorded facts, and is not an HSM or a legal-compliance certification.

An open standard (not just a tool)

A verifiable-evidence feature is copyable; an adopted format is not. CryptoValid is defined by conformance test vectors (spec/vectors/ + spec/CONFORMANCE.md), so a verifier in any language proves interoperability by reproducing the same verdicts:

python3 conformance.py            # exit 0 = the reference verifier conforms (7/7 vectors)

Implement it in Go/Rust/JS, match the vectors, and open a PR to the conformance table — the format becomes a shared standard, not one vendor's tool. Full spec: SPEC_EVIDENCE_FORMAT.md.

Merkle proofs (optional extension, RFC 6962)

cryptovalid_merkle.py builds an RFC 6962 Merkle tree over the same canonical entries, adding what a linear chain cannot do efficiently:

  • inclusion proofs — verify one entry in O(log n) without recomputing the whole chain;
  • consistency proofs — cryptographic proof that a newer ledger append-only extends an older one;
  • a Signed Tree Head ({tree_size, root_sha256}) — submit root_sha256 to a qualified TSP (eIDAS EU Trusted List) for a qualified RFC 3161 timestamp (the admissibility requirement above).
python3 cryptovalid_merkle.py sth    examples/sample_ledger.jsonl   # {tree_size, root_sha256}
python3 cryptovalid_merkle.py prove  examples/sample_ledger.jsonl 1 # audit path for entry 1
python3 cryptovalid_merkle.py verify examples/sample_ledger.jsonl 1 # INCLUSION VALID

A working RFC 3161 client (cryptovalid_tsa.py) requests a timestamp over the STH root from any TSA — point --tsa at an eIDAS qualified TSP for a qualified token, then cryptovalid_lotl.py certifies it is qualified by matching its TSA certificate against the EU List of Trusted Lists (ETSI TS 119 612, service type TSA/QTST) — validated end-to-end LIVE (2026-08-16): a real timestamp from a real qualified TSP (Izenpe, Spanish Trusted List) over an ingestion HEAD hash returned qualified: true, while two non-qualified TSAs (freetsa, and Certum's public TSA — correctly distinguished from the qualified services of the same company listed in the Polish TL) were rejected. Honest scope: qualified: true means the TSA certificate is a QTST-granted service in the EU Trusted Lists at verification time; full ETSI TS 119 615 legal validation (chains, revocation, time constraints) is beyond exact-fingerprint matching, and production use still wants a contracted QTSP with an SLA. The linear hash-chain stays the interoperable core; Merkle is additive. Domain separation: 0x00 leaves, 0x01 nodes. Third-party verification needs only (entry, index, tree_size, audit_path, root). Tests: python3 test_merkle.py.

Java verifier (JDK standard library, 2026-09-16)

verifiers/java/CvVerify.java is a single-file, dependency-free Java implementation of the verifier: the strict JSON acceptance profile, the canonical hash chain (SHA-256 / SHA3-256), per-entry Ed25519 (JDK 15+) and ML-DSA-65 (JDK 24+, Signature.getInstance("ML-DSA-65")) signatures with the same tri-state as signer.py, and the signed chain tip including its post-quantum layer. It is possible only because the profile signs with the EMPTY FIPS 204 context: the JDK (24 → 27) exposes ML-DSA with no context API. It sits in the differential oracle next to Python, JS, Go and Rust (verdicts, tip tri-state and entry signatures compared on every hostile input) and in CI on JDK 27. Run: java verifiers/java/CvVerify.java ledger.jsonl [-pubkey HEX -pq-pubkey B64] [-tip F -trusted-pubkey HEX -trusted-pq-pubkey B64]. Measured on a ledger and tip whose ML-DSA-65 signatures were made inside AWS KMS: PASS, entries and tip pq_protected: true.

Go reference: writer + verifier (2026-09-14)

verifiers/go/ is a stdlib-only Go implementation of the profile — the first one that also writes (cryptovalid.Append: canonical JSON, prev_hash, SHA-256, O(1) per append, flock from tail-read to fsync) so a Go service can produce a cryptovalid ledger at insert time and anyone can verify it with the Python, JS or Go verifier. vectors.json (generated by Python) is the single byte-level oracle for the three languages; on a 200-row audit-log ledger and 6003 enumerated tampers the three verifiers give identical verdicts. Two profile rules made explicit the same day in all three: nesting > 512 and unpaired UTF-16 surrogates are refused fail-closed (spec/CONFORMANCE.md). Since 0.12.0 cvverify -trusted-pq-pubkey <b64> also checks the ML-DSA-65 signature of a hybrid tip, and since 0.13.0 -pubkey <hex> -pq-pubkey <b64> verify every entry's Ed25519 + ML-DSA-65 signatures with the standard library (crypto/mldsa, Go ≥ 1.27, the same tri-state as signer.py); a binary built with an older toolchain answers pq_unverifiable (a FAIL when the key is required), never a silent pass.

Receipts, monitor, eIDAS 2.0 self-assessment, JWS (2026-09-13)

Compared online with the field (Sigstore Rekor v2, immudb, Azure Confidential Ledger, Google Trillian/Tessera, the IETF SCITT architecture) and with the new EU rules for qualified electronic ledgers (eIDAS 2.0, Regulation (EU) 2024/1183 Art. 45l; Commission Implementing Regulation (EU) 2025/2531 of 16 December 2025), four pieces were missing and are now here, stdlib-only:

Module What it adds Standard
cryptovalid_receipt.py Portable receipts: inclusion and consistency proofs for one entry / two tree heads, with a tree head really signed by the log key (the old signed_tree_head in cryptovalid_merkle carried no signature — renamed tree_head, alias kept). JSON profile and COSE_Sign1 encoding per RFC 9942 (vds=1 RFC9162_SHA256, vdp with inclusion −1 / consistency −2 as arrays of bstr, alg EdDSA; consistency receipts carry a detached payload and the verifier recomputes root_2); minimal CBOR codec (duplicate keys and deep nesting refused). Verification is fail-closed: it needs your entry (inclusion) or your previous root (consistency) and a trusted log key — nothing named inside the receipt is trusted, and the proof's tree size must equal the signed head's. RFC 9942, RFC 9162, RFC 9052, RFC 8949
cryptovalid_tip.py Signed chain tip (0.11.0): {entries, tip_sha256, ts} signed with the log key after every append (O(1), atomic sidecar <ledger>.tip.json; Go AppendSigned / cvappend -tipkey writes it under the same lock). Any snapshot verifier holding the tip and the trusted key sees tail truncation, suffix rewrite and unsealed appends — the three attacks a bare chain cannot see. Same signed bytes in Python, JS and Go (cross-signed in the tests). Limit: the holder of the log key; deleting the tip is visible only with --require-tip or a copy kept elsewhere (monitor state, receipts, anchor). signed tree-head idea of RFC 6962, reduced to the chain tip
cryptovalid_monitor.py Append-only / non-equivocation monitor (what rekor-monitor and immudb's auditor do): keeps the last green tree head and proves at every run that the ledger only grew; catches truncation, rewrite of old entries, forks and log-key changes; a red run never advances the baseline; the saved state is itself verified on load (signed head). Three modes, stated: writer (--keyfile, signs the head), auditor (--trusted-pubkey, never writes an unsigned state, advances only with a signed head published by the writer via --sth-file), none (blind trust, declared). Declared limits: the blind window between two runs, and deletion/replay of the state file (keep it where the ledger writer cannot write, or anchor each signed head externally). RFC 6962 consistency proofs
eidas_ledger_check.py Self-assessment of a ledger against IR 2025/2531 (REQ-7.5-03/04/05/06, Art. 45l): what is met by construction (hash list + Merkle, SHA-256/SHA3-256, immediate detectability), what needs the deployment's qualified pieces (qualified certificates, QTSP timestamps, certified signing device), what no code can give (being a QTSP). Produces the automated ledger report (Annex §2) and an Electronic Ledger Practice Statement skeleton (REQ-6.1-12) filled from what was measured. Never a claim of qualification. eIDAS 2.0, IR 2025/2531, ETSI EN 319 401
cryptovalid_jws.py JWS container (RFC 7515, EdDSA per RFC 8037) of a record's canonical form with kid and optional x5c — the header the regulation requires for JAdES; the qualified certificate is the user's, the toolkit puts it in the right place. Not a full JAdES profile (stated). RFC 7515, RFC 8037, ETSI TS 119 182-1 (target)
python3 signer.py keygen log.key
python3 cryptovalid_receipt.py inclusion examples/sample_ledger.jsonl 2 log.key --cose r.cose > r.json
python3 cryptovalid_receipt.py verify r.json <log_pubkey_hex> --entry-json my_entry.json   # offline: YOUR entry + trusted key
python3 cryptovalid_monitor.py examples/sample_ledger.jsonl state.json --keyfile log.key   # run it on a schedule
python3 eidas_ledger_check.py examples/sample_ledger.jsonl --practice-statement --provider "ACME"
python3 cryptovalid_jws.py sign examples/sample_ledger.jsonl 0 log.key --x5c <base64 DER cert>
python3 test_ledger_evidence.py                                         # 18 tests, negatives first

Honest scope, once more: a receipt signed by the log key proves what the log key holder published; a qualified electronic ledger needs a qualified trust service provider, qualified certificates/timestamps and certified devices — eidas_ledger_check.py tells you exactly which of those you still owe.

Vendored key-window vectors restored to the upstream bytes (2026-09-26, 0.16.2)

  • The vendored key-window vectors are now the upstream bytes. examples/acta/key-window/SOURCE.md said the eight files were "copied verbatim" from ScopeBlind/agent-governance-testvectors at 1e24b5687. Compared byte for byte with that commit on 2026-09-26, each copy shipped in 0.16.0 and 0.16.1 lacked the file's final newline (one byte); the JSON content was identical, so no verdict changes: 6/6 on verdict, code and key_status, both before and after. The files now carry the upstream bytes and KW_SHA256 pins them; removing the final newline from one file makes the pin test fail. The re-fetch recipe in SOURCE.md is replaced: the previous command does not run (gh api 2.92.0 rejects --ref as an unknown flag, exit 1). The new recipe, run on 2026-09-26, reproduces the eight files byte for byte.

OpenTimestamps anchoring in a public install (2026-09-26, 0.16.1)

  • committed_attestation.anchor_commitment works in a public install. From its introduction (0.9.0) through 0.16.0 it imported a module that is not part of this repository. 0.16.0 installed from the index in a clean virtualenv, run with an isolated Python (-I), answers ok: false, "No module named 'core'" (measured 2026-09-26). It now uses the public OpenTimestamps library (python-opentimestamps, LGPL-3.0) as an optional dependency: pip install cryptovalid-opencore[ots]. Without it the answer is ok: false with an error that names the missing library; with no calendar reachable it is ok: false, status: "failed", and errors says per calendar what went wrong; a root that is not 32 bytes is ok: false with an error. The return value keeps its fields; errors is new, and a new calendars argument replaces the library's default calendars (an empty list means no calendar). Measured 2026-09-26, with this code built as a wheel and installed with [ots] and cryptography in a clean virtualenv, isolated Python: the function committed on 4/4 calendars — https://a.pool.opentimestamps.org, https://b.pool.opentimestamps.org, https://a.pool.eternitywall.com, https://ots.btc.catallaxy.com. In the same install, the test file run against the installed module passed 14/14 with the live test enabled; that test decodes each returned proof as an OpenTimestamps Timestamp rooted at the commitment hash (the message is an input to the decoder, not carried by the proof) and checks that it holds at least one attestation and that every attestation is pending (not yet Bitcoin-confirmed). Upgrading a proof to a confirmed one is not part of this function. Tests: two new offline ones — one without the library, one with a stand-in calendar that has three entries: committed, unreachable, and answering for another message (a defensive guard: with python-opentimestamps 0.4.5 the message is set from the digest we pass, so it cannot trip there) — and one live test, reported as skipped unless CV_ONLINE_OTS=1. Removing the message guard or the per-calendar error handling turns the offline tests red.
  • test_cryptovalid_acta.py closes the PEM/DER key files it writes (a ResourceWarning in the 0.16.0 CI log).
  • A source comment in cryptovalid_acta.py said Arabic-Indic and fullwidth digits were accepted "up to 0.16.0"; 0.16.0 refuses them, and they were accepted by 0.15.1 and by the untagged 8de0b72 (measured 2026-09-26).

Key status, refusal codes and strict inputs (2026-09-26, 0.16.0)

Code: cryptovalid_acta.py and its tests; the other modules are unchanged since 0.15.1 (the README, the packaging metadata and the vendored key-window vectors also change). The public master already carried part of this work as 8de0b72 (25 September, version string 0.16.0, never tagged or released); this is the release. The "before" figures below are measured on 2026-09-26, unless dated otherwise, on that commit and on the 0.15.1 tag, with the author's probe scripts (not part of this repository). This release also removes .nocrypto_test/, a test virtualenv skeleton (activation scripts, interpreter and lib symlinks and a pyvenv.cfg naming a local path; no packages) that the repository had carried by mistake since the 0.14.0 release commit (c12d847, 19 September). Tests: test_cryptovalid_acta.py 51/51 with cedarpy and cryptography installed (one test is skipped without cedarpy).

Key status is always stated (§5.5). Every verification outcome — the library's result, and the CLI's output on exit 0, 1 or 77 — carries key_status and a machine-readable code; a refusal at load (exit 2: an unusable key set, key file or input file) reports only ok: false and error. key_status is one of not_reached (the receipt was refused before the key set was consulted — the default, because saying anything else would assert what was not measured), unknown_key, no_window, inside, outside, undecidable. code is null or one of key_outside_validity_window, key_window_undecidable, key_not_supplied, issuer_not_trusted, key_material_invalid, signature_invalid (the signature does not verify under the key) and signature_in_signing_input (a payload that carries a signature member, §6.6); every other refusal — a malformed envelope, a sig that is not lowercase hex — carries its reason in why with code null. On 8de0b72 a signature with one bit flipped and a payload carrying signature both came back with code null.

Small-order keys. A relying-party Ed25519 key that is a point of small order is unusable key material: with such a key anyone can forge a signature on any message. An invalid or non-canonical encoding (y >= p, or the sign bit set with x = 0) is refused too, because it does not name one point unambiguously; no forgery is claimed for it. With the identity as key, R=identity and S=0 verifies on every message. With a small-order key of order n (2, 4 or 8), pick S, guess g = k mod n, set R = [S]B − [g]A and retry until k = H(R‖A‖M) matches the guess: each try succeeds with probability about 1/n. Measured 2026-09-26 with the probe's own curve arithmetic and OpenSSL (Python cryptography) as the verifier: a signature on a message of the forger's choice verified for all 8 small-order keys, after 1 to 16 tries; the same construction against an ordinary key verified 0 times in 2000 (null control). Measured 2026-09-25: a receipt forged with R=identity, S=0 against the identity key passed. --key, a key file and --jwks refuse such a key at load (exit 2); the library returns not_assessed with code: key_material_invalid. The check is curve arithmetic (canonical decoding, then [8]P is not the identity). Measured 2026-09-26 on the check itself: the 8 distinct small-order points (computed with the probe's own curve arithmetic, not the library's, as the torsion part of random points), the 2 encodings with the sign bit on x = 0 and 4 encodings with y >= p are all refused, and an ordinary key is accepted; through the CLI, the identity key is refused at load given as a --key hex argument, as a key file (hex, PEM or DER) and inside --jwks (exit 2), and the library returns not_assessed / key_material_invalid.

The relying party's keys. verify --jwks <file> reads an Ed25519 key set with its valid_from/valid_until, through the same strict JSON parser as the receipts (duplicate member names, NaN and nesting deeper than 512 refused). A key held for another purpose is ignored and named on stderr: a kty other than OKP, a crv other than Ed25519, an alg other than EdDSA or Ed25519, a use other than sig, and key_ops without verify (RFC 7517 §5 asks implementations to ignore keys whose kty they do not understand or whose values are outside the supported ranges). Refused — exit 2, never resolved by order or skipped — are a kid that appears twice (in the set, or again through --key), a JWK with x but without kty or crv, an x that is not the canonical unpadded base64url encoding of 32 bytes, and a valid_from or valid_until that is not an RFC 3339 instant. On a JWK with x but without kty or crv, or with an x that does not decode to 32 bytes, this is stricter than RFC 7517 §5, which would ignore a key missing a required member or with a value out of range: such an entry is more likely a defect in the relying party's own list than a foreign key, and ignoring it would turn a configuration error into "key not supplied". On 8de0b72 a duplicate kid and a JWK without kty both verified the receipt (exit 0).

--key kid=<hex|file> accepts 64 hex digits in either case, and a key file in SubjectPublicKeyInfo form, PEM or DER; it refuses at load key material it cannot read as a key (an empty file, a malformed argument). On 8de0b72 a valid Ed25519 key file, PEM or DER, made a valid receipt FAIL (exit 1, "EdDSA needs a 32-byte key and a 64-byte sig…").

Instants. Dates are read in ASCII digits only (RFC 3339 §5.6, ABNF DIGIT): on 0.15.1 and on 8de0b72 an issued_at written in Arabic-Indic digits was accepted, because Python's \d matches every Unicode decimal digit. A leap second is accepted only when it is 23:59:60 once the offset is applied (RFC 3339 §5.7; 00:59:60+01:00 is accepted); §5.7 also limits it to the end of a month in which a leap second occurs, which is not checked here — 23:59:60 UTC is accepted on any day. It sorts after every 23:59:59.x and before the next second (the comparison is exact by construction; measured with an 18-digit .999…); 0.15.1 and 8de0b72 accepted :60 at another minute (12:00:60Z, measured). Window comparisons use every fractional digit: on 8de0b72 an issued_at 100 ns before valid_from compared equal to it (the comparison had microsecond resolution) and the receipt passed as inside; it is now outside. Through the CLI, a receipt whose issued_at is one unit in the last digit before valid_from is outside, and inside when it equals valid_from; measured in two runs, one where issued_at and valid_from both carry 20 fractional digits and one where both carry 5000.

Every file is read the same way. Receipts, --previous, --jwks, key files and policy files must be regular files of at most 64 MiB, opened without blocking and judged on the open descriptor: a FIFO, a device or a directory is refused at once (exit 2). On 8de0b72 a FIFO given as the receipt blocked the verifier until the probe's 10 s timeout, and a symlink to /dev/zero given as the receipt or as --jwks was read until the process died of a SIGKILL sent from outside the probe, before its 20 s timeout ran out (sender not identified). A receipt can still come from standard input through a redirect (verify /dev/stdin < r.json); a pipe is a FIFO and is refused.

Key validity windows (§9.2 of -03). --keys-are-complete declares the key set to be the whole trust list. The proposed -04 text of the draft (VeritasActa/drafts#3; the IETF datatracker still lists -03 on 2026-09-26) requires the window to be applied to issued_at and the output to say which case applied, "so that a verifier that does not check windows cannot be mistaken for one whose check passed". Measured against the six key-window vectors of ScopeBlind/agent-governance-testvectors (merged to main in 1e24b5687, vendored under examples/acta/key-window/), scoring all three columns the bench states — verdict, code and key_status — through the CLI with --jwks: 6/6, 6/6, 6/6 on this release, and also on 8de0b72 (the window check predates it; the differences between the two are the ones measured above, which these six vectors do not exercise). The same vectors on 0.15.1, with the bare key material that release accepts, give 3/6 verdicts, 3/6 code (a missing field scored as null), 0/6 key_status: it has no window check at all (valid_from and valid_until do not occur in that file), so it accepts all six and gets the three rejections wrong. The vectors' own declared invocation is verify <file> --jwks <jwks> --mode receipt --json; --mode and --json are flags of the reference verifier and this CLI does not implement them, so the run is driven with --jwks alone. An earlier version of this paragraph said 6/6 verdicts for 0.15.1; that was wrong and is corrected here.

The absence side of the verdict. A check this host could not run is not a finding about the receipt. verify_receipt and verify_chain report verdict with a total order FAIL > NOT_ASSESSED > PASS, and the CLI maps it to exit 1 / 77 / 0; ok and assessed are derived from it for existing readers, and ok stays false under NOT_ASSESSED (fail-closed — a signature that was never checked is never a pass). not_assessed lists which receipts of a chain were affected. Measured 2026-09-26 in a virtualenv without cryptography: on 0.15.1 a valid receipt and a forged one both returned ok: false and exit 1 — our own missing library reported with the value of a bad signature; on this release (and already on 8de0b72) both return not_assessed and exit 77; on this release the output reads key_status: no_window, code: null.

The boundary is declared, not left implicit. A capability of this host (a missing cryptography, no ML-DSA backend) is an absence. A usable input the relying party supplies (keys, policy bytes) and the profile itself (an alg outside §6.9) are judgments; an unusable one is the relying party's configuration defect (below). An unknown kid is a "could not look" unless the relying party passes keys_are_complete=True, declaring its set to be the whole trust list: answering "invalid" about a signature this host never checked asserts what it did not measure. Key material of the relying party that is not a key in any form read here (a number, an empty or non-hex string, 5 bytes, a PEM that does not load) is the relying party's configuration defect, not a finding about the receipt: not_assessed with code key_material_invalid in the library, exit 2 at load in the CLI. That test looks at the material alone, never at the receipt, so a usable key of another type than the receipt's alg stays a fail — otherwise flipping the unsigned alg of a forged receipt would buy it "could not look". An absence never hides a finding: a forged receipt beside an unverifiable one gives fail, and a broken §6.7 link is still checked above an unassessed receipt, because the link is a hash and needs no signature backend. A window bound of the relying party that is not an instant is its configuration defect too: the CLI refuses the key set at load, and the library — given such an entry directly — returns not_assessed with key_window_undecidable (on 8de0b72: fail). A bound that CAN be read and excludes the receipt still decides, even when the other bound is unreadable: fail, key_outside_validity_window (on 8de0b72: key_window_undecidable).

Verifier hygiene from the cra-evidence review (2026-09-21, 0.15.1)

Twelve review rounds on cra-evidence 0.3.0 — whose verifiers are this repository's, re-implemented — found four classes of defect in shared code. Measured here on 21/09/2026 with the probe before the fix (a divergence in every case) and with the differential oracle after it (76 cases, five verifiers, 0 disagreements, five CLI-grammar cases among them):

  • JavaScript dropped an own __proto__ key from its canonical form (d[k] = … invokes the prototype setter): an entry with such a key added and its self_hash untouched verified PASS in JS alone; an entry legitimately carrying that key failed in JS alone. Object.fromEntries keeps the key. Two oracle cases, one in each direction.
  • Rust's JSON parser accepted +10, 010 and raw control characters in strings, whose canonical form is the same as the escaped one, so a byte-tampered line verified PASS in Rust alone (Python, JS, Go and Java refuse them as not JSON). RFC 8259 number grammar and the control-character rule enforced. Three oracle cases.
  • A file that is not UTF-8 was FILE_ERROR in Rust, and JavaScript decoded it lossily (a raw byte in a value whose self_hash was computed over U+FFFD verified PASS in JS alone — found by the review of this very section) → strict UTF-8 and a FAIL verdict everywhere; two oracle cases as bytes.
  • CLI grammar was five grammars: an unknown flag, a value flag without a value or with "", or a second positional was silently taken as the ledger path by the JS and Rust CLIs (a verdict with the constraint the operator asked for dropped — --trusted-pubkey "$KEY" with the variable unset verified with the tip unchecked); Go's flag and Java accepted an empty value as "not given"; the Python CLI validated five of its seven value flags. Now a usage error (exit 2, no verdict) in all five; the oracle runs the five CLIs on five such inputs.

No verdict changed on an in-profile ledger; the 42 test suites (Python, Go, Rust, JS) are unchanged and green.

Signed decision receipts for agents (2026-09-20, 0.15.0)

cryptovalid_acta.py implements draft-farley-acta-signed-receipts-03 (T. Farley, ScopeBlind / Veritas Acta, 29 August 2026; an individual Internet-Draft, Informational; copy downloaded 2026-09-20): a signed receipt of an access-control decision for an AI agent's tool call — the evidence artifact the FINOS AI Governance Framework thread on MI-21 (agent decision audit) is converging on, and the profile behind the shared conformance vectors at ScopeBlind/agent-governance-testvectors. Transcribed and enforced: the envelope shape {payload, signature{alg, kid, sig}} (§2.1; the flat shape is read, never emitted), issuer_id == kid (§2.2), protectmcp:decision payloads (§3.1), PureEdDSA over the JCS (RFC 8785) bytes of the payload with no pre-hash (§5, §6.6), previousReceiptHash = "sha256:" + hex(SHA-256(JCS(entire signed receipt))) with the member omitted on a genesis and an unknown prefix refused (§6.7), the normative policy digest acta-policy-digest-v1 over the exact bytes of every policy file (§6.8), and algorithm agility (§6.9: EdDSA emitted; ML-DSA-65 and ES256 read against the declared alg, never assumed — the ES256 signature encoding is not in the draft, IEEE P1363 r||s is read and said). The kid follows §2.1.1 (sb:issuer: + 12 Base58 characters; checked against the base58 2.1.1 library).

Measured 2026-09-20 result
Fixture policy digest (expected/chain.jsonl of the vectors repository, commit 49ad7c1) reproduced from the .cedar bytes: sha256:81ba074a…
Fixture key: seed 00…01 → public key 4cb5abf6… (as published in fixtures/keys/README.md); kid sb:issuer:6ASf5EcmmEHT computed per §2.1.1 agrees with the base58 2.1.1 library (the repository's own receipts use the kid conformance)
Our receipts through the repository's own checks (conformance/verify.sh) schema (ajv, draft-07) 4/4; @veritasacta/verify 0.10.18 signatures 4/4; chain/outcomes PASS — 6/6; each of three tampered sets (decision, link, signature) fails checks 2 and 3 (the schema check passes, as it must: the tampered values are well-formed)
Decisions from the official Cedar bindings (cedarpy 4.12, the cedar-policy Rust crate) — the fixtures' expected_decision is never read
Reference receipts of the draft author's implementation (protect-mcp 0.29.0, sign --cedar) through cryptovalid_acta verify 4/4, §6.7 links and §6.8 digest recomputed; the decision altered → refused (examples/acta/reference_protect-mcp/)
Algorithm-mixed chain (EdDSA then ML-DSA-65, §6.11) verifies, each receipt against its own alg
python3 cryptovalid_acta.py policy-digest examples/acta                       # sha256:81ba074a…
python3 cryptovalid_acta.py sign --key agent.seed --tool Bash --decision deny --policy-dir examples/acta --out r2.json --previous r1.json
python3 cryptovalid_acta.py verify r1.json r2.json --key sb:issuer:6ASf5EcmmEHT=4cb5abf6…29 --policy-dir examples/acta
python3 cryptovalid_acta.py run-vectors <agent-governance-testvectors clone> receipts/cryptovalid-opencore --seed 00…01

examples/acta/testvectors_driver/run.sh is the driver to drop into that repository's implementations/. Review round 1 (Claude Opus 5, Sonnet 5, Haiku 4.5 — Gemini 3.1 Pro's credits were exhausted that day; every finding re-measured and ablated in the tests): the §2.2 issuer_id == kid rule was enforced only on the envelope shape — a flat-shape receipt signed by any registered key could name another issuer (fixed for both shapes); a valid signature with hostile members outside the signed bytes (NaN inside the signature object, 600-deep nesting) raised instead of returning a verdict (the whole receipt is canonicalized first; the signature object must carry exactly alg, kid, sig; a payload with a signature member is refused, §6.6); with the policy bytes given, a receipt without policy_digest passed as "bound" (now a problem) and policy_digest / previousReceiptHash formats were not checked on read; a chain whose earlier receipt failed raised at the next link; a --policy-dir without policy files verified nothing and said ok; the CLI printed a traceback on 100 000-deep JSON and accepted duplicate JSON keys last-wins (both refused). Round 2: verify_chain read the link and the policy digest from a payload member even on a flat-shape receipt, so a signed decoy payload could make a broken chain pass and a good one fail (now one shape decision for signature, link and digest, §6.6); ES256 is read low-S only (the (r, n−s) twin verifies and would change the §6.7 hash of the receipt); issued_at must be a real instant, not just the RFC 3339 shape; previous must be a signed receipt; a tool name that cannot sit in a Cedar entity literal is refused before evaluation; the ML-DSA-65 context choice (empty) is declared as ours, not the draft's. Round 3 (the last; its findings applied without a further round): an earlier-revision policy_digest (16 hex, no prefix — the §6.8 compatibility note) no longer fails the signature check, it is an opaque label the policy binding refuses to bind on; the ML-DSA-65 and ES256 negative controls now reach the signature check itself (a real key that did not sign), not a length or format gate; the sentence "tampered sets fail all three checks" was false (the schema check passes on well-formed tampered values) and is corrected above. Stated limits: a chain truncated at its end is undetectable without an external commitment to its head (§9.7 — cryptovalid's checkpoints and witnesses are that commitment); the first receipt of a set may be a genesis or a segment of a longer chain (§2.2 says the format does not distinguish them; reported as a warning); whether the policy is the one in force is a separate check (§6.8); commitment mode (§6, Merkle-committed selectively disclosable fields), the evidence predicate (§4) and the other five receipt types are not implemented; keys come from the relying party — a receipt's embedded public_key (the reference implementation adds one) is never used (§9.5).

Checkpoints, witnesses, split-view (2026-09-19, 0.14.0)

The two limits above were the honest gap between this archive and a transparency log. They are closed the way the field closes them — with the world's formats, so that the field's own software verifies the result — and the closure is measured, not asserted (test_cryptovalid_witness.py, 27 tests — 26 without a Go toolchain, 5 without cryptography; the bench against the reference witness is in the release dossier):

Module What it does Standard (editor's copies downloaded 2026-09-19)
cryptovalid_checkpoint.py A checkpoint of a ledger: origin, tree size, RFC 6962 root of the canonical entries (the same leaves the receipts prove against), signed as a note by the log key (Ed25519, key id = SHA-256(name ‖ 0x0A ‖ type ‖ pubkey)[:4]); vkey strings; strict parsing where the spec is strict (tree size without leading zeroes, canonical base64 root, control characters refused, at most 100 signature lines — the ceiling of the Go implementation, the spec floor being 16) and permissive where the spec says so (any non-empty origin line: "go.sum database tree" parses); every line of a known key is verified (the spec's MUST — the Go library verifies the first line per key and drops repeats, so a note with a bogus repeat opens there and is refused here: declared, fail-closed), byte-identical repeats count once, keys of types nobody verifies here (RFC 6962 THS 0x05, unassigned) are ignored, never accepted — ECDSA (0x02) and ML-DSA-44 (0x06) ARE verified, see below — two different trusted keys colliding on one 4-byte id are refused as ambiguous (measured with a real collision); consistency proofs as base64 lists. c2sp.org/tlog-checkpoint, c2sp.org/signed-note
cryptovalid_witness.py A witness: keeps the last checkpoint it cosigned per origin (trust on first use, stated; several trusted keys per origin for rotation; an origin that differs from the key name is configured explicitly, as for sum.golang.org), cosigns a new one only when the log signature verifies and the new tree extends the stored one (same size → same root; larger → a consistency proof that verifies; smaller → rollback). Every refusal that involves two log-signed checkpoints returns both (evidenza); the pairs that are proof — split-view, two log-signed roots at one size, anyone can check both signatures — are appended once each to <state>.evidence.jsonl (also when the refusal reaches an HTTP client as a bare status), whether the witness meets them at the size it holds, at a size it cosigned earlier (<state>.cosigned.jsonl, one line per cosigned checkpoint; the last 4 MiB are searched) or at a size it refused earlier without proof and is now passing; rollback and unproven-extension are not proof (a log genuinely signed the older checkpoint, a missing proof may be a client error): they are returned and counted (distinct ones), and remembered as size/root/time (last 100 per origin); the log-signed notes refused without proof beyond the stored size — whatever old size the request stated — are kept in a pending ring (at most 100 per origin and 256 KiB, notes above 16 KiB kept as root only, smallest sizes dropped first) so that a later cosign, a second root at that size or a later rollback to it can turn them into proof (a cosign consumes the pending roots at its own size and leaves the others in the ring). A second root at a size the witness never cosigned pairs with a root it still remembers (pending ring, or the refusals ring as root only → split-view-unkept); one it no longer remembers (never seen, or dropped from the rings) is a rollback/unproven-extension it cannot turn into proof — stated. The cosignature is cosignature/v1 (timestamp inside the signed bytes). serve exposes the add-checkpoint endpoint of the tlog-witness HTTP interface (POST <prefix>/add-checkpoint; 200 with the signature lines, 400, 403, 404, 409 with the size as text/x.tlog.size, 422, in the spec's order; 500 when the witness cannot decide — no Ed25519 implementation (the reason in the body), state file unreadable or unwritable or invalid configuration (the reason on stderr); check-and-persist under a process lock and a file lock, state and evidence fsync'ed before the response); submit is the log side (old size 0, then 409 → the witness's size and our proof; from the answer it keeps only the lines of the witness keys it trusts, each replacing that key's older line). verify_witnessed is the relying party: log signature (one key or a rotation set), the expected origin, ≥ N distinct trusted witnesses on this text — every line of a trusted witness key is verified and its newest counts — and, with max_age_s, ≥ N of them younger than it (a witness whose newest cosignature is in the future beyond clock_skew_s does not count; without max_age_s no clock is consulted). c2sp.org/tlog-cosignature (Ed25519 v1), c2sp.org/tlog-witness
cryptovalid_scitt.py SCITT (RFC 9943, Proposed Standard, June 2026): a Signed Statement (COSE_Sign1 with the CWT Claims header — iss, sub — content type and kid, payload attached or detached), its registration on a cryptovalid ledger (the entry binds the statement's SHA-256), a Receipt with the CWT Claims RFC 9943 requires and the RFC 9942 inclusion proof (vds 1, vdp −1; the canonical leaf travels unsigned beside it), and the Transparent Statement (receipts under label 394). The relying party verifies offline, fail-closed: Issuer signature, TS signature with ITS trusted key, that the leaf binds THIS statement, and the RFC 6962 path up to the signed root. Measured with pycose, an independent COSE implementation: it decodes both messages, verifies both signatures, reads labels 15/394/395, refuses a wrong key or a tampered payload (test_cryptovalid_scitt.py, 5 tests, pycose required in CI). Stated: EdDSA only; the RFC9162_SHA256 profile, not CCF's; no public Transparency Service here. RFC 9943, RFC 9942, RFC 9052
cryptovalid_monitor.py --match REGEX Identity / subject search while monitoring, the way rekor-monitor searches a log for identities (its README, 2026-09-19): each pattern is matched against the canonical bytes of every entry — the bytes the Merkle leaf commits to — and the run reports the matching leaves (index, self_hash, snippet) and the NEW ones since the last green state, so an inclusion receipt can prove any of them. rekor-monitor (Sigstore)
verifiers/note_oracle/ Independent verifier with no cryptovalid code: golang.org/x/mod/sumdb/note (the Go checksum database's implementation of signed notes) and github.com/transparency-dev/formats/note (cosignature/v1; pinned to the commit the reference witness of 2026-09-18 uses, which also caps cosigner names at 255 bytes — so do we). Built and run in CI; every listed key must have signed, a tampered byte or a missing cosigner is exit 1. the ecosystem's own libraries

Measured on 2026-09-19 against the reference witness (github.com/transparency-dev/witness, commit 55a5a0bf332a of 2026-09-18, the code the Armored Witness hardware runs), three ways: its in-process witness.Update cosigned our checkpoint of a 5-entry ledger, refused the 8-entry one without a proof, cosigned it with our RFC 6962 consistency proof, refused a fork at size 8 ("roots do not match", logging both checkpoints as inconsistent) and refused rollbacks; its HTTP client drove our serve with the same outcome at every step; our submit client drove its HTTP server (200, 409 then proof, 422 on the fork, 409 on rollbacks) and the cosignature it returned verified in verify_witnessed and in the Go oracle (VERIFIED sigs=3 unverified=0 on a note cosigned by both witnesses).

python3 signer.py keygen log.key && python3 signer.py keygen witness.key
python3 cryptovalid_checkpoint.py sign ledger.jsonl --origin example.org/ledger --key log.key --out cp.note
LOG=$(python3 cryptovalid_checkpoint.py vkey --key log.key --name example.org/ledger)
WIT=$(python3 cryptovalid_checkpoint.py vkey --key witness.key --name witness.example/w1 --cosigner)
python3 cryptovalid_witness.py serve --state witness.json --name witness.example/w1 --key witness.key --log-vkey "$LOG" --port 8477 &
python3 cryptovalid_witness.py submit ledger.jsonl --origin example.org/ledger --key log.key --url http://127.0.0.1:8477 --witness-vkey "$WIT" --out cp.cosigned.note
python3 cryptovalid_witness.py verify cp.cosigned.note --log-vkey "$LOG" --witness-vkey "$WIT" --min-witnesses 1 --max-age 3600

Log keys: Ed25519 (0x01) and ECDSA (0x02, P-256/384/521, the type Rekor v1 uses — key id SHA-256(DER SPKI)[:4], as transparency-dev/formats computes it) — measured on the three real rekor.sigstore.dev checkpoints of 2026-09-19 in examples/checkpoints/ (2.77 billion entries on the active shard: verified by us and by the Go oracle's NewECDSAVerifier, refused when tampered, cosigned by our witness). Cosignatures: Ed25519 cosignature/v1 (0x04) and ML-DSA-44 cosignature/v1 (0x06, the type the witness spec now says witnesses SHOULD use; FIPS 204 pure mode, the spec's cosigned_message over origin/size/root — extension lines are outside it, as the spec states): serve --pq-key adds the ML-DSA-44 line beside the Ed25519 one, and the Go oracle (filippo.io/mldsa through transparency-dev/formats) verifies it. Stated limits of this layer: RFC 6962 THS keys (0x05) are ignored; our own logs sign Ed25519 (ECDSA is verified, not emitted); the monitoring (GET) side of tlog-witness is not implemented, only add-checkpoint; no public witness has cosigned an OMEGA ledger (there is no public OMEGA log — the interop above is local, with the reference implementation); the witness state file's integrity is the operator's; the per-entry timestamps remain local-clock claims (pre-anchor window) and an append-only log never proves completeness (selective omission). Declared divergences from the Go libraries, in the refusing direction: a cosignature timestamp above 2^63−1 is refused here because the cosignature spec says it MUST NOT exceed that value, a zero timestamp is refused because the witness spec says a witness MUST NOT emit one (Go's Ed25519 cosignature verifier checks neither; its ML-DSA-44 one refuses above 2^63−1 and accepts zero), a second, invalid line from a key that already signed validly makes the note invalid here (Go drops repeats), a signature line above 8 KiB of decoded material makes the note invalid here even from an unknown key (Go has only the 100-line cap and ignores the line); the checkpoint parser demands canonical base64 for the root (the formats/log reader does not); and one in the accepting direction: the same vkey listed twice is one key here, ambiguous key in Go's VerifierList (the same key, no effect on what verifies). The evidence file holds proofs only (one record per distinct pair of log-signed roots at one size), so an unauthenticated client replaying the log's public history cannot grow it (measured: 5 replays of one rollback → no record; 291 future checkpoints without proof → no record at the 289 fresh sizes, and one record at each of the two sizes that already held refused roots, where the third log-signed root is one more genuine proof — one pair per request, so a log signing many roots at one size buys one record and one fsync per request, not one per pair). The state it can grow only up to two bounded rings per origin — the last 100 refusals as size/root/time and at most 100 pending log-signed notes, 256 KiB (UTF-8 bytes) in all, notes above 16 KiB remembered by their root only — a later conflicting root then yields a split-view-unkept record with the earlier root, not a full pair (measured: 291 replays → 100 pending, file size flat; 20 notes with 115 extension lines of 8 000 bytes and 60 with 20 KB of four-byte characters → state under 200 KB) — plus one index of the last 10 000 recorded proof pairs (≈ 0.75 MB at most; beyond it a very old pair replayed is recorded again), which grows only with the log's misbehaviour. A request that adds something to the rings or to the proofs (a rollback, an unproven extension, a log-signed root beyond the stored size, a new pair — whatever status it then gets) rewrites and fsyncs the state file once; a request that adds nothing (a replay of a refusal already in the ring, a replay of a recorded pair, a bad signature, an unknown origin, a malformed body) writes nothing — measured on every path with a spy on the writer. The refusals ring counts distinct refusals, not attempts. A rollback is compared with the roots the witness still holds at that size and with the last 4 MiB of the cosigned history — one file shared by all the origins a witness serves, so a busy origin can push a quiet one's older cosigned checkpoints out of that window (a bounded read: stated). What grows without a bound: the cosigned history, one line per checkpoint this witness cosigns (each needed a valid consistency proof), and the proofs, at most one per request — a log that signs many roots for one size fills the file with proofs of its own misbehaviour at the rate of its requests, each request costing the witness a bounded number of writes. Lines from keys the witness does not know are stripped from everything it stores.

High-frequency ingestion (Art. 12 logs at scale)

cryptovalid_ingest.py turns a high-rate event stream into this same evidence format — nothing new to verify, just more of it, faster:

  • segments (ledger-000000.jsonl, …): each one an ordinary hash-chained ledger the stdlib verifier accepts unchanged — an auditor needs one file, not the archive;
  • sealing: each segment gets an RFC 6962 STH sidecar, hash-chained to the previous STH (removing or reordering interior segments is detectable), optionally Ed25519-signed via any KMS/HSM backend above, and ready for a single RFC 3161 timestamp per batch;
  • HEAD manifest (<prefix>.head.json, rewritten at every seal): commits to the segment count and the last STH — a bare STH chain, like any bare hash chain, cannot see its own tail being truncated or extended (same limit the threat model states for entries). Honest scope: the HEAD closes the tail gap only when signed and verified against a pinned public key; an unsigned HEAD can be rewritten by whoever rewrites the archive. Anchor the HEAD hash via RFC 3161 for third-party time;
  • batched fsync (the durability unit is the flush; batch_size=1 for per-event durability) and fail-closed crash recovery: a torn trailing line is truncated and reported; a tampered history refuses to resume;
  • automatic RFC 3161 anchoring (tsa_url=): every seal live-timestamps the STH canonical hash (token in <segment>.sth.tsr + honest metadata receipt), optionally LOTL-certified as eIDAS-qualified (lotl_check=True, cached) — validated live against a real QTSP (Izenpe). Best-effort by design: a TSA/network failure never blocks ingestion (it lands in the receipt and in a verify warning) and runs outside the writer lock (a slow TSA never stalls appends). Verification of tokens is two-layer and fail-closed: CMS signature (openssl, -noverify: the chain-to-QTST question belongs to the LOTL match) + digest binding to the STH — a forged blob or an unbound token fails; without openssl, acceptance degrades to binding-only with an explicit warning. The LOTL cache lives in ~/.cache/cryptovalid/, never inside the archive (a co-located cache could be poisoned to fake the qualified verdict);
  • verify_archive() / python3 cryptovalid_ingest.py verify <dir> [--lotl --lotl-ms ES] re-checks every sealed segment: hash chain, Merkle root vs sidecar, STH chain, signatures, HEAD manifest, and RFC 3161 token binding (LOTL qualification opt-in, needs network).
python3 cryptovalid_ingest.py bench  /tmp/cv-bench --n 50000   # throughput MEASURED on your machine
python3 cryptovalid_ingest.py verify /tmp/cv-bench --prefix bench

Honest numbers: throughput depends on machine/filesystem/batch — the bench measures it where you run it (reference dev box: ~25k events/s at batch 256 with per-batch fsync; your number is the one that counts). Timestamps come from the host clock: pair sealed STHs with RFC 3161 tokens for independent time. Tests: python3 test_ingest.py (tamper, torn-tail recovery, concurrency, signed-STH negative controls).

Self-updating regulatory profiles

spec/regulatory_profiles.json maps evidence to EU requirements (MiCA, EU AI Act, DORA, GDPR) with status, effective_utc, source_url and an as_of date. A ledger entry may set data.regulatory_ref = "<id>" to declare what it supports.

python3 refresh_regulatory.py --check-urls   # re-checks sources, flags stale entries; exit 1 = needs review

Honest scope: it keeps the mapping fresh and provenance-honest and flags stale entries for a human to re-verify against the primary source — it does not auto-interpret law. Outdated regulatory status is never allowed to pass silently.

Standards & freedom-to-operate (factual, not legal advice)

CryptoValid is built on open standards and techniques whose inventors we name and credit: Merkle trees (Ralph Merkle, US4309569, 1979 — expired), hash-chain timestamping (Stuart Haber & W. Scott Stornetta, US5136647/US5373561, filed 1991–92 — expired 2004), RFC 3161 timestamping and RFC 6962 Merkle proofs (open IETF standards; CT by Laurie, Langley & Kasper), SHA-256 (NIST) and Ed25519 (D. J. Bernstein et al., public domain). The same design has been practised openly since 2013 by Certificate Transparency, Sigstore/Rekor, OpenTimestamps and immudb — broad, load-bearing prior art.

Closest patented art, considered and distinguished (screening 2026-08-17): the Guardtime / KSI family (Buldas et al.) — its foundational US8719576B2 ("distributed calendar infrastructure", priority 2003) expired December 2024, and the living members (e.g. US9614682, US11057187) claim the calendar-aggregation KSI architecture, which this design does not practise (we use per-archive Merkle STHs + independent RFC 3161 TSAs, not a distributed calendar). The IETF datatracker records zero IPR disclosures against RFC 6962 — noting honestly that IETF disclosure duties bind participants only, so this is corroboration, not proof.

No known third-party patent is practised, after the documented screening above. A freedom-to- operate claim is a universal negative and cannot be proven — pending applications and non-US filings remain invisible by construction. Technical FTO note, not a legal opinion; a definitive FTO requires professional counsel.

The next decade (read on 15/09/2026)

What will change under an evidence ledger written today, and what this repository already does about it:

  • Signatures. NIST IR 8547 (an initial public draft, not a final standard) proposes deprecating the 112-bit-strength quantum-vulnerable schemes after 2030 and disallowing all of them — Ed25519 and ECDSA included — after 2035. A ledger kept for the multi-year retention windows of DORA, the CRA (technical documentation for at least 10 years, Art. 13(13)) or eIDAS crosses that line. The pqcrypto/ suite lets the AP2 evidence format carry a hybrid classical + ML-DSA-65 (FIPS 204) signature through the cryptography library, checked against the NIST ACVP ML-DSA-65 signature-verification vectors; since 0.12.0 the core ledger entries, the signed chain tip carry the same hybrid Ed25519 + ML-DSA-65 layer and the evidence-pack manifest records it per ledger (threat model above: opt-in, pinned by the relying party's PQ key; entries checked by Python, the tip by Python and Go). After 2035 an RFC 3161 token (itself RSA/ECDSA-signed) proves nothing on its own: hash-only anchors (OpenTimestamps) and periodic re-timestamping are what keep an Ed25519-only ledger load-bearing, which is why anchor coverage is part of the threat model.
  • Transparency and receipts. The IETF SCITT architecture is now RFC 9943 (Proposed Standard, June 2026) and COSE receipts are RFC 9942; this repository's receipts follow RFC 9942 (vds=1 RFC9162_SHA256, inclusion −1 / consistency −2, alg EdDSA) and its monitor plays the role rekor-monitor and immudb's auditor play (append-only, consistency over time; the blind window between two runs is declared), and since 0.14.0 its checkpoints and witness speak the C2SP formats and protocol the transparency ecosystem (Sigstore, Tessera, the Go checksum database, the witness network) uses — measured against the reference witness, see above. Registering signed statements with a SCITT transparency service and obtaining its receipts is the natural next step and is not implemented yet.
  • Qualified ledgers in the EU. Commission Implementing Regulation (EU) 2025/2531 gives the requirements for qualified electronic ledgers under eIDAS 2.0 (Art. 45l); eidas_ledger_check.py self-assesses a ledger against them and says what only a QTSP can add. Being qualified is a status a QTSP holds, never something this code confers.
  • Supply-chain formats. CycloneDX 1.7 (ECMA-424, October 2025) is the current specification; the CRA evidence packs attached to this project's releases are produced with the public cra-evidence tool (CycloneDX 1.6 by choice — the stable schema with mature validators — Art. 14 clock, ENISA SRP fields) and signed with an AWS KMS-held (HSM-backed) key whose public part ships with the release; the Transparency Exchange API (ECMA TC54) is the distribution channel to watch.
  • Verification without the producer. The five verifiers in the differential oracle (Python, JavaScript, Go, Java, Rust) plus a reduced-scope Swift one exist so that the evidence outlives this codebase: an auditor in 2035 needs the profile and one independent implementation, not this repository.

Contact, pilots, citation

  • Questions, interoperability reports, divergences found by your own verifier: open a thread in this repository's Discussions or an issue; e-mail: roberto.locatelli.81@gmail.com.
  • Pilots: the author runs short evaluation pilots (four to six weeks, scoped and priced up front) with banks, QTSPs, GRC vendors and Java shops that need evidence verifiable offline for years, including through the post-quantum transition. Write with the use case; the answer says what is measured and what is not.
  • Licence: AGPL-3.0-or-later for this repository (see the licensing table above); a commercial licence of the same code is available from the author for organisations that cannot adopt AGPL.
  • Citation: DOI 10.5281/zenodo.22539578 (Zenodo concept DOI: always the latest version).
  • Author: Roberto Locatelli, 2026. Public interventions by his AI agent (Noûs) are signed as such.

Install (pip)

pip install --extra-index-url https://robertolocatelli81-dev.github.io/pypi/ cryptovalid-opencore

Release artifacts are attached to GitHub Releases; the index links carry #sha256= fragments verified by pip. All documented python3 <file>.py commands keep working unchanged from a clone.

About

Verifiable compliance evidence: hash-chained ledgers, signed tip, hybrid Ed25519+ML-DSA-65, KMS custody, evidence packs, C2SP checkpoints + tlog-witness (split-view evidence, ML-DSA-44 cosignatures, real Rekor checkpoints verified), SCITT RFC 9943; verifiers in Python, JS, Go, Rust, Java + differential oracle. AGPL-3.0 / commercial.

Topics

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages