Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,15 @@ Keep the verification results separate:
| Action issuance evidence | Canonical action digest, receipt signature, trusted issuer key, session or call binding, chain order | Successful physical completion |
| Outcome evidence | Controller or monitor decision carried by the receipt payload | Functional-safety certification unless the issuer and profile explicitly claim it |

For action receipts, a verifier should distinguish four common outcomes:
For action receipts, a verifier should distinguish five common outcomes:

| Outcome | Meaning |
|---|---|
| `receipt_valid_accepted` | The receipt is well-formed, trusted, bound to the call, and reports acceptance. |
| `receipt_valid_rejected` | The receipt is well-formed, trusted, bound to the call, and reports controller or policy rejection. This is valid negative evidence. |
| `receipt_missing_required` | The profile required a receipt, but none was present for the consequential action. |
| `receipt_invalid` | The receipt is present but fails signature, digest, issuer, freshness, ordering, or call-binding checks. |
| `receipt_invalid` | The receipt is present but fails signature, digest, freshness, ordering, or call-binding checks against a key the verifier holds. |
| `receipt_unverified` | The receipt names an issuer key the verifier has not pinned, and nothing else failed. Per section 3.3.1 of the spec this is unverified, not invalid: the receipt confers no trust and proves no wrongdoing, surfaced with an advisory rather than a failure. |

The key boundary is that a valid rejection is not malformed evidence. It is
evidence that the downstream authority declined the action. A valid acceptance
Expand Down
20 changes: 20 additions & 0 deletions examples/action-receipts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ three operations:
| `07-receipt-chain-gap.json` | `receipt_invalid` | unknown | The receipt does not link to the expected predecessor. |
| `08-same-party-self-report.json` | `receipt_valid_accepted` with warning | `accepted` | The evidence verifies, but the issuer is not independent from the gateway. |
| `09-unsupported-physical-completion.json` | `receipt_invalid` | unknown | Base TRACE cannot verify the asserted physical-completion claim. |
| `10-action-ref-not-recomputable.json` | `receipt_invalid` | unknown | The declared `action_ref` is not the digest of its own preimage, so it binds nothing. |
| `11-call-id-mismatch.json` | `receipt_invalid` | unknown | An authentic receipt bound to a different call. |
| `12-session-id-mismatch.json` | `receipt_invalid` | unknown | An authentic receipt from a different session. |
| `13-evidence-hash-mismatch.json` | `receipt_invalid` | unknown | The receipt is authentic but the detached evidence was swapped after signing. |
| `14-receipt-issuer-key-unknown.json` | `receipt_unverified` with advisory | unknown | The issuer key is not in the verifier's pinned set. Unverifiable is not invalid (spec §3.3.1): no trust is conferred and no forgery is proven, surfaced as an `issuer_key_unknown` advisory. |
| `15-receipt-from-future.json` | `receipt_invalid` | unknown | Issued after the verification time, so an upper bound on age never rejects it. |
| `16-decision-not-in-enum.json` | `receipt_invalid` | unknown | An unrecognised decision verb, which must not read as accept or reject. |

Fixtures `10`–`16` each pin down one rule that the verifier applies and that no fixture
previously exercised. Every one was a check a conforming implementation could have
omitted entirely while passing this set. Two matter beyond tidiness: without
`issuer_key_unknown` a receipt authenticates itself — a signature verifies against
whatever key it names, and only the pinned set decides which keys the verifier can
check at all; per spec §3.3.1 the outcome is `receipt_unverified`, not
`receipt_invalid`, but a verifier that never consults its pinned set would report such
a receipt as fully valid, which is what the vector distinguishes. Without
`evidence_hash_mismatch` the signature covers a digest whose document may have been
replaced. They pin their own deterministic test key, since the private half of the key
used by `01`–`09` is not published; `gen_rule_coverage_vectors.py` regenerates them
byte-for-byte and only public JWKs appear in the files.

