Skip to content

Commit 3dd586c

Browse files
committed
feat(spark): solver_integrity SPARK companion — run #2
SPARK 2014/2022 formal companion for src/rust/integrity/solver_integrity.rs, the SHAKE3-512 + BLAKE3 binary integrity layer. Advances the Rust→Rust/SPARK migration roadmap to run #2. Files - spark/solver_integrity/solver_integrity.ads — Ada spec, Pre/Post + ghost lemmas - spark/solver_integrity/solver_integrity.adb — body mirroring the 5-arm Rust decision tree - spark/solver_integrity/solver_integrity_proof.adb — 11 proof obligations catalogue - spark/solver_integrity/solver_integrity.gpr — GPRbuild project (gnat2022, --level=1) - spark/solver_integrity/alire.toml — Alire manifest pinning gnat_native ^14 / gprbuild ^22 - spark/solver_integrity/.gitignore — ignore .obj/ - spark/README.adoc — module index updated Proved - Total classification: Classify_Verification covers every input combination and never returns Unchecked (which is reserved for "no check done") - Hash-mismatch monotonicity: Binary_Found ∧ ¬Placeholder ∧ Compute_Succeeded ∧ ¬Hashes_Match ⇒ Tampered (issue #40 roadmap's load-bearing obligation) - Safety predicate exclusion: Is_Safe(S) = (S in {Verified, Uninitialized, Unchecked}) - Quick_Reverify: Unchanged ⇔ Cached_Match - Aggregate cap-at-Tampered: Has_Tampered(S) ⇒ ¬Aggregate_Safe(S) gnatprove summary: 11 checks, 0 unproved, 0 justified, max 53 steps. SPARK does not model file IO, async/tokio, or cryptographic primitives, so the companion is intentionally a pure-decision mirror. The Rust impl's find_solver_binary / compute_shake3_512 / compute_blake3 / RwLock layer is abstracted into the four boolean inputs of Classify_Verification, and the crypto guarantees come from the tiny_keccak (SHAKE3) and blake3 crates. SPARK's contribution is proving the decision tree *above* the crypto layer is correct and that hash mismatch unambiguously yields Tampered. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent e87cc7d commit 3dd586c

7 files changed

Lines changed: 443 additions & 1 deletion

File tree

spark/README.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ Summary of SPARK analysis
4545

4646
| link:axiom_tracker/[axiom_tracker]
4747
| `src/rust/verification/axiom_tracker.rs`
48-
| Run #1 — proved
48+
| Run #1 — proved (18/0)
49+
50+
| link:solver_integrity/[solver_integrity]
51+
| `src/rust/integrity/solver_integrity.rs`
52+
| Run #2 — proved (11/0)
4953

5054
|===
5155

spark/solver_integrity/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.obj/

