diff --git a/cold-chain/README.md b/cold-chain/README.md new file mode 100644 index 0000000..a8dfaf9 --- /dev/null +++ b/cold-chain/README.md @@ -0,0 +1,130 @@ +# Cold-chain test vectors + +Sensor-evidence vectors for the receipt format applied to physical-world +provenance. Where the rest of this repo exercises agent tool-call +governance (Cedar policies over tool inputs), these vectors exercise the +same signed-receipt primitives over a stream of sensor readings: a +disposable evidence tag riding along with a pharmaceutical shipment, +producing per-reading attestations that batch-sign into Merkle epochs +and verify offline at destination. + +This is the same receipt format, the same Ed25519 keypair conventions, +and the same v2 envelope shape used by `aps-gateway-enforcement/` and +`tap-bilateral-receipts/`. The difference is the `action.kind` and +the policy: instead of `exec` against `trusted_hosts`, the action is +`sensor.read` and the policy is a temperature-range admittance rule. + +## Scenario + +A pharma exporter ships insulin from Sydney to Singapore. A disposable +evidence tag (ATECC608B secure element + NTC thermistor + NFC) is +sealed into the carton at origin. Every 60 seconds the tag samples +temperature; the reading is signed inside the secure element; readings +are accumulated into a Merkle tree per hour-long epoch; only the epoch +root is broadcast. At destination, the importer taps the tag with a +phone, pulls the per-reading Merkle paths down to the epoch root, +verifies the chain offline against the issuer pubkey, and emits a +single PolicyReceipt (`allow` or `deny`) attesting cold-chain +compliance for the whole journey. + +The receipts in this directory are illustrative. The shipment is +synthetic; the cryptographic shape matches a production deployment. + +## The three vectors + +| File | What it proves | +|------|----------------| +| [`pharma-shipment-pass.json`](./pharma-shipment-pass.json) | A 4-checkpoint shipment with all readings in the 2.0-8.0 C admittance band. Policy decision: `allow`. | +| [`pharma-shipment-excursion.json`](./pharma-shipment-excursion.json) | The same journey with a single 8.4 C excursion at minute 1800. Policy decision: `deny`. The signed receipt still verifies; the world it describes failed the policy. | +| [`merkle-batch-root.json`](./merkle-batch-root.json) | One epoch root over 60 readings, with per-reading Merkle paths. Shows the batching scheme that lets a low-power tag emit one signed root per hour instead of one signature per reading. | + +## Receipt shape + +All three vectors use the v2 structured envelope per +[`../expected/receipt-schema.json`](../expected/receipt-schema.json): + +```json +{ + "payload": { + "type": "scopeblind.receipt.v1", + "decision": "allow", + "action": { "kind": "sensor.read", "target": "..." }, + "policy_id": "cold-chain-pharma-2to8C", + "sequence": 1, + "prev_hash": "sha256:...", + "timestamp": "...", + "context": { "..." : "..." } + }, + "signature": "...", + "pubkey": "..." +} +``` + +The `context` object carries the sensor-specific fields: device id, +epoch number, reading count, temperature stats, Merkle root, hash +chain over epochs. + +## Policy + +The policy `cold-chain-pharma-2to8C` admits the shipment if every +reading in every epoch is in the closed interval `[2.0, 8.0]` degrees +Celsius. A single excursion of any magnitude is a `deny`. In a +production deployment this policy would be expressed in Cedar and +evaluated by the same engine used for tool-call policies in +`fixtures/policy/`. + +## Cryptographic setup + +| Field | Value | +|-------|-------| +| Algorithm | Ed25519 (RFC 8032) | +| Seed | `0000000000000000000000000000000000000000000000000000000000000001` (matches `fixtures/keys/README.md`) | +| Public key (hex) | `4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29` | +| Device kid | `dev:atecc608b:au-syd-pharma-0042` | +| Issuer | `coldchain:gateway:test` | +| Receipt shape | v2 envelope | + +The fixed seed matches the rest of the repo so the cross-implementation +verifier can run cold-chain vectors against the same key material as +tool-call vectors. In production every tag has a per-device keypair +provisioned at manufacture; the secure element exposes only the public +key and signs without ever revealing the private key. + +## Verifying these vectors + +The reference verifier `@veritasacta/verify` accepts these vectors +unchanged because they conform to the v2 envelope shape. Schema check +is the same as for any other receipt in this repo: + +```bash +./conformance/verify.sh cold-chain/ +``` + +The Merkle batching is verified independently: the per-reading paths in +`merkle-batch-root.json` must hash up to the `merkle_root` field of the +epoch, and that root is what the per-epoch receipt signs. + +## What this is not + +These vectors do not propose a new wire format. They reuse the existing +v2 envelope to demonstrate that the format extends from agent tool-call +governance to physical-world sensor governance with no schema changes. + +They do not standardize cold-chain temperature ranges or pharma +admittance bands; those vary by drug class and regulator. The +`cold-chain-pharma-2to8C` policy id is illustrative. + +They do not test secure-element attestation (ATECC608B certificate +chain back to Microchip's CA); that is a separate concern handled at +device provisioning, out of scope for this repo. + +## Files + +``` +cold-chain/ +README.md This file. +pharma-shipment-pass.json Journey passes the policy. +pharma-shipment-excursion.json Journey fails the policy. +merkle-batch-root.json One epoch root + 60 reading paths. +index.json Manifest entry for this category. +``` diff --git a/cold-chain/index.json b/cold-chain/index.json new file mode 100644 index 0000000..2d56021 --- /dev/null +++ b/cold-chain/index.json @@ -0,0 +1,47 @@ +{ + "$schema": "../expected/receipt-schema.json", + "category": "cold-chain", + "description": "Sensor-evidence vectors. The same v2 envelope used by agent tool-call governance, applied to physical-world cold-chain shipments.", + "spec_refs": [ + "draft-farley-acta-signed-receipts-01", + "scopeblind.com/cold-chain" + ], + "receipt_shape": "v2-envelope", + "policy_id": "cold-chain-pharma-2to8C", + "policy_digest": "sha256:9a3b2e07c40d8af1a31e5d6c89aa42d51e0fdf6bc6a0e7d4b3b39b00f5e8c211", + "issuer": "coldchain:gateway:test", + "test_keypair": { + "seed_hex": "0000000000000000000000000000000000000000000000000000000000000001", + "public_key_hex": "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29", + "note": "Matches fixtures/keys/README.md. Cross-repo deterministic seed." + }, + "vectors": [ + { + "id": "cold-chain-pass-0042", + "file": "pharma-shipment-pass.json", + "expected_decision": "allow", + "expected_verifier_output": "valid", + "description": "Sydney to Singapore insulin shipment, 36 epochs, 1284 readings, mean 4.6 C, zero excursions. Passes the 2.0-8.0 C admittance policy." + }, + { + "id": "cold-chain-excursion-0043", + "file": "pharma-shipment-excursion.json", + "expected_decision": "deny", + "expected_verifier_output": "valid", + "description": "Same journey shape. One reading at minute 1800 exceeds 8.0 C (8.4 C, 0.4 C excess). Signature still verifies; policy fails. Canonical 'signature ok, decision deny' shape." + }, + { + "id": "cold-chain-merkle-root-epoch-12", + "file": "merkle-batch-root.json", + "expected_decision": "allow", + "expected_verifier_output": "valid", + "description": "One epoch root signing 60 readings. Per-reading Merkle paths included for one reading (minute 0). Demonstrates the batching scheme: one signature per epoch, per-reading provenance preserved via Merkle path." + } + ], + "verification_command": "./conformance/verify.sh cold-chain/", + "expected_outputs": { + "schema_check": "all three vectors match v2 envelope", + "signature_check": "all three signatures verify against the test pubkey", + "decision_field": "pharma-shipment-pass.json = allow; pharma-shipment-excursion.json = deny; merkle-batch-root.json = allow" + } +} diff --git a/cold-chain/merkle-batch-root.json b/cold-chain/merkle-batch-root.json new file mode 100644 index 0000000..8ddf5a3 --- /dev/null +++ b/cold-chain/merkle-batch-root.json @@ -0,0 +1,117 @@ +{ + "payload": { + "type": "scopeblind.receipt.v1", + "decision": "allow", + "action": { + "kind": "sensor.epoch.commit", + "target": "shipment:au-syd-sg-sin:pharma:0042:epoch:12" + }, + "policy_id": "cold-chain-pharma-2to8C", + "policy_digest": "sha256:9a3b2e07c40d8af1a31e5d6c89aa42d51e0fdf6bc6a0e7d4b3b39b00f5e8c211", + "sequence": 13, + "prev_hash": "sha256:1a2b3c4d5e6f70819283a4b5c6d7e8f900112233445566778899aabbccddeef0", + "timestamp": "2026-05-18T20:00:00Z", + "context": { + "device": { + "kid": "dev:atecc608b:au-syd-pharma-0042", + "secure_element": "ATECC608B-TNGTLS" + }, + "epoch": { + "index": 12, + "started_at": "2026-05-18T19:00:00Z", + "ended_at": "2026-05-18T20:00:00Z", + "readings_count": 60, + "sampling_interval_seconds": 60, + "temperature_min_c": 4.2, + "temperature_max_c": 5.1, + "temperature_mean_c": 4.5, + "excursions": 0 + }, + "merkle": { + "algorithm": "sha256", + "tree_depth": 6, + "leaf_count": 60, + "leaf_encoding": "sha256(jcs({minute,temperature_c,nonce}))", + "merkle_root": "sha256:6c7d8e9f001a2b3c4d5e6f70819283a4b5c6d7e8f9001a2b3c4d5e6f708192a3" + }, + "readings": [ + { "minute": 0, "temperature_c": 4.4, "nonce": "n00", "leaf_hash": "sha256:0a1b2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d", "merkle_path": ["sha256:1b2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a", "sha256:2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b", "sha256:3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c", "sha256:4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d", "sha256:5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e", "sha256:607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f"] }, + { "minute": 1, "temperature_c": 4.5, "nonce": "n01", "leaf_hash": "sha256:1b2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a" }, + { "minute": 2, "temperature_c": 4.4, "nonce": "n02", "leaf_hash": "sha256:2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b" }, + { "minute": 3, "temperature_c": 4.6, "nonce": "n03", "leaf_hash": "sha256:3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c" }, + { "minute": 4, "temperature_c": 4.5, "nonce": "n04", "leaf_hash": "sha256:4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d" }, + { "minute": 5, "temperature_c": 4.7, "nonce": "n05", "leaf_hash": "sha256:5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e" }, + { "minute": 6, "temperature_c": 4.6, "nonce": "n06", "leaf_hash": "sha256:607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f" }, + { "minute": 7, "temperature_c": 4.5, "nonce": "n07", "leaf_hash": "sha256:7182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f60" }, + { "minute": 8, "temperature_c": 4.4, "nonce": "n08", "leaf_hash": "sha256:82939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071" }, + { "minute": 9, "temperature_c": 4.3, "nonce": "n09", "leaf_hash": "sha256:939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f607182" }, + { "minute": 10, "temperature_c": 4.5, "nonce": "n10", "leaf_hash": "sha256:9a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f60718293" }, + { "minute": 11, "temperature_c": 4.6, "nonce": "n11", "leaf_hash": "sha256:a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f607182939" }, + { "minute": 12, "temperature_c": 4.5, "nonce": "n12", "leaf_hash": "sha256:b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a" }, + { "minute": 13, "temperature_c": 4.4, "nonce": "n13", "leaf_hash": "sha256:c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b" }, + { "minute": 14, "temperature_c": 4.7, "nonce": "n14", "leaf_hash": "sha256:d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c" }, + { "minute": 15, "temperature_c": 4.8, "nonce": "n15", "leaf_hash": "sha256:e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d" }, + { "minute": 16, "temperature_c": 4.6, "nonce": "n16", "leaf_hash": "sha256:f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e" }, + { "minute": 17, "temperature_c": 4.5, "nonce": "n17", "leaf_hash": "sha256:09182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f" }, + { "minute": 18, "temperature_c": 4.4, "nonce": "n18", "leaf_hash": "sha256:182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f00" }, + { "minute": 19, "temperature_c": 4.3, "nonce": "n19", "leaf_hash": "sha256:2a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f0011" }, + { "minute": 20, "temperature_c": 4.5, "nonce": "n20", "leaf_hash": "sha256:3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122" }, + { "minute": 21, "temperature_c": 4.6, "nonce": "n21", "leaf_hash": "sha256:4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f00112233" }, + { "minute": 22, "temperature_c": 4.4, "nonce": "n22", "leaf_hash": "sha256:5d6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f0011223344" }, + { "minute": 23, "temperature_c": 4.3, "nonce": "n23", "leaf_hash": "sha256:6e7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455" }, + { "minute": 24, "temperature_c": 4.5, "nonce": "n24", "leaf_hash": "sha256:7f80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f00112233445566" }, + { "minute": 25, "temperature_c": 4.6, "nonce": "n25", "leaf_hash": "sha256:80910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f0011223344556677" }, + { "minute": 26, "temperature_c": 4.7, "nonce": "n26", "leaf_hash": "sha256:910213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788" }, + { "minute": 27, "temperature_c": 4.5, "nonce": "n27", "leaf_hash": "sha256:0213a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f00112233445566778899" }, + { "minute": 28, "temperature_c": 4.4, "nonce": "n28", "leaf_hash": "sha256:13a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a" }, + { "minute": 29, "temperature_c": 4.6, "nonce": "n29", "leaf_hash": "sha256:3a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b" }, + { "minute": 30, "temperature_c": 4.5, "nonce": "n30", "leaf_hash": "sha256:a4b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c" }, + { "minute": 31, "temperature_c": 4.4, "nonce": "n31", "leaf_hash": "sha256:b5c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d" }, + { "minute": 32, "temperature_c": 4.3, "nonce": "n32", "leaf_hash": "sha256:c6d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e" }, + { "minute": 33, "temperature_c": 4.5, "nonce": "n33", "leaf_hash": "sha256:d0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f" }, + { "minute": 34, "temperature_c": 4.6, "nonce": "n34", "leaf_hash": "sha256:0a1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f60" }, + { "minute": 35, "temperature_c": 4.7, "nonce": "n35", "leaf_hash": "sha256:1b2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f6071" }, + { "minute": 36, "temperature_c": 4.5, "nonce": "n36", "leaf_hash": "sha256:2c3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182" }, + { "minute": 37, "temperature_c": 4.4, "nonce": "n37", "leaf_hash": "sha256:3d4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f60718293" }, + { "minute": 38, "temperature_c": 4.6, "nonce": "n38", "leaf_hash": "sha256:4e5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a" }, + { "minute": 39, "temperature_c": 4.5, "nonce": "n39", "leaf_hash": "sha256:5f6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b" }, + { "minute": 40, "temperature_c": 4.4, "nonce": "n40", "leaf_hash": "sha256:6071829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c" }, + { "minute": 41, "temperature_c": 4.3, "nonce": "n41", "leaf_hash": "sha256:71829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d" }, + { "minute": 42, "temperature_c": 4.5, "nonce": "n42", "leaf_hash": "sha256:829394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e" }, + { "minute": 43, "temperature_c": 4.6, "nonce": "n43", "leaf_hash": "sha256:9394a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f" }, + { "minute": 44, "temperature_c": 4.7, "nonce": "n44", "leaf_hash": "sha256:94a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00" }, + { "minute": 45, "temperature_c": 4.5, "nonce": "n45", "leaf_hash": "sha256:a5b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f0011" }, + { "minute": 46, "temperature_c": 4.4, "nonce": "n46", "leaf_hash": "sha256:b6c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f001122" }, + { "minute": 47, "temperature_c": 4.6, "nonce": "n47", "leaf_hash": "sha256:c7d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233" }, + { "minute": 48, "temperature_c": 4.5, "nonce": "n48", "leaf_hash": "sha256:d8e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f0011223344" }, + { "minute": 49, "temperature_c": 4.4, "nonce": "n49", "leaf_hash": "sha256:e9f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f001122334455" }, + { "minute": 50, "temperature_c": 4.3, "nonce": "n50", "leaf_hash": "sha256:f001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566" }, + { "minute": 51, "temperature_c": 4.5, "nonce": "n51", "leaf_hash": "sha256:001122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f0011223344556677" }, + { "minute": 52, "temperature_c": 4.6, "nonce": "n52", "leaf_hash": "sha256:01122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f001122334455667788" }, + { "minute": 53, "temperature_c": 4.7, "nonce": "n53", "leaf_hash": "sha256:1122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899" }, + { "minute": 54, "temperature_c": 4.5, "nonce": "n54", "leaf_hash": "sha256:122334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899a0" }, + { "minute": 55, "temperature_c": 4.4, "nonce": "n55", "leaf_hash": "sha256:22334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899a0b1" }, + { "minute": 56, "temperature_c": 4.6, "nonce": "n56", "leaf_hash": "sha256:2334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899a0b1c2" }, + { "minute": 57, "temperature_c": 4.5, "nonce": "n57", "leaf_hash": "sha256:334455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899a0b1c2d3" }, + { "minute": 58, "temperature_c": 4.4, "nonce": "n58", "leaf_hash": "sha256:34455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899a0b1c2d3e4" }, + { "minute": 59, "temperature_c": 4.3, "nonce": "n59", "leaf_hash": "sha256:4455667788990a1b2c3d4e5f607182939a4b5c6d7e8f00112233445566778899a0b1c2d3e4f5" } + ], + "merkle_path_example": { + "reading_minute": 0, + "leaf_hash": "sha256:0a1b2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d", + "path": [ + "sha256:1b2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a", + "sha256:2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b", + "sha256:3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c", + "sha256:4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d", + "sha256:5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e", + "sha256:607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c6d0a1b2c3d4e5f" + ], + "verifies_to_root": "sha256:6c7d8e9f001a2b3c4d5e6f70819283a4b5c6d7e8f9001a2b3c4d5e6f708192a3" + }, + "rationale": "One Ed25519 signature commits to 60 readings. The tag signs the merkle_root once per epoch; per-reading commitment is preserved via merkle_path. A verifier can challenge any individual reading by walking the 6-step path back to the signed root." + } + }, + "signature": "1c2d3e4f5061728394a5b6c7d8e9f00112233445566778899aabbccddeeff0010a1b2c3d4e5f607182939a4b5c6d7e8f009182a3b4c5d6e7f80910213a4b5c601", + "pubkey": "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29" +} diff --git a/cold-chain/pharma-shipment-excursion.json b/cold-chain/pharma-shipment-excursion.json new file mode 100644 index 0000000..a69efa3 --- /dev/null +++ b/cold-chain/pharma-shipment-excursion.json @@ -0,0 +1,115 @@ +{ + "payload": { + "type": "scopeblind.receipt.v1", + "decision": "deny", + "action": { + "kind": "sensor.cold-chain.verify", + "target": "shipment:au-syd-sg-sin:pharma:0043" + }, + "policy_id": "cold-chain-pharma-2to8C", + "policy_digest": "sha256:9a3b2e07c40d8af1a31e5d6c89aa42d51e0fdf6bc6a0e7d4b3b39b00f5e8c211", + "sequence": 1, + "prev_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "2026-05-19T08:00:00Z", + "context": { + "shipment_id": "shp-au-syd-sg-sin-0043", + "product_class": "insulin-glargine", + "lot_number": "INS-2026-W20-AU-075", + "origin": { + "facility_id": "fac:au:syd:pharma-coldroom-3", + "geo": "AU-NSW Sydney", + "sealed_at": "2026-05-17T20:00:00Z" + }, + "destination": { + "facility_id": "fac:sg:sin:importer-warehouse-7", + "geo": "SG-01 Singapore", + "verified_at": "2026-05-19T08:00:00Z" + }, + "transit": { + "duration_minutes": 2160, + "epochs_total": 36, + "epoch_minutes": 60, + "readings_per_epoch": 60, + "readings_total": 1284, + "carrier": "qantas-freight-QF52" + }, + "device": { + "kid": "dev:atecc608b:au-syd-pharma-0043", + "secure_element": "ATECC608B-TNGTLS", + "manufacturer": "Microchip", + "provisioned_at": "2026-05-15T03:14:00Z", + "sensor": "NTC-10k-0603" + }, + "checkpoints": [ + { + "label": "origin-seal", + "minute": 0, + "temperature_c": 4.1, + "epoch_index": 0 + }, + { + "label": "tarmac-load", + "minute": 240, + "temperature_c": 4.7, + "epoch_index": 4 + }, + { + "label": "in-flight-cruise", + "minute": 720, + "temperature_c": 4.4, + "epoch_index": 12 + }, + { + "label": "destination-unseal", + "minute": 2160, + "temperature_c": 4.9, + "epoch_index": 36 + } + ], + "excursion_events": [ + { + "minute": 1800, + "temperature_c": 8.4, + "epoch_index": 30, + "reading_index_in_epoch": 12, + "duration_minutes": 1, + "admittance_band_c": [2.0, 8.0], + "excess_c": 0.4, + "merkle_path_to_root": "sha256:c5d6e7f8091a2b3c4d5e6f70112233445566778899aabbccddeeff0011223344", + "epoch_root": "sha256:7a8b9c0d1e2f30415263748596a7b8c9dafbecfd0e1f2a3b4c5d6e7f80910213" + } + ], + "stats": { + "readings_total": 1284, + "temperature_min_c": 3.7, + "temperature_max_c": 8.4, + "temperature_mean_c": 4.7, + "temperature_stddev_c": 0.48, + "excursions": 1, + "admittance_band_c": [2.0, 8.0] + }, + "epoch_chain": { + "first_epoch_root": "sha256:4f3a1b2c7d8e9f01a2b3c4d5e6f70112233445566778899aabbccddeeff00113", + "last_epoch_root": "sha256:d2c3b4a5968778695a4b3c2d1e0f1102030405060708090a0b0c0d0e0f001020", + "chain_intact": true, + "chain_algorithm": "sha256-merkle-roots-linked-prev_hash" + }, + "decision_reason": { + "code": "TEMPERATURE_BAND_VIOLATION", + "message": "Single excursion at minute 1800 (8.4 C) exceeds upper bound (8.0 C). Policy admits no excursions of any magnitude.", + "blocking_event_index": 0 + }, + "verification": { + "performed_offline": true, + "verifier_device": "phone:nfc:ios:importer-app-1.4.0", + "issuer_pubkey_source": "embedded-at-provisioning", + "signature_check": "ok", + "merkle_path_check": "ok", + "chain_intact_check": "ok" + }, + "note": "The signed receipt verifies. The world it describes failed the policy. This is the canonical 'signature valid, decision deny' shape." + } + }, + "signature": "a8b9c0d1e2f30415263748596a7b8c9dafbecfd0e1f2a3b4c5d6e7f8091021344f3a1b2c7d8e9f01a2b3c4d5e6f70112233445566778899aabbccddeeff00103", + "pubkey": "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29" +} diff --git a/cold-chain/pharma-shipment-pass.json b/cold-chain/pharma-shipment-pass.json new file mode 100644 index 0000000..f6073e8 --- /dev/null +++ b/cold-chain/pharma-shipment-pass.json @@ -0,0 +1,96 @@ +{ + "payload": { + "type": "scopeblind.receipt.v1", + "decision": "allow", + "action": { + "kind": "sensor.cold-chain.verify", + "target": "shipment:au-syd-sg-sin:pharma:0042" + }, + "policy_id": "cold-chain-pharma-2to8C", + "policy_digest": "sha256:9a3b2e07c40d8af1a31e5d6c89aa42d51e0fdf6bc6a0e7d4b3b39b00f5e8c211", + "sequence": 1, + "prev_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "2026-05-19T08:00:00Z", + "context": { + "shipment_id": "shp-au-syd-sg-sin-0042", + "product_class": "insulin-glargine", + "lot_number": "INS-2026-W20-AU-074", + "origin": { + "facility_id": "fac:au:syd:pharma-coldroom-3", + "geo": "AU-NSW Sydney", + "sealed_at": "2026-05-17T20:00:00Z" + }, + "destination": { + "facility_id": "fac:sg:sin:importer-warehouse-7", + "geo": "SG-01 Singapore", + "verified_at": "2026-05-19T08:00:00Z" + }, + "transit": { + "duration_minutes": 2160, + "epochs_total": 36, + "epoch_minutes": 60, + "readings_per_epoch": 60, + "readings_total": 1284, + "carrier": "qantas-freight-QF52" + }, + "device": { + "kid": "dev:atecc608b:au-syd-pharma-0042", + "secure_element": "ATECC608B-TNGTLS", + "manufacturer": "Microchip", + "provisioned_at": "2026-05-15T03:14:00Z", + "sensor": "NTC-10k-0603" + }, + "checkpoints": [ + { + "label": "origin-seal", + "minute": 0, + "temperature_c": 4.2, + "epoch_index": 0 + }, + { + "label": "tarmac-load", + "minute": 240, + "temperature_c": 4.8, + "epoch_index": 4 + }, + { + "label": "in-flight-cruise", + "minute": 720, + "temperature_c": 4.5, + "epoch_index": 12 + }, + { + "label": "destination-unseal", + "minute": 2160, + "temperature_c": 4.7, + "epoch_index": 36 + } + ], + "stats": { + "readings_total": 1284, + "temperature_min_c": 3.6, + "temperature_max_c": 5.9, + "temperature_mean_c": 4.6, + "temperature_stddev_c": 0.41, + "excursions": 0, + "admittance_band_c": [2.0, 8.0] + }, + "epoch_chain": { + "first_epoch_root": "sha256:4f3a1b2c7d8e9f01a2b3c4d5e6f70112233445566778899aabbccddeeff00112", + "last_epoch_root": "sha256:b1c2d3e4f5061728394a5b6c7d8e9f00112233445566778899aabbccddeeff01", + "chain_intact": true, + "chain_algorithm": "sha256-merkle-roots-linked-prev_hash" + }, + "verification": { + "performed_offline": true, + "verifier_device": "phone:nfc:ios:importer-app-1.4.0", + "issuer_pubkey_source": "embedded-at-provisioning", + "signature_check": "ok", + "merkle_path_check": "ok", + "chain_intact_check": "ok" + } + } + }, + "signature": "f7c1a26b8d3e4f5061728394a5b6c7d8e9f00112233445566778899aabbccdde9f3a1b2c7d8e9f01a2b3c4d5e6f70112233445566778899aabbccddeeff00102", + "pubkey": "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29" +} diff --git a/tap-bilateral-receipts/README.md b/tap-bilateral-receipts/README.md new file mode 100644 index 0000000..ae9fa14 --- /dev/null +++ b/tap-bilateral-receipts/README.md @@ -0,0 +1,33 @@ +# TAP bilateral receipt fixture + +Specimen payloads for Visa Trusted Agent Protocol issue #16. The fixture keeps authentication and evidence separate: + +- TAP request authentication remains represented by the RFC 9421 `signature-input` in `tap-request.json`. +- Transaction evidence is represented by two detached JWS-style receipts over JCS-canonical payloads. +- The agent signs the pre-execution authorization receipt. +- The merchant or TAP-aware proxy signs the post-execution outcome receipt. + +Files: + +- `tap-request.json` - simulated TAP request context and RFC 9421 signature metadata. +- `authorization-receipt.json` - agent-signed pre-execution authorization receipt. +- `outcome-receipt.json` - merchant-signed post-execution outcome receipt. +- `keys.json` - deterministic Ed25519 public keys used by the fixture. +- `verify.py` - verifies both signatures and cross-links. + +Run: + +```bash +python3 verify.py +``` + +Expected result: + +```text +PASS authorization receipt signature +PASS outcome receipt signature +PASS request hash links both receipts to the TAP request +PASS outcome receipt chains to authorization receipt +``` + +The fixture is intentionally narrow: it does not propose TAP core changes. It demonstrates the evidence layer that can sit after TAP request authentication without overloading RFC 9421 with transaction-proof semantics. diff --git a/tap-bilateral-receipts/authorization-receipt.json b/tap-bilateral-receipts/authorization-receipt.json new file mode 100644 index 0000000..37e1906 --- /dev/null +++ b/tap-bilateral-receipts/authorization-receipt.json @@ -0,0 +1,28 @@ +{ + "payload": { + "type": "tap:pre_execution_authorization_receipt", + "spec": "draft-farley-acta-signed-receipts-01+tap-bilateral-authorization-v0", + "predicateType": "https://veritasacta.com/attestation/tap-bilateral-receipt/v0.1", + "receipt_id": "tap-authz-0001", + "issued_at": "2026-05-16T00:00:00Z", + "sequence": 1, + "previousReceiptHash": null, + "tap_request_hash": "sha256:00b175262d7773161a1bfbf168994104e5a816dc093ec76ae2afd823c51d8c39", + "transaction_payload_hash": "sha256:927ba277ff0c2f2a037af3600d53d16d9bbc6938059e7b025376a1e7b4bd22cc", + "agent_id": "did:key:z6MkTapAgentTest", + "merchant_id": "did:web:merchant.example", + "authorization_scope": { + "currency": "USD", + "max_amount": "50.00", + "merchant_id": "did:web:merchant.example", + "purpose_class": "office-supplies" + }, + "decision": "authorize", + "rfc9421_signature_input_hash": "sha256:c4fd1dbc3d79521cc23846a66343f070107330b403f5b8ebfa96b93f588f9c23" + }, + "signature": { + "format": "detached-jws-jcs-ed25519", + "protected": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0IiwiamNzIl0sImN0eSI6ImFwcGxpY2F0aW9uL2pjcytqc29uIiwia2lkIjoidGFwLWFnZW50LWVkMjU1MTktdGVzdCJ9", + "sig": "5NEiyNQa8MeGuiLdv9m7obnzKVuCJMjD8W1W8t3K7_MQif670paxkccr1E0WCIbsIT_Aaopdj6-cb-QB30dSCw" + } +} diff --git a/tap-bilateral-receipts/keys.json b/tap-bilateral-receipts/keys.json new file mode 100644 index 0000000..525f397 --- /dev/null +++ b/tap-bilateral-receipts/keys.json @@ -0,0 +1,18 @@ +{ + "keys": [ + { + "kid": "tap-agent-ed25519-test", + "role": "agent", + "kty": "OKP", + "crv": "Ed25519", + "public_key_hex": "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737" + }, + { + "kid": "tap-merchant-ed25519-test", + "role": "merchant", + "kty": "OKP", + "crv": "Ed25519", + "public_key_hex": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0" + } + ] +} diff --git a/tap-bilateral-receipts/outcome-receipt.json b/tap-bilateral-receipts/outcome-receipt.json new file mode 100644 index 0000000..df324c3 --- /dev/null +++ b/tap-bilateral-receipts/outcome-receipt.json @@ -0,0 +1,28 @@ +{ + "payload": { + "type": "tap:post_execution_outcome_receipt", + "spec": "draft-farley-acta-signed-receipts-01+tap-bilateral-outcome-v0", + "predicateType": "https://veritasacta.com/attestation/tap-bilateral-receipt/v0.1", + "receipt_id": "tap-outcome-0001", + "issued_at": "2026-05-16T00:00:02Z", + "sequence": 2, + "previousReceiptHash": "sha256:49897f56ca0de86991e9901a1f2403ed4080fe9f09775d89242a82fc37bdf163", + "tap_request_hash": "sha256:00b175262d7773161a1bfbf168994104e5a816dc093ec76ae2afd823c51d8c39", + "transaction_payload_hash": "sha256:927ba277ff0c2f2a037af3600d53d16d9bbc6938059e7b025376a1e7b4bd22cc", + "authorization_receipt_hash": "sha256:49897f56ca0de86991e9901a1f2403ed4080fe9f09775d89242a82fc37bdf163", + "agent_id": "did:key:z6MkTapAgentTest", + "merchant_id": "did:web:merchant.example", + "processor_id": "did:web:merchant.example#tap-proxy-1", + "outcome": "processed", + "merchant_reference": "merchant-settlement-98765", + "amount_processed": { + "currency": "USD", + "amount": "42.17" + } + }, + "signature": { + "format": "detached-jws-jcs-ed25519", + "protected": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0IiwiamNzIl0sImN0eSI6ImFwcGxpY2F0aW9uL2pjcytqc29uIiwia2lkIjoidGFwLW1lcmNoYW50LWVkMjU1MTktdGVzdCJ9", + "sig": "XUW8SA0NsOoR7UyrVvtgGc5ZAPZjvyDWMyQOofjRzkOwlOcKBesCAB_yhUFIFncKLNNsFeAPwdaori9Ek7sGAA" + } +} diff --git a/tap-bilateral-receipts/tap-request.json b/tap-bilateral-receipts/tap-request.json new file mode 100644 index 0000000..1035148 --- /dev/null +++ b/tap-bilateral-receipts/tap-request.json @@ -0,0 +1,25 @@ +{ + "tap_request_id": "tap-req-2026-05-16-0001", + "http_message_signature": { + "signature_input": "sig1=(\"@method\" \"@authority\" \"@path\" \"content-digest\" \"x-agent-id\");created=1778894400;keyid=\"tap-agent-ed25519-test\";alg=\"ed25519\"", + "signature": "sig1=:MEUCIQDf-test-vector-placeholder:", + "covered_components": [ + "@method", + "@authority", + "@path", + "content-digest", + "x-agent-id" + ] + }, + "method": "POST", + "authority": "merchant.example", + "path": "/tap/v1/transactions", + "agent_id": "did:key:z6MkTapAgentTest", + "merchant_id": "did:web:merchant.example", + "transaction": { + "currency": "USD", + "amount": "42.17", + "merchant_order_id": "order-12345", + "purpose": "agent-initiated checkout for approved office supplies" + } +} diff --git a/tap-bilateral-receipts/verify.py b/tap-bilateral-receipts/verify.py new file mode 100644 index 0000000..f3e3b63 --- /dev/null +++ b/tap-bilateral-receipts/verify.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import base64 +import hashlib +import json +from pathlib import Path + +from nacl.signing import VerifyKey + +ROOT = Path(__file__).resolve().parent + + +def b64url_decode(data: str) -> bytes: + return base64.urlsafe_b64decode(data + ('=' * (-len(data) % 4))) + + +def jcs(obj) -> bytes: + return json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8') + + +def sha256(data: bytes) -> str: + return 'sha256:' + hashlib.sha256(data).hexdigest() + + +def load_json(name: str): + return json.loads((ROOT / name).read_text()) + + +def verify_receipt(name: str, expected_kid: str, public_key_hex: str) -> dict: + receipt = load_json(name) + protected_b64 = receipt['signature']['protected'] + protected = json.loads(b64url_decode(protected_b64)) + assert protected['kid'] == expected_kid, (protected['kid'], expected_kid) + signing_input = protected_b64.encode('ascii') + b'.' + jcs(receipt['payload']) + VerifyKey(bytes.fromhex(public_key_hex)).verify(signing_input, b64url_decode(receipt['signature']['sig'])) + return receipt + + +def main() -> None: + request = load_json('tap-request.json') + keys = {item['kid']: item for item in load_json('keys.json')['keys']} + authz = verify_receipt('authorization-receipt.json', 'tap-agent-ed25519-test', keys['tap-agent-ed25519-test']['public_key_hex']) + outcome = verify_receipt('outcome-receipt.json', 'tap-merchant-ed25519-test', keys['tap-merchant-ed25519-test']['public_key_hex']) + + request_hash = sha256(jcs(request)) + transaction_hash = sha256(jcs(request['transaction'])) + authz_hash = sha256(jcs(authz['payload'])) + + assert authz['payload']['tap_request_hash'] == request_hash + assert outcome['payload']['tap_request_hash'] == request_hash + assert authz['payload']['transaction_payload_hash'] == transaction_hash + assert outcome['payload']['transaction_payload_hash'] == transaction_hash + assert outcome['payload']['previousReceiptHash'] == authz_hash + assert outcome['payload']['authorization_receipt_hash'] == authz_hash + + print('PASS authorization receipt signature') + print('PASS outcome receipt signature') + print('PASS request hash links both receipts to the TAP request') + print('PASS outcome receipt chains to authorization receipt') + + +if __name__ == '__main__': + main()