|
| 1 | +-- SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +-- |
| 3 | +-- SPARK companion spec for src/rust/integrity/solver_integrity.rs |
| 4 | +-- |
| 5 | +-- Proved properties |
| 6 | +-- 1. Classify_Verification is total over (Binary_Found, Expected_Is_Placeholder, |
| 7 | +-- Compute_Succeeded, Hashes_Match) and never returns Unchecked. |
| 8 | +-- 2. Hash mismatch monotonicity: binary found AND expected is a real hash |
| 9 | +-- AND compute succeeded AND hashes don't match => status is Tampered. |
| 10 | +-- (This is the load-bearing "hash mismatch => Failed" obligation from |
| 11 | +-- hyperpolymath/echidna#40 roadmap.) |
| 12 | +-- 3. Is_Safe excludes Tampered and Missing exactly: |
| 13 | +-- Is_Safe(S) = (S in {Verified, Uninitialized, Unchecked}). |
| 14 | +-- 4. Quick_Reverify is the boolean-pure restatement of BLAKE3 cached==current. |
| 15 | +-- 5. Aggregate_Safe is cap-at-Tampered monotonic: any Tampered member |
| 16 | +-- forces the aggregate unsafe (Has_Tampered => not Aggregate_Safe). |
| 17 | +-- |
| 18 | +-- SPARK does not model file IO, async, tokio, or cryptographic primitives, |
| 19 | +-- so this companion is intentionally a pure-decision mirror. The Rust impl's |
| 20 | +-- find_solver_binary / compute_shake3_512 / compute_blake3 / RwLock layer is |
| 21 | +-- abstracted into the four boolean inputs of Classify_Verification, and the |
| 22 | +-- crypto guarantees come from the Keccak / BLAKE3 crates. The SPARK proof |
| 23 | +-- establishes that the *decision tree above the crypto layer* is correct. |
| 24 | + |
| 25 | +pragma Ada_2022; |
| 26 | + |
| 27 | +package Solver_Integrity |
| 28 | + with SPARK_Mode => On |
| 29 | +is |
| 30 | + -- ── IntegrityStatus ──────────────────────────────────────────────── |
| 31 | + -- Mirrors Rust enum IntegrityStatus (declared in the same positional |
| 32 | + -- order: Verified, Tampered, Missing, Uninitialized, Unchecked). |
| 33 | + type Integrity_Status is |
| 34 | + (Verified, Tampered, Missing, Uninitialized, Unchecked); |
| 35 | + |
| 36 | + -- ── Reverify_Outcome ─────────────────────────────────────────────── |
| 37 | + -- Quick_reverify returns a Result<bool, _> in Rust; the pure logical |
| 38 | + -- content is a two-valued outcome. |
| 39 | + type Reverify_Outcome is (Unchanged, Changed); |
| 40 | + |
| 41 | + -- ── Classify_Verification ────────────────────────────────────────── |
| 42 | + -- Mirrors verify_solver's decision tree from src/rust/integrity/ |
| 43 | + -- solver_integrity.rs:212-311. |
| 44 | + -- |
| 45 | + -- Inputs encode the verify_solver pipeline: |
| 46 | + -- Binary_Found -- find_solver_binary returned Some(_) |
| 47 | + -- Expected_Is_Placeholder -- entry.shake3_512.starts_with("PLACEHOLDER") |
| 48 | + -- Compute_Succeeded -- compute_shake3_512(actual_path).is_ok() |
| 49 | + -- Hashes_Match -- computed == entry.shake3_512 |
| 50 | + -- |
| 51 | + -- Decision tree (top-to-bottom precedence, mirroring the Rust source): |
| 52 | + -- 1. not Binary_Found => Missing (line 222) |
| 53 | + -- 2. Expected_Is_Placeholder => Uninitialized (line 257) |
| 54 | + -- 3. not Compute_Succeeded => Missing (line 302) |
| 55 | + -- 4. Hashes_Match => Verified (line 279) |
| 56 | + -- 5. otherwise (computed /= expected) => Tampered (line 285) |
| 57 | + function Classify_Verification |
| 58 | + (Binary_Found : Boolean; |
| 59 | + Expected_Is_Placeholder : Boolean; |
| 60 | + Compute_Succeeded : Boolean; |
| 61 | + Hashes_Match : Boolean) return Integrity_Status |
| 62 | + with Post => |
| 63 | + (case Classify_Verification'Result is |
| 64 | + when Verified => |
| 65 | + Binary_Found and not Expected_Is_Placeholder |
| 66 | + and Compute_Succeeded and Hashes_Match, |
| 67 | + when Tampered => |
| 68 | + Binary_Found and not Expected_Is_Placeholder |
| 69 | + and Compute_Succeeded and not Hashes_Match, |
| 70 | + when Uninitialized => |
| 71 | + Binary_Found and Expected_Is_Placeholder, |
| 72 | + when Missing => |
| 73 | + (not Binary_Found) |
| 74 | + or (Binary_Found and not Expected_Is_Placeholder |
| 75 | + and not Compute_Succeeded), |
| 76 | + when Unchecked => False); |
| 77 | + |
| 78 | + -- ── Is_Safe ──────────────────────────────────────────────────────── |
| 79 | + -- Mirrors IntegrityChecker::is_solver_safe (Rust line 355-361): |
| 80 | + -- matches!(status, Verified | Uninitialized | Unchecked) |
| 81 | + -- |
| 82 | + -- Expressed as an expression function so callers can use it freely |
| 83 | + -- in their own Pre/Post contracts. |
| 84 | + function Is_Safe (S : Integrity_Status) return Boolean is |
| 85 | + (S = Verified or S = Uninitialized or S = Unchecked); |
| 86 | + |
| 87 | + -- ── Quick_Reverify ───────────────────────────────────────────────── |
| 88 | + -- Mirrors IntegrityChecker::quick_reverify's decision (Rust line 334): |
| 89 | + -- if current_hash != cached_hash { Changed } else { Unchanged } |
| 90 | + -- |
| 91 | + -- SPARK abstracts the actual BLAKE3 comparison into Cached_Match. |
| 92 | + function Quick_Reverify (Cached_Match : Boolean) return Reverify_Outcome |
| 93 | + with Post => |
| 94 | + (Quick_Reverify'Result = Unchanged) = Cached_Match; |
| 95 | + |
| 96 | + -- ── Bounded status array ─────────────────────────────────────────── |
| 97 | + -- Mirrors verify_all's Vec<SolverIntegrityReport>, projected to just |
| 98 | + -- the status field (the rest is informational). 256 solvers covers |
| 99 | + -- any realistic deployment. |
| 100 | + Max_Solvers : constant := 256; |
| 101 | + subtype Solver_Index is Positive range 1 .. Max_Solvers; |
| 102 | + type Status_Array is array (Solver_Index range <>) of Integrity_Status; |
| 103 | + |
| 104 | + -- ── Aggregate_Safe ───────────────────────────────────────────────── |
| 105 | + -- A set of solvers is safe iff every member is safe. Expression |
| 106 | + -- function — the body IS the postcondition, no separate VC needed. |
| 107 | + function Aggregate_Safe (Statuses : Status_Array) return Boolean is |
| 108 | + (for all I in Statuses'Range => Is_Safe (Statuses (I))); |
| 109 | + |
| 110 | + -- ── Has_Tampered (ghost) ─────────────────────────────────────────── |
| 111 | + -- Predicate: does any solver in the set have status Tampered? |
| 112 | + -- Stated as ghost so callers can use it in their own contracts. |
| 113 | + function Has_Tampered (Statuses : Status_Array) return Boolean is |
| 114 | + (for some I in Statuses'Range => Statuses (I) = Tampered) |
| 115 | + with Ghost; |
| 116 | + |
| 117 | + -- ── Cap_At_Tampered (ghost lemma) ────────────────────────────────── |
| 118 | + -- Cap-at-Tampered monotonicity: if any solver is Tampered, the |
| 119 | + -- aggregate is unsafe. Mirrors axiom_tracker's cap-at-Reject |
| 120 | + -- monotonicity for the integrity domain. |
| 121 | + -- |
| 122 | + -- Proof: Has_Tampered (S) => (exists I, S(I) = Tampered) |
| 123 | + -- => (exists I, not Is_Safe (S(I))) |
| 124 | + -- => not (forall I, Is_Safe (S(I))) |
| 125 | + -- => not Aggregate_Safe (S). |
| 126 | + procedure Cap_At_Tampered (Statuses : Status_Array) |
| 127 | + with Ghost, |
| 128 | + Pre => Has_Tampered (Statuses), |
| 129 | + Post => not Aggregate_Safe (Statuses); |
| 130 | + |
| 131 | + -- ── Hash_Mismatch_Means_Tampered (ghost lemma) ───────────────────── |
| 132 | + -- Direct named restatement of issue #40's load-bearing obligation |
| 133 | + -- ("hash mismatch => Failed"). Proof: Classify_Verification's Post. |
| 134 | + procedure Hash_Mismatch_Means_Tampered |
| 135 | + (Binary_Found : Boolean; |
| 136 | + Expected_Is_Placeholder : Boolean; |
| 137 | + Compute_Succeeded : Boolean; |
| 138 | + Hashes_Match : Boolean) |
| 139 | + with Ghost, |
| 140 | + Pre => Binary_Found and not Expected_Is_Placeholder |
| 141 | + and Compute_Succeeded and not Hashes_Match, |
| 142 | + Post => Classify_Verification |
| 143 | + (Binary_Found, Expected_Is_Placeholder, |
| 144 | + Compute_Succeeded, Hashes_Match) = Tampered; |
| 145 | + |
| 146 | +end Solver_Integrity; |
0 commit comments