/ tools / ats-shadow / argon2-checks.dats
argon2-checks.dats
  1  (*
  2    argon2-checks.dats: ATS2 shadow model for Argon2id parameter validation
  3  
  4    Non-authoritative: intended for CI shadow verification only.
  5    This checks that keystore policy parameters satisfy constraints
  6    without implementing actual Argon2id (that's the SPARK core's job).
  7  
  8    Checks:
  9      - salt_bytes in [16, 32]
 10      - hash_bytes in [32, 64]
 11      - parallelism in [1, 8]
 12      - memory_kib in [65536, 1048576]
 13      - time_cost in [2, 10]
 14      - profile name is valid
 15      - upgrade only increases parameters
 16  *)
 17  
 18  #include "share/atspre_define.hats"
 19  #include "share/atspre_staload.hats"
 20  
 21  staload UN = "prelude/SATS/unsafe.sats"
 22  
 23  (* --- Constraint bounds from spec/keystore-policy.json --- *)
 24  
 25  val SALT_BYTES_MIN: int = 16
 26  val SALT_BYTES_MAX: int = 32
 27  val HASH_BYTES_MIN: int = 32
 28  val HASH_BYTES_MAX: int = 64
 29  val PARALLELISM_MIN: int = 1
 30  val PARALLELISM_MAX: int = 8
 31  val MEMORY_KIB_MIN: int = 65536
 32  val MEMORY_KIB_MAX: int = 1048576
 33  val TIME_COST_MIN: int = 2
 34  val TIME_COST_MAX: int = 10
 35  
 36  (* --- Types --- *)
 37  
 38  typedef argon2_params = @{
 39    salt_bytes = int,
 40    hash_bytes = int,
 41    parallelism = int,
 42    memory_kib = int,
 43    time_cost = int
 44  }
 45  
 46  typedef validation_result = @{
 47    valid = bool,
 48    error_msg = string
 49  }
 50  
 51  (* --- Validation functions --- *)
 52  
 53  fun
 54  validate_salt_bytes(p: int): validation_result =
 55    if p < SALT_BYTES_MIN then
 56      @{ valid = false, error_msg = "salt_bytes below minimum (16)" }
 57    else if p > SALT_BYTES_MAX then
 58      @{ valid = false, error_msg = "salt_bytes above maximum (32)" }
 59    else
 60      @{ valid = true, error_msg = "" }
 61  
 62  fun
 63  validate_hash_bytes(p: int): validation_result =
 64    if p < HASH_BYTES_MIN then
 65      @{ valid = false, error_msg = "hash_bytes below minimum (32)" }
 66    else if p > HASH_BYTES_MAX then
 67      @{ valid = false, error_msg = "hash_bytes above maximum (64)" }
 68    else
 69      @{ valid = true, error_msg = "" }
 70  
 71  fun
 72  validate_parallelism(p: int): validation_result =
 73    if p < PARALLELISM_MIN then
 74      @{ valid = false, error_msg = "parallelism below minimum (1)" }
 75    else if p > PARALLELISM_MAX then
 76      @{ valid = false, error_msg = "parallelism above maximum (8)" }
 77    else
 78      @{ valid = true, error_msg = "" }
 79  
 80  fun
 81  validate_memory_kib(m: int): validation_result =
 82    if m < MEMORY_KIB_MIN then
 83      @{ valid = false, error_msg = "memory_kib below minimum (65536)" }
 84    else if m > MEMORY_KIB_MAX then
 85      @{ valid = false, error_msg = "memory_kib above maximum (1048576)" }
 86    else
 87      @{ valid = true, error_msg = "" }
 88  
 89  fun
 90  validate_time_cost(t: int): validation_result =
 91    if t < TIME_COST_MIN then
 92      @{ valid = false, error_msg = "time_cost below minimum (2)" }
 93    else if t > TIME_COST_MAX then
 94      @{ valid = false, error_msg = "time_cost above maximum (10)" }
 95    else
 96      @{ valid = true, error_msg = "" }
 97  
 98  (* --- Full parameter validation --- *)
 99  
