Receipt Schema
An enforcement receipt is a signed record documenting a governance decision. This specification defines the required fields, their semantics, and the signing process.
What are the required fields?
Every receipt must contain these fields. Missing or malformed fields invalidate the receipt during verification.
{
// Identity
"receipt_v": "1", // Schema version
"receipt_id": "sha256:abc123...", // Content-addressed ID
"run_id": "run_xyz789", // Parent run identifier
// Sequencing
"counter": 42, // Monotonic per run_id
"timestamp": "2024-01-15T10:30:00Z", // Event time (ISO 8601 UTC)
// Event classification
"event_type": "ENFORCED", // What happened
// Decision details
"decision": {
"action": "BLOCK_EXECUTION", // What action taken
"reason_code": "DRIFT_DETECTED", // Why
"details": "Config hash mismatch" // Human-readable
},
// Policy binding
"policy": {
"policy_id": "sha256:def456..." // Which policy governed
},
// Measurement (if applicable)
"measurement": {
"composite_hash": "sha256:...", // Current state hash
"mismatched_paths": ["config.json"] // What differed
},
// Chain linkage
"chain": {
"prev_receipt_hash": "sha256:...", // Previous receipt
"this_receipt_hash": "sha256:..." // This receipt
},
// Signature
"signer": {
"public_key": "base64:...",
"key_id": "a1b2c3d4e5f67890",
"signature": "base64:..."
}
}What event types are defined?
The event_type field classifies what happened. Each type has specific semantics:
POLICY_LOADEDPolicy artifact validated and activated at run start
MEASUREMENT_OKPeriodic measurement matched baseline (no drift)
DRIFT_DETECTEDMeasurement differed from baseline
ENFORCEDEnforcement action executed (CONTINUE, BLOCK, TERMINATE)
BUNDLE_EXPORTEDEvidence bundle created and exported
CHECKPOINTPeriodic checkpoint with chain summary
How is receipt_id computed?
The receipt_id is a content-addressed hash that uniquely identifies the receipt content.
// Step 1: Prepare content receipt_for_id = Receipt EXCLUDING signer.signature EXCLUDING receipt_id // Step 2: Canonicalize canonical_bytes = canonicalize(receipt_for_id) // Step 3: Hash receipt_id = "sha256:" + HEX(SHA-256(canonical_bytes))
How is this_receipt_hash computed?
The this_receipt_hash is used for chain linkage. It's computed differently from receipt_id:
// Step 1: Prepare content receipt_for_hash = Receipt EXCLUDING signer.signature EXCLUDING chain.this_receipt_hash // Step 2: Canonicalize canonical_bytes = canonicalize(receipt_for_hash) // Step 3: Hash this_receipt_hash = "sha256:" + HEX(SHA-256(canonical_bytes))
How is the signature computed?
The signature covers the complete receipt except the signature itself:
// Step 1: Set receipt_id and this_receipt_hash // Step 2: Prepare signing content signing_content = Receipt INCLUDING receipt_id INCLUDING chain.this_receipt_hash EXCLUDING signer.signature // Step 3: Canonicalize canonical_bytes = canonicalize(signing_content) // Step 4: Sign signer.signature = BASE64(Ed25519_sign(private_key, canonical_bytes)) // Step 5: Set key_id signer.key_id = HEX(SHA-256(public_key_bytes))[0:16]
What are the timestamp requirements?
Timestamps must follow strict formatting for deterministic verification:
- ■Format: ISO 8601 with "Z" suffix (UTC)
- ■Precision: Milliseconds recommended, seconds minimum
- ■TSA tokens: Optional but recommended for high-assurance deployments
- ■Degraded mode: If TSA unavailable, mark timestamp as DEGRADED_LOCAL
Frequently asked questions
Can I add custom fields?
Yes, in the "extensions" object. Custom fields in the root may affect hash computation and break verification in other implementations.
What if measurement fails?
Set measurement.composite_hash to null and include an error in decision.details. The receipt still gets signed and chained, documenting the failure.
Why separate receipt_id and this_receipt_hash?
receipt_id identifies the receipt content. this_receipt_hash is for chain linkage and excludes itself to avoid circular dependency. Both serve verification purposes.