`tests/test_action_receipt_fixtures.py` recomputes each digest, verifies each
signature against the pinned key, checks session and call binding, enforces
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "action-ref-not-recomputable",
"description": "The action's declared action_ref is not the digest of its own canonical preimage. A verifier that trusts the declared value instead of recomputing it would accept an action reference that binds nothing.",
"profile": "trace.action_receipt.conformance.v0",
"context": {
"call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"require_receipt": true,
"verification_time": "2026-07-06T15:24:00Z",
"max_receipt_age_seconds": 300,
"expected_previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"action": {
"agent_id": "spiffe://factory.example/agent/ros2-fibonacci/dev",
"action_type": "ros2.action.example_interfaces/Fibonacci",
"action_scope": "/abort_fibonacci_process",
"action_timestamp": "2026-07-06T15:22:13Z",
"action_ref": "sha256:dededededededededededededededededededededededededededededededede"
},
"trusted_issuer_keys": {
"did:web:factory.example:safety-controller#ed25519-coverage-2026q3": {
"kty": "OKP",
"crv": "Ed25519",
"x": "GUrISuzIssGcot1U1RFv02lETpxCYoQ0tz421ALSdqE"
}
},
"evidence": {
"terminal_state": "accepted",
"physical_completion_claim": "none",
"completeness_claim": "not_proven"
},
"receipt": {
"issuer": "did:web:factory.example:safety-controller",
"issuer_key_id": "did:web:factory.example:safety-controller#ed25519-coverage-2026q3",
"issuer_independence": "separate_process",
"linked_call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"action_ref": "sha256:dededededededededededededededededededededededededededededededede",
"evidence_type": "application/vnd.agentrust.action-receipt+json",
"evidence_hash": "sha256:cf43260df01d89b5ce9ed0dce484ae1e3331fb477efda92cd0fd9365df9089d7",
"previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
"issued_at": "2026-07-06T15:22:15Z",
"decision": "accepted",
"signature": "Cbe4Tbr1XBZLqwnLTlel_ABjjQecA5-E0ygcI9qMP9xf0z8CmMKrGL8uIZqBlV3bKE4CY2IJRUH_WCoMnn2_BQ"
},
"expected": {
"status": "receipt_invalid",
"controller_outcome": "unknown",
"failures": [
"action_ref_invalid"
],
"warnings": []
}
}
54 changes: 54 additions & 0 deletions examples/action-receipts/conformance/11-call-id-mismatch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "call-id-mismatch",
"description": "An authentic receipt bound to a different call. Without this check a valid receipt from one call could be presented as evidence for another.",
"profile": "trace.action_receipt.conformance.v0",
"context": {
"call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"require_receipt": true,
"verification_time": "2026-07-06T15:24:00Z",
"max_receipt_age_seconds": 300,
"expected_previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"action": {
"agent_id": "spiffe://factory.example/agent/ros2-fibonacci/dev",
"action_type": "ros2.action.example_interfaces/Fibonacci",
"action_scope": "/abort_fibonacci_process",
"action_timestamp": "2026-07-06T15:22:13Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034"
},
"trusted_issuer_keys": {
"did:web:factory.example:safety-controller#ed25519-coverage-2026q3": {
"kty": "OKP",
"crv": "Ed25519",
"x": "GUrISuzIssGcot1U1RFv02lETpxCYoQ0tz421ALSdqE"
}
},
"evidence": {
"terminal_state": "accepted",
"physical_completion_claim": "none",
"completeness_claim": "not_proven"
},
"receipt": {
"issuer": "did:web:factory.example:safety-controller",
"issuer_key_id": "did:web:factory.example:safety-controller#ed25519-coverage-2026q3",
"issuer_independence": "separate_process",
"linked_call_id": "01986d7c-0000-7c68-9ff8-000000000000",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034",
"evidence_type": "application/vnd.agentrust.action-receipt+json",
"evidence_hash": "sha256:cf43260df01d89b5ce9ed0dce484ae1e3331fb477efda92cd0fd9365df9089d7",
"previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
"issued_at": "2026-07-06T15:22:15Z",
"decision": "accepted",
"signature": "X-kzP-GgEpcy30o8jzylNKN_QU86WldO4vN9mHziBnuOo_vGKio1_oRa1voVQWN396wQ82HCMH3vTTm9aVrKCg"
},
"expected": {
"status": "receipt_invalid",
"controller_outcome": "unknown",
"failures": [
"call_id_mismatch"
],
"warnings": []
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "session-id-mismatch",
"description": "An authentic receipt from a different session. Session binding is what stops a receipt being replayed into an unrelated run.",
"profile": "trace.action_receipt.conformance.v0",
"context": {
"call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"require_receipt": true,
"verification_time": "2026-07-06T15:24:00Z",
"max_receipt_age_seconds": 300,
"expected_previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"action": {
"agent_id": "spiffe://factory.example/agent/ros2-fibonacci/dev",
"action_type": "ros2.action.example_interfaces/Fibonacci",
"action_scope": "/abort_fibonacci_process",
"action_timestamp": "2026-07-06T15:22:13Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034"
},
"trusted_issuer_keys": {
"did:web:factory.example:safety-controller#ed25519-coverage-2026q3": {
"kty": "OKP",
"crv": "Ed25519",
"x": "GUrISuzIssGcot1U1RFv02lETpxCYoQ0tz421ALSdqE"
}
},
"evidence": {
"terminal_state": "accepted",
"physical_completion_claim": "none",
"completeness_claim": "not_proven"
},
"receipt": {
"issuer": "did:web:factory.example:safety-controller",
"issuer_key_id": "did:web:factory.example:safety-controller#ed25519-coverage-2026q3",
"issuer_independence": "separate_process",
"linked_call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T09:00:00Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034",
"evidence_type": "application/vnd.agentrust.action-receipt+json",
"evidence_hash": "sha256:cf43260df01d89b5ce9ed0dce484ae1e3331fb477efda92cd0fd9365df9089d7",
"previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
"issued_at": "2026-07-06T15:22:15Z",
"decision": "accepted",
"signature": "lvIqDgwunh5ou0AIwNuJyDIXGYv7hGH_-S2KuAZzB8nwnER-_ee2QuTzTspT4zlVCRfA_O9UiUFkfFBGLt4rBA"
},
"expected": {
"status": "receipt_invalid",
"controller_outcome": "unknown",
"failures": [
"session_id_mismatch"
],
"warnings": []
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "evidence-hash-mismatch",
"description": "The receipt is authentic but its evidence_hash does not match the detached evidence supplied with it. The signature covers the digest, not the document, so only recomputation catches a swapped evidence body.",
"profile": "trace.action_receipt.conformance.v0",
"context": {
"call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"require_receipt": true,
"verification_time": "2026-07-06T15:24:00Z",
"max_receipt_age_seconds": 300,
"expected_previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"action": {
"agent_id": "spiffe://factory.example/agent/ros2-fibonacci/dev",
"action_type": "ros2.action.example_interfaces/Fibonacci",
"action_scope": "/abort_fibonacci_process",
"action_timestamp": "2026-07-06T15:22:13Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034"
},
"trusted_issuer_keys": {
"did:web:factory.example:safety-controller#ed25519-coverage-2026q3": {
"kty": "OKP",
"crv": "Ed25519",
"x": "GUrISuzIssGcot1U1RFv02lETpxCYoQ0tz421ALSdqE"
}
},
"evidence": {
"terminal_state": "rejected",
"physical_completion_claim": "none",
"completeness_claim": "not_proven"
},
"receipt": {
"issuer": "did:web:factory.example:safety-controller",
"issuer_key_id": "did:web:factory.example:safety-controller#ed25519-coverage-2026q3",
"issuer_independence": "separate_process",
"linked_call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034",
"evidence_type": "application/vnd.agentrust.action-receipt+json",
"evidence_hash": "sha256:cf43260df01d89b5ce9ed0dce484ae1e3331fb477efda92cd0fd9365df9089d7",
"previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
"issued_at": "2026-07-06T15:22:15Z",
"decision": "accepted",
"signature": "f1bVg8ZaBv6SaT5JGzs0J4XkzyVkvhS5wGNit-790NFeHAGLfw1kalsiqBxGC8HFNUVjHRcW2urOPl8A90sbDw"
},
"expected": {
"status": "receipt_invalid",
"controller_outcome": "unknown",
"failures": [
"evidence_hash_mismatch"
],
"warnings": []
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "receipt-issuer-key-unknown",
"description": "The receipt names an issuer key the verifier has not pinned. A signature verifies against whatever key it names; only a pinned set decides whether that key was ever entitled to speak. Not holding the key is an inability to check, not evidence of forgery: the receipt is unverified, not invalid.",
"profile": "trace.action_receipt.conformance.v0",
"context": {
"call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"require_receipt": true,
"verification_time": "2026-07-06T15:24:00Z",
"max_receipt_age_seconds": 300,
"expected_previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"action": {
"agent_id": "spiffe://factory.example/agent/ros2-fibonacci/dev",
"action_type": "ros2.action.example_interfaces/Fibonacci",
"action_scope": "/abort_fibonacci_process",
"action_timestamp": "2026-07-06T15:22:13Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034"
},
"trusted_issuer_keys": {
"did:web:factory.example:safety-controller#ed25519-some-other-key": {
"kty": "OKP",
"crv": "Ed25519",
"x": "GUrISuzIssGcot1U1RFv02lETpxCYoQ0tz421ALSdqE"
}
},
"evidence": {
"terminal_state": "accepted",
"physical_completion_claim": "none",
"completeness_claim": "not_proven"
},
"receipt": {
"issuer": "did:web:factory.example:safety-controller",
"issuer_key_id": "did:web:factory.example:safety-controller#ed25519-coverage-2026q3",
"issuer_independence": "separate_process",
"linked_call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034",
"evidence_type": "application/vnd.agentrust.action-receipt+json",
"evidence_hash": "sha256:cf43260df01d89b5ce9ed0dce484ae1e3331fb477efda92cd0fd9365df9089d7",
"previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
"issued_at": "2026-07-06T15:22:15Z",
"decision": "accepted",
"signature": "f1bVg8ZaBv6SaT5JGzs0J4XkzyVkvhS5wGNit-790NFeHAGLfw1kalsiqBxGC8HFNUVjHRcW2urOPl8A90sbDw"
},
"expected": {
"status": "receipt_unverified",
"controller_outcome": "unknown",
"failures": [],
"warnings": [
"issuer_key_unknown"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "receipt-from-future",
"description": "The receipt is issued after the verification time. A future-dated receipt is not stale, so the freshness ceiling alone never rejects it, and a verifier that only checks an upper bound on age accepts it.",
"profile": "trace.action_receipt.conformance.v0",
"context": {
"call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"require_receipt": true,
"verification_time": "2026-07-06T15:24:00Z",
"max_receipt_age_seconds": 300,
"expected_previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1"
},
"action": {
"agent_id": "spiffe://factory.example/agent/ros2-fibonacci/dev",
"action_type": "ros2.action.example_interfaces/Fibonacci",
"action_scope": "/abort_fibonacci_process",
"action_timestamp": "2026-07-06T15:22:13Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034"
},
"trusted_issuer_keys": {
"did:web:factory.example:safety-controller#ed25519-coverage-2026q3": {
"kty": "OKP",
"crv": "Ed25519",
"x": "GUrISuzIssGcot1U1RFv02lETpxCYoQ0tz421ALSdqE"
}
},
"evidence": {
"terminal_state": "accepted",
"physical_completion_claim": "none",
"completeness_claim": "not_proven"
},
"receipt": {
"issuer": "did:web:factory.example:safety-controller",
"issuer_key_id": "did:web:factory.example:safety-controller#ed25519-coverage-2026q3",
"issuer_independence": "separate_process",
"linked_call_id": "01986d7c-6b2f-7c68-9ff8-3e2f9d0db337",
"session_id": "trace-session-2026-07-06T15:22:11Z",
"action_ref": "sha256:c2e97956de8b86d825780fcecea601a6eea02616855e6f0aa19d10bd351fe034",
"evidence_type": "application/vnd.agentrust.action-receipt+json",
"evidence_hash": "sha256:cf43260df01d89b5ce9ed0dce484ae1e3331fb477efda92cd0fd9365df9089d7",
"previous_receipt_hash": "sha256:c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
"issued_at": "2026-07-06T15:26:00Z",
"decision": "accepted",
"signature": "RAYxeAudhBQ9tiJ-XspITdRS7CgZE49pY13yLi9az8QR3UpmWcX5JJKah4rSvmq93DojaT4ODzLpM6tA0v4GCA"
},
"expected": {
"status": "receipt_invalid",
"controller_outcome": "unknown",
"failures": [
"receipt_from_future"
],
"warnings": []
}
}
Loading
Loading