100  fun
101  validate_params(params: argon2_params): validation_result = let
102    val r1 = validate_salt_bytes(params.salt_bytes)
103  in
104    if ~r1.valid then r1
105    else let
106      val r2 = validate_hash_bytes(params.hash_bytes)
107    in
108      if ~r2.valid then r2
109      else let
110        val r3 = validate_parallelism(params.parallelism)
111      in
112        if ~r3.valid then r3
113        else let
114          val r4 = validate_memory_kib(params.memory_kib)
115        in
116          if ~r4.valid then r4
117          else let
118            val r5 = validate_time_cost(params.time_cost)
119          in
120            if ~r5.valid then r5
121            else @{ valid = true, error_msg = "" }
122          end
123        end
124      end
125    end
126  end
127  
128  (* --- Upgrade validation (allow_increase_only) --- *)
129  
130  fun
131  validate_upgrade(old_p: argon2_params, new_p: argon2_params): validation_result =
132    if new_p.salt_bytes < old_p.salt_bytes then
133      @{ valid = false, error_msg = "upgrade cannot decrease salt_bytes" }
134    else if new_p.hash_bytes < old_p.hash_bytes then
135      @{ valid = false, error_msg = "upgrade cannot decrease hash_bytes" }
136    else if new_p.parallelism < old_p.parallelism then
137      @{ valid = false, error_msg = "upgrade cannot decrease parallelism" }
138    else if new_p.memory_kib < old_p.memory_kib then
139      @{ valid = false, error_msg = "upgrade cannot decrease memory_kib" }
140    else if new_p.time_cost < old_p.time_cost then
141      @{ valid = false, error_msg = "upgrade cannot decrease time_cost" }
142    else
143      @{ valid = true, error_msg = "" }
144  
145  (* --- Profile validation --- *)
146  
147  datatype profile_name =
148    | Interactive
149    | Moderate
150    | High
151    | Unknown of string
152  
153  fun
154  parse_profile(s: string): profile_name =
155    if s = "interactive" then Interactive()
156    else if s = "moderate" then Moderate()
157    else if s = "high" then High()
158    else Unknown(s)
159  
160  fun
161  validate_profile_name(s: string): validation_result = let
162    val p = parse_profile(s)
163  in
164    case+ p of
165    | Unknown(name) => @{ valid = false, error_msg = "unknown profile: " + name }
166    | _ => @{ valid = true, error_msg = "" }
167  end
168  
169  (* --- Profile defaults (for shadow checking) --- *)
170  
171  fun
172  get_profile_defaults(p: profile_name): argon2_params =
173    case+ p of
174    | Interactive() => @{
175        salt_bytes = 16,
176        hash_bytes = 32,
177        parallelism = 1,
178        memory_kib = 131072,
179        time_cost = 3
180      }
181    | Moderate() => @{
182        salt_bytes = 16,
183        hash_bytes = 32,
184        parallelism = 2,
185        memory_kib = 262144,
186        time_cost = 4
187      }
188    | High() => @{
189        salt_bytes = 16,
190        hash_bytes = 32,
191        parallelism = 4,
192        memory_kib = 524288,
193        time_cost = 5
194      }
195    | Unknown(_) => @{
196        salt_bytes = 0,
197        hash_bytes = 0,
198        parallelism = 0,
199        memory_kib = 0,
200        time_cost = 0
201      }
202  
203  (* --- Invariant: argon2id MUST NOT be used for artifact hashing --- *)
204  
205  (* This is a compile-time marker. The actual enforcement is in code review
206     and CI grep checks. This type exists to document the invariant. *)
207  
208  abstype artifact_digest  (* opaque: cannot be constructed from argon2 output *)
209  abstype password_verifier  (* opaque: argon2id output, cannot be used as artifact_digest *)
210  
211  (* The type system prevents confusion at compile time if used properly.
212     This is the ATS-style way of encoding the non-goal invariants. *)
213  
214  (*
215    Usage in CI:
216  
217    1. Build this file to verify it compiles (syntax/type check)
218    2. Extract the constraint constants and compare to spec/keystore-policy.json
219    3. Run grep checks to ensure argon2 is not imported in artifact modules
220  
221    patscc -tcats argon2-checks.dats  # Type-check only
222  *)