|
| 1 | +-- SPDX-License-Identifier: MPL-2.0 |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | + |
| 4 | +||| Ochrance.A2ML.Roundtrip — a machine-checked manifest round-trip theorem. |
| 5 | +||| |
| 6 | +||| The production pipeline `parse . lex . serialize` cannot carry a compile-time |
| 7 | +||| `= Right m` theorem in Idris2: the `lex` stage rests on primitive String |
| 8 | +||| operations (`pack`/`unpack`/`++`/`parseInteger`) that have no provable |
| 9 | +||| equational theory, and the production parser is built from `if`-on-comparison |
| 10 | +||| (`expectToken`, `unless`) which the type-checker does not reduce. |
| 11 | +||| |
| 12 | +||| This module instead proves the grammar itself is *invertible*, with a clean |
| 13 | +||| positional token codec over the production `Manifest`/`Ref`/... types: |
| 14 | +||| |
| 15 | +||| decodeManifest (encodeManifest m) = Just m -- for every m |
| 16 | +||| |
| 17 | +||| The codec pattern-matches on token *constructors* only (never on String |
| 18 | +||| literals or `if`), and delegates enum tags to `parseHashAlgorithm`-style |
| 19 | +||| helpers that do reduce — so the inductive proof goes through with no |
| 20 | +||| `believe_me`, `assert_*`, or `postulate`. The production lexer/parser are |
| 21 | +||| then exercised against this reference at runtime (see `roundtripProperty`). |
| 22 | + |
| 23 | +module Ochrance.A2ML.Roundtrip |
| 24 | + |
| 25 | +import Ochrance.A2ML.Types |
| 26 | +import Ochrance.A2ML.Lexer |
| 27 | +import Ochrance.A2ML.Parser |
| 28 | +import Ochrance.A2ML.Serializer |
| 29 | +import Data.List |
| 30 | + |
| 31 | +%default total |
| 32 | + |
| 33 | +-------------------------------------------------------------------------------- |
| 34 | +-- Enum tag round-trips (each by case analysis; every case is Refl) |
| 35 | +-------------------------------------------------------------------------------- |
| 36 | + |
| 37 | +algoRT : (a : HashAlgorithm) -> parseHashAlgorithm (show a) = Just a |
| 38 | +algoRT SHA256 = Refl |
| 39 | +algoRT SHA3_256 = Refl |
| 40 | +algoRT BLAKE3 = Refl |
| 41 | + |
| 42 | +showMode : VerificationMode -> String |
| 43 | +showMode Lax = "lax" |
| 44 | +showMode Checked = "checked" |
| 45 | +showMode Attested = "attested" |
| 46 | + |
| 47 | +parseMode : String -> Maybe VerificationMode |
| 48 | +parseMode "lax" = Just Lax |
| 49 | +parseMode "checked" = Just Checked |
| 50 | +parseMode "attested" = Just Attested |
| 51 | +parseMode _ = Nothing |
| 52 | + |
| 53 | +modeRT : (m : VerificationMode) -> parseMode (showMode m) = Just m |
| 54 | +modeRT Lax = Refl |
| 55 | +modeRT Checked = Refl |
| 56 | +modeRT Attested = Refl |
| 57 | + |
| 58 | +-------------------------------------------------------------------------------- |
| 59 | +-- Primitive field codecs (markers are nullary token constructors) |
| 60 | +-------------------------------------------------------------------------------- |
| 61 | + |
| 62 | +||| Maybe String: present = LBRACE then the string; absent = RBRACE. |
| 63 | +encMStr : Maybe String -> List Token |
| 64 | +encMStr Nothing = [RBRACE] |
| 65 | +encMStr (Just s) = [LBRACE, STRING s] |
| 66 | + |
| 67 | +decMStr : List Token -> Maybe (Maybe String, List Token) |
| 68 | +decMStr (RBRACE :: rest) = Just (Nothing, rest) |
| 69 | +decMStr (LBRACE :: STRING s :: rest) = Just (Just s, rest) |
| 70 | +decMStr _ = Nothing |
| 71 | + |
| 72 | +mstrRT : (x : Maybe String) -> (rest : List Token) -> decMStr (encMStr x ++ rest) = Just (x, rest) |
| 73 | +mstrRT Nothing rest = Refl |
| 74 | +mstrRT (Just s) rest = Refl |
| 75 | + |
| 76 | +||| Bool: True = LBRACE, False = RBRACE. |
| 77 | +encBool : Bool -> List Token |
| 78 | +encBool True = [LBRACE] |
| 79 | +encBool False = [RBRACE] |
| 80 | + |
| 81 | +decBool : List Token -> Maybe (Bool, List Token) |
| 82 | +decBool (LBRACE :: rest) = Just (True, rest) |
| 83 | +decBool (RBRACE :: rest) = Just (False, rest) |
| 84 | +decBool _ = Nothing |
| 85 | + |
| 86 | +boolRT : (b : Bool) -> (rest : List Token) -> decBool (encBool b ++ rest) = Just (b, rest) |
| 87 | +boolRT True rest = Refl |
| 88 | +boolRT False rest = Refl |
| 89 | + |
| 90 | +||| Nat in unary (COLON per successor, EQUALS terminator) — proof-friendly, |
| 91 | +||| avoiding the primitive Integer<->Nat cast. |
| 92 | +encNat : Nat -> List Token |
| 93 | +encNat Z = [EQUALS] |
| 94 | +encNat (S k) = COLON :: encNat k |
| 95 | + |
| 96 | +decNat : List Token -> Maybe (Nat, List Token) |
| 97 | +decNat (EQUALS :: rest) = Just (Z, rest) |
| 98 | +decNat (COLON :: more) = case decNat more of |
| 99 | + Just (k, rest) => Just (S k, rest) |
| 100 | + Nothing => Nothing |
| 101 | +decNat _ = Nothing |
| 102 | + |
| 103 | +natRT : (n : Nat) -> (rest : List Token) -> decNat (encNat n ++ rest) = Just (n, rest) |
| 104 | +natRT Z rest = Refl |
| 105 | +natRT (S k) rest = rewrite natRT k rest in Refl |
| 106 | + |
| 107 | +||| Maybe Nat. |
| 108 | +encMNat : Maybe Nat -> List Token |
| 109 | +encMNat Nothing = [RBRACE] |
| 110 | +encMNat (Just n) = LBRACE :: encNat n |
| 111 | + |
| 112 | +decMNat : List Token -> Maybe (Maybe Nat, List Token) |
| 113 | +decMNat (RBRACE :: rest) = Just (Nothing, rest) |
| 114 | +decMNat (LBRACE :: more) = case decNat more of |
| 115 | + Just (n, rest) => Just (Just n, rest) |
| 116 | + Nothing => Nothing |
| 117 | +decMNat _ = Nothing |
| 118 | + |
| 119 | +mnatRT : (x : Maybe Nat) -> (rest : List Token) -> decMNat (encMNat x ++ rest) = Just (x, rest) |
| 120 | +mnatRT Nothing rest = Refl |
| 121 | +mnatRT (Just n) rest = rewrite natRT n rest in Refl |
| 122 | + |
| 123 | +-------------------------------------------------------------------------------- |
| 124 | +-- References (the recursive heart of the grammar) |
| 125 | +-------------------------------------------------------------------------------- |
| 126 | + |
| 127 | +encRef : Ref -> List Token |
| 128 | +encRef r = [IDENT r.name, COLON, HASH (show r.hash.algorithm) r.hash.value] |
| 129 | + |
| 130 | +||| Reference list, terminated by RBRACE. |
| 131 | +encRefs : List Ref -> List Token |
| 132 | +encRefs [] = [RBRACE] |
| 133 | +encRefs (r :: rs) = encRef r ++ encRefs rs |
| 134 | + |
| 135 | +decRefs : List Token -> Maybe (List Ref, List Token) |
| 136 | +decRefs (RBRACE :: rest) = Just ([], rest) |
| 137 | +decRefs (IDENT n :: COLON :: HASH a v :: more) = |
| 138 | + case parseHashAlgorithm a of |
| 139 | + Just algo => case decRefs more of |
| 140 | + Just (rs, rest) => Just (MkRef n (MkHash algo v) :: rs, rest) |
| 141 | + Nothing => Nothing |
| 142 | + Nothing => Nothing |
| 143 | +decRefs _ = Nothing |
| 144 | + |
| 145 | +refsRT : (rs : List Ref) -> (rest : List Token) -> decRefs (encRefs rs ++ rest) = Just (rs, rest) |
| 146 | +refsRT [] rest = Refl |
| 147 | +refsRT (MkRef n (MkHash a v) :: rs) rest = |
| 148 | + rewrite algoRT a in |
| 149 | + rewrite refsRT rs rest in Refl |
| 150 | + |
| 151 | +-------------------------------------------------------------------------------- |
| 152 | +-- Attestation (optional) |
| 153 | +-------------------------------------------------------------------------------- |
| 154 | + |
| 155 | +encMAtt : Maybe Attestation -> List Token |
| 156 | +encMAtt Nothing = [RBRACE] |
| 157 | +encMAtt (Just a) = [LBRACE, STRING a.witness, STRING a.signature, STRING a.pubkey] |
| 158 | + |
| 159 | +decMAtt : List Token -> Maybe (Maybe Attestation, List Token) |
| 160 | +decMAtt (RBRACE :: rest) = Just (Nothing, rest) |
| 161 | +decMAtt (LBRACE :: STRING w :: STRING s :: STRING p :: rest) = |
| 162 | + Just (Just (MkAttestation w s p), rest) |
| 163 | +decMAtt _ = Nothing |
| 164 | + |
| 165 | +mattRT : (x : Maybe Attestation) -> (rest : List Token) -> decMAtt (encMAtt x ++ rest) = Just (x, rest) |
| 166 | +mattRT Nothing rest = Refl |
| 167 | +mattRT (Just (MkAttestation w s p)) rest = Refl |
| 168 | + |
| 169 | +-------------------------------------------------------------------------------- |
| 170 | +-- Policy (optional) |
| 171 | +-------------------------------------------------------------------------------- |
| 172 | + |
| 173 | +encMPol : Maybe Policy -> List Token |
| 174 | +encMPol Nothing = [RBRACE] |
| 175 | +encMPol (Just p) = LBRACE :: STRING (showMode p.mode) :: (encMNat p.maxAge ++ encBool p.requireSig) |
| 176 | + |
| 177 | +decMPol : List Token -> Maybe (Maybe Policy, List Token) |
| 178 | +decMPol (RBRACE :: rest) = Just (Nothing, rest) |
| 179 | +decMPol (LBRACE :: STRING modeStr :: more) = |
| 180 | + case parseMode modeStr of |
| 181 | + Just mode => case decMNat more of |
| 182 | + Just (maxAge, more2) => case decBool more2 of |
| 183 | + Just (requireSig, rest) => Just (Just (MkPolicy mode maxAge requireSig), rest) |
| 184 | + Nothing => Nothing |
| 185 | + Nothing => Nothing |
| 186 | + Nothing => Nothing |
| 187 | +decMPol _ = Nothing |
| 188 | + |
| 189 | +mpolRT : (x : Maybe Policy) -> (rest : List Token) -> decMPol (encMPol x ++ rest) = Just (x, rest) |
| 190 | +mpolRT Nothing rest = Refl |
| 191 | +mpolRT (Just (MkPolicy mode ma rs)) rest = |
| 192 | + rewrite modeRT mode in |
| 193 | + rewrite sym (appendAssociative (encMNat ma) (encBool rs) rest) in |
| 194 | + rewrite mnatRT ma (encBool rs ++ rest) in |
| 195 | + rewrite boolRT rs rest in Refl |
| 196 | + |
| 197 | +-------------------------------------------------------------------------------- |
| 198 | +-- Whole manifest |
| 199 | +-------------------------------------------------------------------------------- |
| 200 | + |
| 201 | +encodeManifest : Manifest -> List Token |
| 202 | +encodeManifest m = |
| 203 | + MANIFEST :: STRING m.manifestData.version :: STRING m.manifestData.subsystem |
| 204 | + :: (encMStr m.manifestData.timestamp |
| 205 | + ++ REFS :: encRefs m.refs |
| 206 | + ++ encMAtt m.attestation |
| 207 | + ++ encMPol m.policy |
| 208 | + ++ [EOF]) |
| 209 | + |
| 210 | +decodeManifest : List Token -> Maybe Manifest |
| 211 | +decodeManifest (MANIFEST :: STRING ver :: STRING sub :: r0) = |
| 212 | + case decMStr r0 of |
| 213 | + Just (ts, REFS :: r2) => case decRefs r2 of |
| 214 | + Just (rs, r3) => case decMAtt r3 of |
| 215 | + Just (att, r4) => case decMPol r4 of |
| 216 | + Just (pol, EOF :: []) => |
| 217 | + Just (MkManifest (MkManifestData ver sub ts) rs att pol) |
| 218 | + _ => Nothing |
| 219 | + Nothing => Nothing |
| 220 | + Nothing => Nothing |
| 221 | + _ => Nothing |
| 222 | +decodeManifest _ = Nothing |
| 223 | + |
| 224 | +||| THEOREM: the reference codec is invertible for every manifest. |
| 225 | +export |
| 226 | +roundtripManifest : (m : Manifest) -> decodeManifest (encodeManifest m) = Just m |
| 227 | +roundtripManifest (MkManifest (MkManifestData ver sub ts) rs att pol) = |
| 228 | + rewrite mstrRT ts (REFS :: (encRefs rs ++ encMAtt att ++ encMPol pol ++ [EOF])) in |
| 229 | + rewrite refsRT rs (encMAtt att ++ encMPol pol ++ [EOF]) in |
| 230 | + rewrite mattRT att (encMPol pol ++ [EOF]) in |
| 231 | + rewrite mpolRT pol [EOF] in |
| 232 | + Refl |
| 233 | + |
| 234 | +-------------------------------------------------------------------------------- |
| 235 | +-- Production-pipeline check (runtime) |
| 236 | +-------------------------------------------------------------------------------- |
| 237 | + |
| 238 | +||| Round-trip check against the *production* pipeline: serialize, then lex and |
| 239 | +||| parse, then confirm the result re-serializes identically. |
| 240 | +||| |
| 241 | +||| `roundtripManifest` above is the formal guarantee (over the reference token |
| 242 | +||| codec). This complements it by exercising the real lexer + parser, whose |
| 243 | +||| String stage lies outside the proof's reach (primitive `pack`/`unpack` etc. |
| 244 | +||| have no equational theory). It holds for well-formed manifests (hex digests, |
| 245 | +||| no in-string delimiters) and is exercised by the test suite. |
| 246 | +export |
| 247 | +roundtripProperty : Manifest -> Bool |
| 248 | +roundtripProperty m = |
| 249 | + case lex (serialize m) of |
| 250 | + Left _ => False |
| 251 | + Right toks => case parse toks of |
| 252 | + Left _ => False |
| 253 | + Right m2 => serialize m2 == serialize m |
0 commit comments