Security, provenance and oversight
Proof of governed execution and the five-chain ledger
A consequential action is governably proved only when identity, evidence, authority, execution and outcome records are independently signed and bound to the same action.
TLDR
- Logs can all be authentic while referring to different actions, policies, evidence snapshots or retries.
- The join key must be a digest of the exact typed action, rather than only a trace ID or human-readable description.
- Five chains are required: identity, evidence, authority, execution and independently observed outcome.
- Signatures and append-only receipts protect records, but they do not prove truth, completeness or causal order by themselves.
- Release authority should depend on a machine-verifiable proof package whose failures block, contain or compensate the action.
Five true records and one false conclusion
At 09:00, an operations agent proposes a £4,800 payment for invoice INV-742. The identity service records a valid workload credential. The policy engine records an allow decision. The payment gateway records an accepted request. The settlement ledger records a debit. An observability trace links the services. Every component has a log, and every log entry is authentic.
A week later, the supplier reports that the money arrived at the wrong beneficiary. The investigation discovers something awkward. The identity record belonged to the correct agent run. The evidence snapshot came from an earlier run, before a beneficiary change. The policy decision approved the earlier snapshot. The gateway receipt belonged to a retry whose idempotency key had changed. The debit was real, but the readback service never checked which beneficiary token received it.
No record was forged. The conclusion was forged by composition. Reviewers silently assumed that a shared trace identifier meant all records committed to the same action. It did not. A trace says that events travelled together. It does not say that each participant signed the same amount, beneficiary, purpose, evidence cut-off, policy decision, request and observed effect.
The security problem is not the absence of records. It is the absence of a verifiable join. A governed system must prove more than “something happened near these logs”. It must show that the right identity used the permitted evidence, received authority for the exact typed action, dispatched that action once, and obtained an independently verified postcondition.
This distinction matters most when an action is disputed. A conventional audit trail invites investigators to infer continuity from timestamps, names and proximity. A proof package makes continuity an explicit claim that software can reject. The goal is not to replace human judgement. It is to prevent humans from spending their judgement on questions that hashes, signatures, temporal rules and independent readback can settle deterministically.
The audit trail that proves too little
Most distributed systems can tell a plausible story about an action. Far fewer can demonstrate that the story’s sentences refer to the same event.
Modern observability is good at correlation. W3C Trace Context standardises identifiers that travel across service boundaries, and OpenTelemetry can attach those identifiers to spans and logs.[1][2] This is invaluable for diagnosis. Yet a trace identifier is normally mutable application context. It does not commit to the amount, object, evidence snapshot or authority under which a call was made. A compromised or confused component can copy the identifier onto a different request.
Policy systems improve the picture. Open Policy Agent decision logs can record the query input, policy bundle metadata and decision identifier.[3] That proves more, but only if the input contains the exact action and if the dispatch later proves it used that decision. A log of “allow” beside a payment is not enough. The decision might concern another beneficiary, a lower amount, an earlier policy bundle or an evidence snapshot that expired before execution.
Why a central audit lake can still mislead
Teams often respond to fragmented logs by copying everything into one warehouse. Centralisation improves search, retention and access control, but it does not repair missing semantics. An ingestion pipeline may flatten policy bundle versions, convert event times into arrival times, discard signature envelopes, or normalise distinct identities into the same display name. The resulting table looks coherent precisely because the distinctions have been removed.
A central store also creates correlated control. If one administrator, pipeline or service account can rewrite identity, policy and execution records before they are sealed, the apparent multiplicity of witnesses collapses. The better pattern is to let each authoritative producer sign a small, typed statement before export. The audit lake may index those statements, but the verifier should validate the original commitments and their role-specific keys. Search convenience and evidential authority should not be confused.
Three claims that must remain separate
A useful design starts by separating three claims. A record integrity claim says bytes have not changed since a signer committed to them. A process conformance claim says required steps occurred in an acceptable order. An outcome truth claim says the world now satisfies a stated postcondition. Signatures help the first. Linked attestations help the second. Independent readback helps the third. None automatically supplies the others.
W3C PROV gives a domain-neutral vocabulary for entities, activities and agents, which helps represent how data or artefacts were produced.[7] in-toto link attestations bind a supply-chain step to materials, products and a signer, while SLSA provenance records how a build platform produced artefacts.[8][9] These are strong precedents. A governed action needs a related structure, but its proof must also carry policy authority, temporal entitlement, idempotency and verified postconditions.
Cryptographic identity is not moral responsibility
A workload credential can show which software principal held a key. It does not settle which human, team or institution should answer for the action. The proof package should therefore distinguish executing workload, initiating actor, represented subject, approving principal and policy owner. Collapsing them into one “user” field creates a confused-deputy channel.
flowchart LR A["Five true records and one<br/>false conclusion"] --> B["The audit trail that proves<br/>too little"] B --> C["Five chains, one action"] C --> D["Proof is a join, not a pile"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#087A98,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
Five chains, one action
The five-chain ledger is not one database. It is a logical proof object assembled from records whose authority comes from different systems and, ideally, different control domains.
Begin with a typed action projection. Include every property whose change would require a new decision: operation, subject, resource, material parameters, purpose, evidence-snapshot digest, requested time and idempotency key. Exclude incidental transport details. Canonicalise the projection using a specified representation, add a domain separator and hash it. JSON Canonicalization Scheme exists because cryptographic operations require a repeatable byte representation; COSE and DSSE provide established envelopes for type-bound signed content.[10][11][12]
The action digest
The action digest is a commitment, not a correlation label. Every chain record repeats it. A verifier can therefore reject a valid policy decision, execution receipt or readback that belongs to another action. The digest must be recomputed from the supplied typed action; accepting a caller-provided digest without the action body only relocates the trust problem.
Here, A is the exact action being judged. D is its digest. Each record carries the previous record hash, creating continuity inside that chain. The signature says the chain’s issuer committed to the record. The assumption is that canonicalisation is fixed, hash and signature algorithms remain acceptable, verification keys are trusted for the stated role, and the issuer’s event-capture path is not silently bypassed.
Change one field, change every admissible join
Suppose the action is “release £4,800 for INV-742 to beneficiary token 9f7a”. The canonical bytes produce digest sha256:6d4…c81. A policy record signed against 6d4…c81 cannot authorise an execution record carrying 2ab…e05, even when both display “INV-742” in a dashboard. If the amount changes to £480, the typed action changes, so the digest changes. The old authority is unusable by construction.
£4,800 · token 9f7a
6d4…c81£480 · token 9f7a
2ab…e05£4,800 · token 51bb
95c…7f2A deletion that a dashboard cannot see
Consider an authority chain with two records. Record one says policy bundle p17 allowed digest D. Record two says approver u42 confirmed decision dec-9. The second record stores the first record’s hash as previous_hash. If an exporter drops record one but keeps record two, a dashboard may still show “approved by u42”. The verifier recomputes the chain and finds that record two points to a missing predecessor.
Now suppose an attacker inserts a new policy record and reuses the old approval. The inserted record has a different hash, so the approval’s predecessor no longer matches. To make the chain valid, the attacker must also rewrite and re-sign the approval. This does not make compromise impossible, but it localises the required compromise to the authority signer rather than allowing an untrusted log pipeline to rewrite history silently.
Chain 1: identity
The identity chain answers: who or what acted, on whose behalf, for which subject and purpose, in which measured environment? It can include a short-lived workload credential, runtime attestation result, initiating actor, represented subject, delegation reference and credential expiry. Entity Attestation Tokens provide a standards-track format for attested claims about an entity’s state and characteristics, but the relying party still decides how much trust to place in those claims.[6]
The chain must be valid at dispatch, rather than only at planning time. It should also expose substitutions. If a tool adapter, model runtime or execution worker changes after approval, the verifier needs a rule that decides whether the replacement remains inside the authorised equivalence class.
Chain 2: evidence
The evidence chain answers: what facts were permitted to influence the decision, from which versions, with what provenance, entitlement and cut-off? Store content hashes or selectively disclosed commitments rather than copying sensitive documents into every ledger. Record the snapshot digest, source versions, acquisition times, authorisation basis, freshness limit and redaction policy.
This chain prevents a common retrospective trick: evaluating yesterday’s action against today’s cleaner evidence. A system may reach the same conclusion after a data correction, yet the original decision remains unsupported. The proof must preserve what was knowable and authorised at the decision cut-off.
Chain 3: authority
The authority chain answers: which policy and principal allowed this exact action, under what conditions, until when? Record the policy bundle digest, query input digest, decision identifier, obligations, limits, human confirmation where required and expiry. NIST’s zero-trust model separates policy decision and enforcement functions; the five-chain design extends that separation by requiring the enforcement path to return evidence that it consumed the decision for the same action.[16]
Authority is perishable. An allow decision at 09:00 cannot be replayed at 12:00 if the policy, credential, evidence or world state has changed beyond its declared window. The dispatch must prove that all required entitlements remained live at the moment of effect.
Chain 4: execution
The execution chain answers: what typed request crossed the effect boundary, through which tool and version, with which idempotency key, and what did the executor acknowledge? Separate the request receipt from the effect receipt. A 202 response, queue acceptance or network acknowledgement does not mean the business effect occurred. Record timeout semantics and an explicit unknown-outcome state.
in-toto’s link model is useful here: materials enter a named step and products emerge, with signed metadata about command, environment and by-products.[8] For operational actions, the “product” may be an effect identifier or authoritative state transition rather than a file.
Chain 5: outcome
The outcome chain answers: what state was observed after execution, by which independent path, and did it satisfy the intended postcondition? Read back from an authoritative source rather than trusting the tool’s success message. Bind the readback to the effect identifier and action digest. Record mismatch, compensation and unresolved uncertainty.
Independence is practical, not metaphysical. The read path should not reuse the same adapter, cache, credentials and operator that produced the write. A self-authored success receipt is evidence of what the executor claims, not independent evidence of what the world became.
What the verifier asks before releasing £4,800
The verifier starts from the action body, not from the logs. It recomputes digest D over vendor V-204, invoice INV-742, amount, currency, beneficiary token, purpose, evidence snapshot and idempotency key. It then asks five narrow questions. Did the workload credential and actor binding name D and remain valid at dispatch? Did the sealed invoice and vendor snapshot match the evidence digest inside the action? Did policy decision dec-4821 evaluate D, remain unexpired and carry the required approval? Did the payment gateway receive D with the authorised idempotency key? Did an independent settlement read path observe the same effect identifier, amount and beneficiary?
Any “no” changes the operational response. A stale evidence snapshot triggers re-evaluation. A changed beneficiary creates a new action requiring new authority. A timeout creates an effect-unknown state, not permission to retry. A mismatched readback triggers containment or compensation. This is why the five chains are not five copies of the same audit event. Each one closes a different inference gap.
Proof is a join, not a pile
A folder containing five signed records is still only a folder. The verifier must test their semantic joins, their order and the independence of the witnesses.
A join manifest commits to the action digest and the current head of each chain. Hash continuity detects deletion, insertion or reordering inside a supplied chain. Signatures identify which trusted key committed to each record. The manifest’s hash locks the assembled proof package. A transparency service may then issue an inclusion receipt so later omission or equivocation becomes detectable.
Certificate Transparency demonstrates how append-only Merkle structures support public audit of logged statements.[13] COSE Receipts generalise signed proofs about verifiable data structures, including inclusion and consistency. SCITT defines an architecture for registering signed statements with transparency services and receiving verifiable receipts.[14][15] These mechanisms strengthen accountability. They do not prove that the issuer’s statement was true, that every event was logged, or that a legitimate signer was uncompromised.
Five classes of checks
A verifier should reject unless it can establish: syntactic integrity of every record; action equality across all chains; causal order and freshness; role-appropriate signatures with adequate separation; and semantic postconditions tied to the effect identifier. A green signature icon addresses only the first and part of the fourth.
Every signature verifies, yet the package must fail
Assume all five chain signatures are cryptographically valid. The evidence record expired at 09:02, while dispatch occurred at 09:04. The authority record allowed the right digest, but its obligation required a beneficiary check that never appeared in the execution receipt. The outcome record was signed by the same gateway that performed the write. Nothing has been forged.
The package still fails three semantic tests: evidence was stale at the effect boundary, an authority obligation was not consumed, and the outcome lacked an independent observer. This counterexample matters because signature verification is often treated as the finish line. In governed execution, it is merely admission to semantic verification. A valid signature authenticates a statement; it does not make the statement sufficient for the decision at hand.
Four honest-looking failure modes
Signed but stale: the issuer committed honestly, but the claim had expired before effect. Transparent but false: a transparency service proves that a signed statement was registered, while the statement’s sensor or issuer was wrong. Complete but bypassed: the supplied package is internally perfect, yet another ungoverned interface created a second effect. Separate names, common control: five services use different hostnames but share one administrator, signing key or mutable database.
These failures suggest a disciplined language for assurance. Say exactly what was established: “the authority service signed an allow record for digest D under policy p17”, not “the action was compliant”. The latter conclusion requires the other chains, temporal constraints and the absence of a relevant bypass. Precise claims make both audits and incident response faster because they reveal which premise is missing.
Independence has degrees
Five signatures from one service account do not provide five witnesses. Neither do five microservices administered by one pipeline, sharing one key vault and one database. Independence improves when chains use separate keys, code paths, data sources, operators and failure modes. Perfect independence is rarely possible, so the proof should declare common dependencies.
The small chart below uses an illustrative index, not a failure probability. Let five chains have common-control correlation ρ. Define effective challenge paths as 1 + (5 − 1)(1 − ρ). At zero correlation, the index is five. At complete correlation, it is one. The formula merely forces an architectural conversation: where can one compromise rewrite several chains?
The ledger cannot prove complete capture from inside itself
A perfectly valid package proves claims about the records it contains under stated trust anchors. It cannot show that no unrecorded action occurred through another path. Completeness requires architectural enforcement: all effects must cross a boundary that refuses requests without valid proof inputs and emits non-bypassable receipts. External reconciliation, transparency monitoring and asset-level invariants are needed to detect paths that evade the ledger entirely.
stateDiagram-v2 [*] --> Claim state "The identical debit" as Claim state "Why a central audit lake can<br/>still mislead" as Test state "Three claims that must remain<br/>separate" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
Build the smallest useful verifier
The minimum viable implementation is not a blockchain, a new observability platform or a universal ontology. It is a typed action, five append-only record sequences, separate signers, a join manifest and a deterministic verifier.
A supplier payment package
The runnable artefact below constructs a synthetic payment action. The identity chain attests a payment-orchestrator workload and binds an approving actor. The evidence chain seals invoice and vendor-master versions. The authority chain records a policy allow and human approval. The execution chain records dispatch and an effect receipt under one idempotency key. The outcome chain reads the settlement ledger through a separate service and verifies amount, currency, beneficiary token and effect identifier.
The verifier recomputes the action digest; checks hash continuity and signatures; requires named events; compares subjects, purpose, evidence and policy input; enforces causal order and expiry; verifies idempotency; matches execution to readback; checks signer separation; and recomputes the join manifest.
A migration path that does not require a platform rewrite
Start at one irreversible boundary, not across the whole estate. Choose a single typed action such as payment release, account closure or production configuration change. Define its material fields and idempotency semantics. Add the action digest to the existing policy query, tool request and readback. This first slice already detects many splice and mutation failures.
Next, move signatures to the authoritative producers. The identity service signs actor and workload binding. The evidence service signs the snapshot commitment. The policy engine signs its decision input and obligations. The effect gateway signs dispatch and effect receipts. The read service signs the observed postcondition. Keep existing logs for diagnosis, but treat signed records as the evidential layer.
Finally, put verification in the control path. Before dispatch, validate identity, evidence and authority. After dispatch, persist an effect-unknown state until readback resolves it. Assemble the five heads, verify semantic joins, and optionally register the manifest in a transparency service. This staged route lets teams measure value before adopting more elaborate ledgers or attestation infrastructure.
Key custody, privacy and retention are proof properties
A verifier is only as credible as its trust anchors. Give each chain role a distinct key policy, rather than only a distinct key identifier. Record issuer, key version, validity interval, revocation status and algorithm. Retain enough retired public-key material to verify historical packages. Where hardware or confidential-computing attestation is used, keep the appraisal result and reference-value version, not an unexplained “trusted” boolean.
Evidence minimisation matters equally. The ledger rarely needs raw invoices, personal data or model prompts. It needs stable commitments, entitlement metadata and a retrieval path governed for audit. Selective disclosure or encrypted evidence stores can preserve later verification without turning the proof system into a second uncontrolled data lake. Retention should follow the consequence and dispute window of the action, while deletion policies should preserve non-sensitive hashes and receipts only where they remain lawful and useful.
This design should not be applied mechanically to every read or low-impact suggestion. The extra signing, storage and verification cost is justified when an action is consequential, disputed outcomes are expensive, or several organisations must rely on the record. For reversible, low-risk operations, a smaller subset may be proportionate, but the omitted inference gaps should be named rather than disguised as full proof.
Run the lab
Save the file as paper-196-five-chain-ledger-verifier.py and run it with Python 3.10 or later. The default package passes. Each tamper option introduces one failure. The demo uses HMAC keys embedded in the file so it requires no external library. That choice is intentionally unsuitable for production. Replace it with independently managed asymmetric signatures, specified envelopes and external trust anchors.
Expected pass
python paper-196-five-chain-ledger-verifier.py
All five chains join to one authorised action and verified outcome.
Splice authority
python paper-196-five-chain-ledger-verifier.py --tamper policy-splice
Rejects a validly re-signed authority record tied to another action.
Expire evidence
python paper-196-five-chain-ledger-verifier.py --tamper stale-evidence
Rejects evidence whose freshness window ended before dispatch.
Fork the retry
python paper-196-five-chain-ledger-verifier.py --tamper duplicate-key
Rejects a dispatch that changes the authorised idempotency key.
#!/usr/bin/env python3
"""Synthetic verifier for the five-chain governed-execution ledger.
Run:
python paper-196-five-chain-ledger-verifier.py
python paper-196-five-chain-ledger-verifier.py --tamper policy-splice
The demo uses HMAC keys embedded in the file so it can run with the Python
standard library. Replace HMAC with independently managed public-key
signatures (for example COSE or DSSE envelopes) in a real deployment.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import hmac
import json
import sys
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Tuple
ACTION_DOMAIN = b"gexec/action/v1\x00"
RECORD_DOMAIN = b"gexec/record/v1\x00"
SIGN_DOMAIN = b"gexec/signature/v1\x00"
JOIN_DOMAIN = b"gexec/join/v1\x00"
# Deliberately visible demo secrets. Production keys must live in separate
# control domains and should be asymmetric, rotated and independently audited.
TRUST: Dict[str, Tuple[str, bytes]] = {
"identity": ("workload-identity.example", b"demo-key-identity"),
"evidence": ("evidence-sealer.example", b"demo-key-evidence"),
"authority": ("policy-engine.example", b"demo-key-authority"),
"execution": ("payment-gateway.example", b"demo-key-execution"),
"outcome": ("ledger-readback.example", b"demo-key-outcome"),
}
REQUIRED_EVENTS = {
"identity": {"workload_attested", "actor_bound"},
"evidence": {"evidence_sealed"},
"authority": {"policy_evaluated", "approval_granted"},
"execution": {"request_dispatched", "effect_receipt"},
"outcome": {"state_readback", "postcondition_verified"},
}
def canonical(value: Any) -> bytes:
"""Deterministic JSON for the demo; use a specified canonical format in production."""
return json.dumps(
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
).encode("utf-8")
def digest(domain: bytes, value: Any) -> str:
return "sha256:" + hashlib.sha256(domain + canonical(value)).hexdigest()
def sign(chain: str, body: Mapping[str, Any]) -> str:
_, key = TRUST[chain]
return "hmac-sha256:" + hmac.new(
key, SIGN_DOMAIN + canonical(body), hashlib.sha256
).hexdigest()
def make_record(
chain: str,
event: str,
action_digest: str,
occurred_at: str,
previous_hash: str | None,
payload: Mapping[str, Any],
) -> Dict[str, Any]:
signer, _ = TRUST[chain]
body: Dict[str, Any] = {
"schema": "gexec/record/v1",
"chain": chain,
"event": event,
"action_digest": action_digest,
"occurred_at": occurred_at,
"previous_hash": previous_hash,
"signer": signer,
"payload": dict(payload),
}
record_hash = digest(RECORD_DOMAIN, body)
return {**body, "record_hash": record_hash, "signature": sign(chain, body)}
def append(
chains: MutableMapping[str, List[Dict[str, Any]]],
chain: str,
event: str,
action_digest: str,
occurred_at: str,
payload: Mapping[str, Any],
) -> None:
previous = chains[chain][-1]["record_hash"] if chains[chain] else None
chains[chain].append(
make_record(chain, event, action_digest, occurred_at, previous, payload)
)
def build_demo() -> Dict[str, Any]:
action: Dict[str, Any] = {
"schema": "gexec/action/v1",
"operation": "release_supplier_payment",
"subject": "vendor:V-204",
"resource": "invoice:INV-742",
"parameters": {
"amount_minor": 480000,
"currency": "GBP",
"beneficiary_token": "beneficiary:9f7a",
},
"purpose": "settle-approved-invoice",
"evidence_snapshot": "sha256:8f9f3f3c5d8a1c02",
"idempotency_key": "pay-V204-INV742-v1",
"requested_at": "2026-08-30T09:00:00Z",
}
action_digest = digest(ACTION_DOMAIN, action)
chains: Dict[str, List[Dict[str, Any]]] = {name: [] for name in TRUST}
append(
chains,
"identity",
"workload_attested",
action_digest,
"2026-08-30T09:00:00Z",
{
"workload_id": "spiffe://ops.example/agent/payment-orchestrator",
"runtime_measurement": "sha256:runtime-71c9",
"credential_expires_at": "2026-08-30T09:10:00Z",
},
)
append(
chains,
"identity",
"actor_bound",
action_digest,
"2026-08-30T09:00:01Z",
{
"actor": "user:approver-17",
"subject": action["subject"],
"purpose": action["purpose"],
},
)
append(
chains,
"evidence",
"evidence_sealed",
action_digest,
"2026-08-30T09:00:01Z",
{
"snapshot_digest": action["evidence_snapshot"],
"cutoff_at": "2026-08-30T08:59:55Z",
"expires_at": "2026-08-30T09:05:00Z",
"sources": ["invoice-register:v18", "vendor-master:v42"],
},
)
append(
chains,
"authority",
"policy_evaluated",
action_digest,
"2026-08-30T09:00:02Z",
{
"decision_id": "dec-4821",
"allow": True,
"policy_digest": "sha256:policy-2026-08-17",
"input_digest": action_digest,
"expires_at": "2026-08-30T09:02:00Z",
},
)
append(
chains,
"authority",
"approval_granted",
action_digest,
"2026-08-30T09:00:03Z",
{"approver": "user:approver-17", "decision_id": "dec-4821"},
)
append(
chains,
"execution",
"request_dispatched",
action_digest,
"2026-08-30T09:00:04Z",
{
"tool": "payments-api:v3",
"request_digest": action_digest,
"idempotency_key": action["idempotency_key"],
},
)
append(
chains,
"execution",
"effect_receipt",
action_digest,
"2026-08-30T09:00:06Z",
{
"effect_id": "pay-90210",
"accepted": True,
"request_digest": action_digest,
"idempotency_key": action["idempotency_key"],
},
)
append(
chains,
"outcome",
"state_readback",
action_digest,
"2026-08-30T09:00:09Z",
{
"effect_id": "pay-90210",
"state": "posted",
**action["parameters"],
"observer": "settlement-ledger-read-api:v2",
},
)
append(
chains,
"outcome",
"postcondition_verified",
action_digest,
"2026-08-30T09:00:10Z",
{"effect_id": "pay-90210", "matches_expected_state": True},
)
heads = {name: records[-1]["record_hash"] for name, records in chains.items()}
join = {
"schema": "gexec/join/v1",
"action_digest": action_digest,
"chain_heads": heads,
"joined_at": "2026-08-30T09:00:11Z",
}
return {
"action": action,
"action_digest": action_digest,
"chains": chains,
"join": {**join, "join_hash": digest(JOIN_DOMAIN, join)},
}
def parse_time(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
def event(package: Mapping[str, Any], chain: str, name: str) -> Mapping[str, Any]:
for record in package["chains"][chain]:
if record["event"] == name:
return record
raise KeyError(f"missing {chain}.{name}")
def verify(package: Mapping[str, Any]) -> List[str]:
errors: List[str] = []
expected_action_digest = digest(ACTION_DOMAIN, package.get("action"))
if package.get("action_digest") != expected_action_digest:
errors.append("action digest does not match the canonical action")
chains = package.get("chains", {})
if set(chains) != set(TRUST):
errors.append("the package must contain exactly the five required chains")
seen_signers = set()
for chain, expected in TRUST.items():
records = chains.get(chain, [])
previous = None
events = set()
expected_signer, _ = expected
for index, record in enumerate(records):
events.add(record.get("event"))
body = {k: v for k, v in record.items() if k not in {"record_hash", "signature"}}
if record.get("chain") != chain:
errors.append(f"{chain}[{index}] declares the wrong chain")
if record.get("action_digest") != expected_action_digest:
errors.append(f"{chain}[{index}] is spliced to another action digest")
if record.get("previous_hash") != previous:
errors.append(f"{chain}[{index}] breaks hash continuity")
computed_hash = digest(RECORD_DOMAIN, body)
if record.get("record_hash") != computed_hash:
errors.append(f"{chain}[{index}] record hash is invalid")
if record.get("signature") != sign(chain, body):
errors.append(f"{chain}[{index}] signature is invalid")
if record.get("signer") != expected_signer:
errors.append(f"{chain}[{index}] signer is outside the trust policy")
seen_signers.add(record.get("signer"))
previous = record.get("record_hash")
missing = REQUIRED_EVENTS[chain] - events
if missing:
errors.append(f"{chain} is missing required events: {sorted(missing)}")
if len(seen_signers) != 5:
errors.append("the five chains are not independently signed")
try:
identity = event(package, "identity", "actor_bound")
attested = event(package, "identity", "workload_attested")
evidence = event(package, "evidence", "evidence_sealed")
policy = event(package, "authority", "policy_evaluated")
approval = event(package, "authority", "approval_granted")
dispatch = event(package, "execution", "request_dispatched")
effect = event(package, "execution", "effect_receipt")
readback = event(package, "outcome", "state_readback")
verified = event(package, "outcome", "postcondition_verified")
action = package["action"]
if identity["payload"]["subject"] != action["subject"]:
errors.append("identity subject does not match the action")
if identity["payload"]["purpose"] != action["purpose"]:
errors.append("identity purpose does not match the action")
if evidence["payload"]["snapshot_digest"] != action["evidence_snapshot"]:
errors.append("evidence snapshot does not match the action")
if not policy["payload"]["allow"]:
errors.append("policy decision did not allow the action")
if policy["payload"]["input_digest"] != expected_action_digest:
errors.append("policy decision evaluated a different action")
if approval["payload"]["decision_id"] != policy["payload"]["decision_id"]:
errors.append("human approval references another policy decision")
if dispatch["payload"]["request_digest"] != expected_action_digest:
errors.append("tool dispatch carries another request digest")
if dispatch["payload"]["idempotency_key"] != action["idempotency_key"]:
errors.append("tool dispatch changed the idempotency key")
if effect["payload"]["idempotency_key"] != action["idempotency_key"]:
errors.append("effect receipt changed the idempotency key")
if effect["payload"]["effect_id"] != readback["payload"]["effect_id"]:
errors.append("readback refers to another effect")
for field, expected_value in action["parameters"].items():
if readback["payload"].get(field) != expected_value:
errors.append(f"readback field {field!r} does not match the action")
if not verified["payload"]["matches_expected_state"]:
errors.append("postcondition verifier rejected the observed state")
t_attested = parse_time(attested["occurred_at"])
t_evidence = parse_time(evidence["occurred_at"])
t_policy = parse_time(policy["occurred_at"])
t_approval = parse_time(approval["occurred_at"])
t_dispatch = parse_time(dispatch["occurred_at"])
t_effect = parse_time(effect["occurred_at"])
t_readback = parse_time(readback["occurred_at"])
if not (t_attested <= t_policy <= t_approval <= t_dispatch <= t_effect <= t_readback):
errors.append("events violate the required causal time order")
if parse_time(evidence["payload"]["cutoff_at"]) > t_dispatch:
errors.append("evidence cutoff is after dispatch")
if parse_time(evidence["payload"]["expires_at"]) < t_dispatch:
errors.append("evidence was stale at dispatch")
if parse_time(policy["payload"]["expires_at"]) < t_dispatch:
errors.append("authority expired before dispatch")
if parse_time(attested["payload"]["credential_expires_at"]) < t_dispatch:
errors.append("workload identity expired before dispatch")
except (KeyError, TypeError, ValueError) as exc:
errors.append(f"semantic verification could not complete: {exc}")
heads = {
name: records[-1]["record_hash"]
for name, records in chains.items()
if records
}
join = package.get("join", {})
join_body = {k: v for k, v in join.items() if k != "join_hash"}
if join_body.get("action_digest") != expected_action_digest:
errors.append("join manifest names another action")
if join_body.get("chain_heads") != heads:
errors.append("join manifest does not commit to the current chain heads")
if join.get("join_hash") != digest(JOIN_DOMAIN, join_body):
errors.append("join manifest hash is invalid")
return errors
def apply_tamper(package: Dict[str, Any], mode: str) -> None:
if mode == "none":
return
if mode == "unsigned-byte":
event(package, "outcome", "state_readback")["payload"]["amount_minor"] = 48000
return
if mode == "policy-splice":
record = event(package, "authority", "policy_evaluated")
record["action_digest"] = "sha256:" + "a" * 64
elif mode == "stale-evidence":
record = event(package, "evidence", "evidence_sealed")
record["payload"]["expires_at"] = "2026-08-30T08:59:59Z"
elif mode == "duplicate-key":
record = event(package, "execution", "request_dispatched")
record["payload"]["idempotency_key"] = "pay-V204-INV742-retry-2"
elif mode == "self-readback":
record = event(package, "outcome", "state_readback")
record["signer"] = TRUST["execution"][0]
else:
raise ValueError(f"unknown tamper mode: {mode}")
# Model a compromised producer that can rewrite its own record consistently.
chain = record["chain"]
body = {k: v for k, v in record.items() if k not in {"record_hash", "signature"}}
record["record_hash"] = digest(RECORD_DOMAIN, body)
record["signature"] = sign(chain, body)
records = package["chains"][chain]
index = records.index(record)
for later in records[index + 1 :]:
later["previous_hash"] = record["record_hash"]
later_body = {k: v for k, v in later.items() if k not in {"record_hash", "signature"}}
later["record_hash"] = digest(RECORD_DOMAIN, later_body)
later["signature"] = sign(chain, later_body)
record = later
package["join"]["chain_heads"][chain] = records[-1]["record_hash"]
join_body = {k: v for k, v in package["join"].items() if k != "join_hash"}
package["join"]["join_hash"] = digest(JOIN_DOMAIN, join_body)
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--tamper",
choices=["none", "unsigned-byte", "policy-splice", "stale-evidence", "duplicate-key", "self-readback"],
default="none",
help="inject a failure before verification",
)
parser.add_argument("--json", action="store_true", help="print the evidence package")
args = parser.parse_args(list(argv) if argv is not None else None)
package = build_demo()
apply_tamper(package, args.tamper)
if args.json:
print(json.dumps(package, indent=2, ensure_ascii=False))
errors = verify(package)
if errors:
print(f"FAIL: {len(errors)} verification error(s)")
for item in errors:
print(f" - {item}")
return 1
print("PASS: all five chains join to one authorised action and verified outcome")
return 0
if __name__ == "__main__":
sys.exit(main())
A pass permits one narrow conclusion: under the configured trust anchors and invariants, the supplied package is internally consistent with one authorised action and matching observed outcome. It does not prove that the signers’ keys were well protected, that no bypass path existed, that the readback source was truthful, or that all relevant business rules were encoded.
A negative result has operational meaning. A digest mismatch indicates a splice or mutation. An expired window means the action needs a fresh decision. An idempotency mismatch means the system must reconcile before retry. A self-readback failure means the outcome lacks an independent witness. The verifier should return typed failure codes so orchestration can block, contain, investigate or compensate rather than merely write an error string.
The pilot should measure more than pass rate. Record proof-generation latency, verifier latency, storage overhead, false rejection rate, time to classify injected incidents and human review effort. Compare those measures with a control condition using well-structured conventional logs. The five-chain proposal earns adoption only if it improves decisions under failure, not because its cryptography looks sophisticated.
Full tamper matrix and what each test establishes
| Mode | Injected condition | Expected failure | What the failure means |
|---|---|---|---|
unsigned-byte | Readback amount changed without re-signing | Hash, signature and semantic mismatch | Byte integrity and postcondition both fail. |
policy-splice | Authority chain re-signed for another digest | Cross-chain action mismatch | A compromised authority producer cannot splice itself into this package unnoticed. |
stale-evidence | Evidence expiry moved before dispatch | Freshness violation | Authentic evidence is not temporally entitled. |
duplicate-key | Dispatch uses a different idempotency key | Execution semantic mismatch | The retry is a new action, not continuation of the authorised one. |
self-readback | Outcome record claims the execution signer | Signer-role violation | The effect lacks the configured independent observation path. |
Cross-chain proof will reduce expensive manual reconstruction
The proposal predicts that systems which bind five independent chains to one typed action will require fewer human-hours to determine whether a disputed effect was authorised, duplicated, stale or mis-observed. Evidence would strengthen the claim if blinded incident teams reach correct classifications faster with proof packages than with conventional logs. It would weaken the claim if package generation creates equivalent ambiguity, or if operators routinely bypass the effect boundary. A rival explanation is simply better structured logging. The smallest useful study compares both designs on the same injected failures.
Implementation profile for a production pilot
Use a versioned action schema with explicit semantic compatibility rules. Canonicalise with JCS or deterministic CBOR. Sign records with COSE or DSSE using keys issued for one chain role. Propagate trace context for diagnosis, but never treat it as the proof join. Store chain records in append-only repositories with retention, key-rotation and revocation metadata. Create a join manifest after readback, register it with a transparency service when non-repudiation or cross-organisation audit matters, and retain the receipt with the package.
Place a policy enforcement point at the irreversible boundary. It should refuse dispatch unless identity, evidence and authority records validate for the action digest and current time. After dispatch, persist effect-unknown before returning control to the agent. Permit release from that state only through readback, idempotent reconciliation or compensation. Monitor asset-level invariants outside the proof system to detect unlogged bypasses.
sequenceDiagram participant Q as Question participant M as Mechanism participant E as Evidence participant D as Decision Q->>M: Five true records and one false conclusion M->>E: The identical debit E-->>M: compare rival explanations E->>D: Require all five chains before the effect D-->>Q: revise scope or act
Require all five chains before the effect
When a team asks whether an agent action is auditable, stop asking how many logs, traces or approval screens exist. Ask whether an independent verifier can reconstruct one typed action and prove five joins: the acting identity was entitled; the evidence snapshot was permitted and fresh; the exact action received live authority; the declared request crossed the effect boundary once; and a separate path verified the postcondition.
That test changes architecture reviews. A missing chain is not “future observability work”. It is a missing premise in the governance claim. A shared trace ID is not evidence that policy and execution agreed. A signed gateway receipt is not a verified outcome. A transparency receipt is not proof that the issuer told the truth. A human approval is not reusable authority for a mutated action.
The five-chain ledger does not make systems infallible. It makes a narrower and more valuable improvement: it converts a persuasive operational story into a set of checkable commitments, and it makes the gaps visible before they become somebody else’s loss.
The practical threshold is simple. When one missing or mismatched premise would change whether an effect is permitted, that premise belongs in the proof path before release, not in an incident checklist after release.
Glossary
- Action projection
- The material fields that must remain unchanged for an existing authority decision to apply.
- Action digest
- A domain-separated cryptographic hash of the canonical action projection, used as the cross-chain commitment.
- Chain head
- The hash of the latest record in one append-only sequence, committing to that chain’s supplied history.
- Effect boundary
- The enforcement point beyond which a request can create an externally consequential state change.
- Effect receipt
- An executor’s signed acknowledgement of a request or effect identifier. It is not, by itself, independent outcome evidence.
- Join manifest
- A record that binds one action digest to the five current chain heads.
- Readback
- An observation of authoritative post-action state through a path meaningfully independent of execution.
- Transparency receipt
- A verifiable proof that a signed statement was registered in a declared verifiable data structure.
References
- W3C. Trace Context. W3C Recommendation.
- OpenTelemetry. Context propagation.
- Open Policy Agent. Decision Logs.
- SPIFFE. SPIFFE Identity and Verifiable Identity Document.
- IETF. RFC 9334: Remote ATtestation procedureS (RATS) Architecture.
- IETF. RFC 9711: The Entity Attestation Token (EAT).
- W3C. PROV-O: The PROV Ontology.
- in-toto. Link attestation predicate.
- SLSA. Build provenance specification v1.2.
- Secure Systems Lab. Dead Simple Signing Envelope.
- RFC Editor. RFC 8785: JSON Canonicalization Scheme.
- IETF. RFC 9052: CBOR Object Signing and Encryption structures and process.
- IETF. RFC 9162: Certificate Transparency Version 2.0.
- IETF. RFC 9942: COSE Receipts.
- IETF. RFC 9943: An Architecture for Trustworthy and Transparent Digital Supply Chains.
- NIST. SP 800-207: Zero Trust Architecture.