spark/solver_integrity/alire.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name = "solver_integrity_spark"
2+
description = "SPARK companion for echidna solver_integrity module"
3+
version = "0.1.0"
4+
licenses = "PMPL-1.0-or-later"
5+
maintainers = ["hyperpolymath@users.noreply.github.com"]
6+
maintainers-logins = ["hyperpolymath"]
7+
8+
[build-switches]
9+
"*".Ada_Version = "Ada2022"
10+
11+
[[depends-on]]
12+
gnat_native = "^14"
13+
gprbuild = "^22"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
--
3+
-- SPARK companion body for solver_integrity.ads
4+
--
5+
-- Implementation strategy
6+
-- -----------------------
7+
-- Classify_Verification is a 5-arm if-chain mirroring the Rust decision tree
8+
-- top-to-bottom. Each return statement closes exactly one Post case;
9+
-- gnatprove substitutes the case-branch predicate at the return site and
10+
-- discharges at --level=0/1.
11+
--
12+
-- The ghost lemmas (Cap_At_Tampered, Hash_Mismatch_Means_Tampered) are
13+
-- named restatements with null bodies — gnatprove derives the conclusion
14+
-- from the spec's Post-condition + the standard quantifier dualities.
15+
16+
pragma Ada_2022;
17+
18+
package body Solver_Integrity
19+
with SPARK_Mode => On
20+
is
21+
22+
function Classify_Verification
23+
(Binary_Found : Boolean;
24+
Expected_Is_Placeholder : Boolean;
25+
Compute_Succeeded : Boolean;
26+
Hashes_Match : Boolean) return Integrity_Status is
27+
begin
28+
-- (1) Mirrors Rust line 222: find_solver_binary returned None.
29+
if not Binary_Found then
30+
return Missing;
31+
end if;
32+
33+
-- (2) Mirrors Rust line 240: entry.shake3_512.starts_with("PLACEHOLDER").
34+
if Expected_Is_Placeholder then
35+
return Uninitialized;
36+
end if;
37+
38+
-- (3) Mirrors Rust line 298-309: compute_shake3_512 returned Err.
39+
if not Compute_Succeeded then
40+
return Missing;
41+
end if;
42+
43+
-- (4) Mirrors Rust line 277-286: computed == expected.
44+
if Hashes_Match then
45+
return Verified;
46+
else
47+
return Tampered;
48+
end if;
49+
end Classify_Verification;
50+
51+
function Quick_Reverify (Cached_Match : Boolean) return Reverify_Outcome is
52+
begin
53+
if Cached_Match then
54+
return Unchanged;
55+
else
56+
return Changed;
57+
end if;
58+
end Quick_Reverify;
59+
60+
-- ── Cap_At_Tampered ────────────────────────────────────────────────
61+
-- Ghost lemma; gnatprove discharges from definitions:
62+
-- Has_Tampered (S) = (for some I, S(I) = Tampered)
63+
-- Is_Safe (Tampered) = False (by Is_Safe expression function)
64+
-- => (for some I, not Is_Safe (S(I)))
65+
-- => not (for all I, Is_Safe (S(I)))
66+
-- => not Aggregate_Safe (S).
67+
-- Null body is sufficient — Post follows from Pre by SMT.
68+
procedure Cap_At_Tampered (Statuses : Status_Array) is
69+
begin
70+
null;
71+
end Cap_At_Tampered;
72+
73+
-- ── Hash_Mismatch_Means_Tampered ───────────────────────────────────
74+
-- Ghost lemma; the four-fold conjunction in Pre is exactly the case
75+
-- arm of Classify_Verification's Post that maps to Tampered, so the
76+
-- conclusion follows by post-condition application + the disjointness
77+
-- of the case arms.
78+
procedure Hash_Mismatch_Means_Tampered
79+
(Binary_Found : Boolean;
80+
Expected_Is_Placeholder : Boolean;
81+
Compute_Succeeded : Boolean;
82+
Hashes_Match : Boolean) is
83+
begin
84+
null;
85+
end Hash_Mismatch_Means_Tampered;
86+
87+
end Solver_Integrity;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- GPRbuild project for solver_integrity SPARK companion
2+
project Solver_Integrity is
3+
4+
for Source_Dirs use (".");
5+
for Object_Dir use ".obj";
6+
for Main use ();
7+
8+
package Compiler is
9+
for Switches ("Ada") use
10+
("-gnat2022", -- Ada 2022 syntax
11+
"-gnatX", -- experimental Ada extensions (quantified exprs)
12+
"-O1",
13+
"-gnatn");
14+
end Compiler;
15+
16+
package Prove is
17+
-- gnatprove settings:
18+
-- --mode=prove run full proof (not just flow analysis)
19+
-- --level=1 CVC5 + Z3, medium timeout
20+
-- --counterexamples=on surface failing inputs when a VC fails
21+
-- -j0 use all available cores
22+
for Proof_Switches ("Ada") use
23+
("--mode=prove",
24+
"--level=1",
25+
"--counterexamples=on",
26+
"-j0",
27+
"--report=all");
28+
end Prove;
29+
30+
end Solver_Integrity;

0 commit comments

Comments
 (0)