|
| 1 | +-- SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +-- ProvenanceChain.agda — Formal proof of provenance chain immutability (V7). |
| 5 | +-- |
| 6 | +-- REQUIREMENTS-MASTER.md: V7 | Provenance chain immutability (hash chain, monotonic timestamps) | SEC | Ag | P1 |
| 7 | +-- |
| 8 | +-- Models rust-core/verisim-provenance/src/lib.rs: |
| 9 | +-- - ProvenanceRecord has: content, timestamp, parentHash, actorId, contentHash |
| 10 | +-- - Hash chain: parentHash[i+1] = contentHash[i] (each record links to its predecessor) |
| 11 | +-- - First record has parentHash = genesisHash (the "0"*64 sentinel) |
| 12 | +-- |
| 13 | +-- List representation: PREPEND order — the HEAD of the list is the NEWEST record. |
| 14 | +-- This makes chain extension (hc-cons) structurally natural. |
| 15 | +-- |
| 16 | +-- Properties proved: |
| 17 | +-- 1. HashChain predicate — well-formedness of a provenance chain |
| 18 | +-- 2. chain-tail-valid — tail of valid chain is valid |
| 19 | +-- 3. chain-head-consistent — head is self-consistent |
| 20 | +-- 4. chain-link — parentHash of head links to contentHash of predecessor |
| 21 | +-- 5. tamper-detected — changing content changes contentHash (by hash injectivity) |
| 22 | +-- 6. chain-tamper-breaks-link — a tampered record breaks the link from its successor |
| 23 | +-- 7. timestamps-non-decreasing — timestamps grow along the chain (oldest last) |
| 24 | +-- 8. genesis-unique — only the tail (oldest) record has genesisHash as parent |
| 25 | +-- 9. chain-extend — prepending a new record with a valid link extends the chain |
| 26 | + |
| 27 | +module ProvenanceChain where |
| 28 | + |
| 29 | +open import Data.Nat using (ℕ; zero; suc; _≤_; _<_; z≤n; s≤s) |
| 30 | +open import Data.Nat.Properties using (≤-refl; ≤-trans; n<1+n; <⇒≤) |
| 31 | +open import Data.List using (List; []; _∷_; length) |
| 32 | +open import Data.Product using (_×_; _,_; proj₁; proj₂) |
| 33 | +open import Data.Empty using (⊥; ⊥-elim) |
| 34 | +open import Relation.Binary.PropositionalEquality |
| 35 | + using (_≡_; refl; sym; trans; cong; _≢_) |
| 36 | +open import Relation.Nullary using (¬_) |
| 37 | + |
| 38 | +------------------------------------------------------------------------ |
| 39 | +-- Section 1: Abstract hash model |
| 40 | +------------------------------------------------------------------------ |
| 41 | + |
| 42 | +-- Hashes are abstracted as ℕ. |
| 43 | +Hash : Set |
| 44 | +Hash = ℕ |
| 45 | + |
| 46 | +-- Abstract hash function for a provenance record's fields. |
| 47 | +-- content × timestamp × parentHash × actorId → contentHash |
| 48 | +postulate |
| 49 | + hashRecord : Hash → ℕ → Hash → Hash → Hash |
| 50 | + -- Collision resistance: equal outputs imply equal inputs. |
| 51 | + hash-injective : ∀ {c₁ t₁ p₁ a₁ c₂ t₂ p₂ a₂} |
| 52 | + → hashRecord c₁ t₁ p₁ a₁ ≡ hashRecord c₂ t₂ p₂ a₂ |
| 53 | + → c₁ ≡ c₂ × t₁ ≡ t₂ × p₁ ≡ p₂ × a₁ ≡ a₂ |
| 54 | + -- hashRecord never returns the genesis sentinel. |
| 55 | + -- Holds in Rust because SHA-256 of non-empty input ≠ "0"*64. |
| 56 | + hashRecord-not-genesis : ∀ (c t p a : Hash) → hashRecord c t p a ≢ 0 |
| 57 | + |
| 58 | +-- Genesis parent hash: sentinel for the oldest record's parentHash. |
| 59 | +genesisHash : Hash |
| 60 | +genesisHash = 0 |
| 61 | + |
| 62 | +------------------------------------------------------------------------ |
| 63 | +-- Section 2: Provenance record model |
| 64 | +------------------------------------------------------------------------ |
| 65 | + |
| 66 | +record ProvenanceRecord : Set where |
| 67 | + constructor mkRecord |
| 68 | + field |
| 69 | + content : Hash |
| 70 | + timestamp : ℕ |
| 71 | + parentHash : Hash |
| 72 | + actorId : Hash |
| 73 | + contentHash : Hash -- = hashRecord content timestamp parentHash actorId |
| 74 | + |
| 75 | +-- A record is self-consistent if its contentHash is correctly computed. |
| 76 | +SelfConsistent : ProvenanceRecord → Set |
| 77 | +SelfConsistent r = |
| 78 | + ProvenanceRecord.contentHash r ≡ |
| 79 | + hashRecord |
| 80 | + (ProvenanceRecord.content r) |
| 81 | + (ProvenanceRecord.timestamp r) |
| 82 | + (ProvenanceRecord.parentHash r) |
| 83 | + (ProvenanceRecord.actorId r) |
| 84 | + |
| 85 | +------------------------------------------------------------------------ |
| 86 | +-- Section 3: Hash chain predicate |
| 87 | +-- |
| 88 | +-- The list is stored NEWEST-FIRST (prepend order). |
| 89 | +-- HashChain (r_new ∷ r_old ∷ rs) means: |
| 90 | +-- - r_new is the most recently appended record |
| 91 | +-- - r_old is the record before r_new |
| 92 | +-- - r_new.parentHash = r_old.contentHash (link) |
| 93 | +-- - r_old.timestamp ≤ r_new.timestamp (monotone) |
| 94 | +------------------------------------------------------------------------ |
| 95 | + |
| 96 | +data HashChain : List ProvenanceRecord → Set where |
| 97 | + |
| 98 | + hc-nil : HashChain [] |
| 99 | + |
| 100 | + hc-first : ∀ {r} |
| 101 | + → SelfConsistent r |
| 102 | + → ProvenanceRecord.parentHash r ≡ genesisHash |
| 103 | + → HashChain (r ∷ []) |
| 104 | + |
| 105 | + -- Prepend r_new to an existing chain headed by r_old. |
| 106 | + hc-cons : ∀ {r_new r_old rs} |
| 107 | + → HashChain (r_old ∷ rs) -- existing chain |
| 108 | + → SelfConsistent r_new |
| 109 | + → ProvenanceRecord.parentHash r_new ≡ ProvenanceRecord.contentHash r_old |
| 110 | + → ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new |
| 111 | + → HashChain (r_new ∷ r_old ∷ rs) -- extended chain |
| 112 | + |
| 113 | +------------------------------------------------------------------------ |
| 114 | +-- Section 4: Structural lemmas |
| 115 | +------------------------------------------------------------------------ |
| 116 | + |
| 117 | +-- V7 — LEMMA 1: The tail of a valid chain is valid. |
| 118 | +-- (Easy: hc-cons's first argument is exactly the tail chain.) |
| 119 | +chain-tail-valid : ∀ {r rs} |
| 120 | + → HashChain (r ∷ rs) |
| 121 | + → HashChain rs |
| 122 | +chain-tail-valid (hc-first _ _) = hc-nil |
| 123 | +chain-tail-valid (hc-cons hc_tail _ _ _) = hc_tail |
| 124 | + |
| 125 | +-- V7 — LEMMA 2: The head of a valid chain is self-consistent. |
| 126 | +chain-head-consistent : ∀ {r rs} |
| 127 | + → HashChain (r ∷ rs) |
| 128 | + → SelfConsistent r |
| 129 | +chain-head-consistent (hc-first sc _) = sc |
| 130 | +chain-head-consistent (hc-cons _ sc _ _) = sc |
| 131 | + |
| 132 | +-- V7 — LEMMA 3: The head's parentHash equals its predecessor's contentHash. |
| 133 | +chain-link : ∀ {r_new r_old rs} |
| 134 | + → HashChain (r_new ∷ r_old ∷ rs) |
| 135 | + → ProvenanceRecord.parentHash r_new ≡ ProvenanceRecord.contentHash r_old |
| 136 | +chain-link (hc-cons _ _ link _) = link |
| 137 | + |
| 138 | +-- V7 — LEMMA 4: Timestamps are non-decreasing head-to-predecessor. |
| 139 | +-- Since the list is newest-first, the head timestamp is ≥ the second element's. |
| 140 | +chain-ts-head-ge : ∀ {r_new r_old rs} |
| 141 | + → HashChain (r_new ∷ r_old ∷ rs) |
| 142 | + → ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new |
| 143 | +chain-ts-head-ge (hc-cons _ _ _ ts) = ts |
| 144 | + |
| 145 | +------------------------------------------------------------------------ |
| 146 | +-- Section 5: Immutability theorems |
| 147 | +------------------------------------------------------------------------ |
| 148 | + |
| 149 | +-- V7 — THEOREM 1: Two self-consistent records with the same contentHash |
| 150 | +-- have identical field values (injectivity of hashRecord). |
| 151 | +same-hash-same-fields : |
| 152 | + ∀ {r₁ r₂ : ProvenanceRecord} |
| 153 | + → SelfConsistent r₁ |
| 154 | + → SelfConsistent r₂ |
| 155 | + → ProvenanceRecord.contentHash r₁ ≡ ProvenanceRecord.contentHash r₂ |
| 156 | + → ProvenanceRecord.content r₁ ≡ ProvenanceRecord.content r₂ |
| 157 | + × ProvenanceRecord.timestamp r₁ ≡ ProvenanceRecord.timestamp r₂ |
| 158 | + × ProvenanceRecord.parentHash r₁ ≡ ProvenanceRecord.parentHash r₂ |
| 159 | + × ProvenanceRecord.actorId r₁ ≡ ProvenanceRecord.actorId r₂ |
| 160 | +same-hash-same-fields sc₁ sc₂ heq = |
| 161 | + hash-injective (trans (sym sc₁) (trans heq sc₂)) |
| 162 | + |
| 163 | +-- V7 — THEOREM 2: Changing the content field changes the contentHash. |
| 164 | +tamper-detected : |
| 165 | + ∀ {r r' : ProvenanceRecord} |
| 166 | + → SelfConsistent r |
| 167 | + → SelfConsistent r' |
| 168 | + → ProvenanceRecord.content r ≢ ProvenanceRecord.content r' |
| 169 | + → ProvenanceRecord.contentHash r ≢ ProvenanceRecord.contentHash r' |
| 170 | +tamper-detected sc sc' hne heq = |
| 171 | + hne (proj₁ (same-hash-same-fields sc sc' heq)) |
| 172 | + |
| 173 | +-- V7 — THEOREM 3: Replacing the predecessor r_old with a tampered r_old' |
| 174 | +-- (different content) causes the head r_new's parentHash to no longer match. |
| 175 | +chain-tamper-breaks-link : |
| 176 | + ∀ {r_new r_old : ProvenanceRecord} {rs : List ProvenanceRecord} |
| 177 | + → HashChain (r_new ∷ r_old ∷ rs) |
| 178 | + → (r_old' : ProvenanceRecord) |
| 179 | + → SelfConsistent r_old' |
| 180 | + → ProvenanceRecord.content r_old ≢ ProvenanceRecord.content r_old' |
| 181 | + → ProvenanceRecord.parentHash r_new ≢ ProvenanceRecord.contentHash r_old' |
| 182 | +chain-tamper-breaks-link hc r_old' sc' hContentNe = |
| 183 | + let hc-old : SelfConsistent _ |
| 184 | + hc-old = chain-head-consistent (chain-tail-valid hc) |
| 185 | + hLink = chain-link hc |
| 186 | + hHashNe = tamper-detected hc-old sc' hContentNe |
| 187 | + in λ link' → hHashNe (trans (sym hLink) link') |
| 188 | + |
| 189 | +------------------------------------------------------------------------ |
| 190 | +-- Section 6: Timestamp monotonicity |
| 191 | +------------------------------------------------------------------------ |
| 192 | + |
| 193 | +-- Extract timestamps in newest-first order. |
| 194 | +chainTimestamps : List ProvenanceRecord → List ℕ |
| 195 | +chainTimestamps [] = [] |
| 196 | +chainTimestamps (r ∷ rs) = ProvenanceRecord.timestamp r ∷ chainTimestamps rs |
| 197 | + |
| 198 | +-- V7 — THEOREM 4: Timestamps in a valid chain are non-decreasing from |
| 199 | +-- oldest (tail) to newest (head). Equivalently, chainTimestamps is a |
| 200 | +-- non-increasing sequence (newest first → values are ≥ previous). |
| 201 | +-- |
| 202 | +-- We prove the pointwise version: for any adjacent pair in the chain, |
| 203 | +-- the older record's timestamp ≤ the newer record's timestamp. |
| 204 | +timestamps-non-decreasing : |
| 205 | + ∀ {r_new r_old rs} |
| 206 | + → HashChain (r_new ∷ r_old ∷ rs) |
| 207 | + → ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new |
| 208 | +timestamps-non-decreasing = chain-ts-head-ge |
| 209 | + |
| 210 | +------------------------------------------------------------------------ |
| 211 | +-- Section 7: Genesis uniqueness |
| 212 | +------------------------------------------------------------------------ |
| 213 | + |
| 214 | +-- V7 — THEOREM 5: Only the tail (oldest) record has genesisHash as parentHash. |
| 215 | +-- Any non-first record's parentHash equals the contentHash of its predecessor, |
| 216 | +-- and contentHash is produced by hashRecord, which never returns genesisHash. |
| 217 | +genesis-unique : |
| 218 | + ∀ {r_new r_old rs} |
| 219 | + → HashChain (r_new ∷ r_old ∷ rs) |
| 220 | + → ProvenanceRecord.parentHash r_new ≢ genesisHash |
| 221 | +genesis-unique {r_new} {r_old} hc hEq = |
| 222 | + let hLink = chain-link hc |
| 223 | + sc_old = chain-head-consistent (chain-tail-valid hc) |
| 224 | + in hashRecord-not-genesis |
| 225 | + (ProvenanceRecord.content r_old) |
| 226 | + (ProvenanceRecord.timestamp r_old) |
| 227 | + (ProvenanceRecord.parentHash r_old) |
| 228 | + (ProvenanceRecord.actorId r_old) |
| 229 | + (trans (sym sc_old) (trans (sym hLink) hEq)) |
| 230 | + |
| 231 | +------------------------------------------------------------------------ |
| 232 | +-- Section 8: Chain extension |
| 233 | +------------------------------------------------------------------------ |
| 234 | + |
| 235 | +-- V7 — THEOREM 6: Prepending a new well-formed record extends the chain. |
| 236 | +-- The caller supplies the link proof and the timestamp proof. |
| 237 | +chain-extend : |
| 238 | + ∀ {r_old rs} |
| 239 | + → HashChain (r_old ∷ rs) |
| 240 | + → (r_new : ProvenanceRecord) |
| 241 | + → SelfConsistent r_new |
| 242 | + → ProvenanceRecord.parentHash r_new ≡ ProvenanceRecord.contentHash r_old |
| 243 | + → ProvenanceRecord.timestamp r_old ≤ ProvenanceRecord.timestamp r_new |
| 244 | + → HashChain (r_new ∷ r_old ∷ rs) |
| 245 | +chain-extend hc r_new sc link ts = hc-cons hc sc link ts |
0 commit comments