betrusted-ed25519-verified/verification/Proofs/DsmNafSpec.lean
mrwulf 112036eb0d NAF encoder proven end-to-end + the phase-1 double-scalar-mul apex
The complete non_adjacent_form(5) verification (four stages):
- `Proofs/DsmNafLoadSpec.lean` (generated) — the LE byte-to-word load.
- `Proofs/DsmNafMath.lean` — the digit loop's arithmetic core: window-read
  lemmas (single/cross-word), the exact ZZ invariant steps (Nat.mod_mul
  telescope), the carry-kill argument from V < 2^253, and the exit theorem.
- `Proofs/DsmNafLoopSpec.lean` — the w=5 digit loop by induction on the
  remaining-bits measure: per-step 64-bit window read (4-way word split),
  digit write via hcast/wrapping_sub (exact value window - 32*carry',
  oddness, |d| < 16), invariant carried through even/odd steps.
- `Proofs/DsmNafSpec.lean` — the public spec: both entry masserts
  DISCHARGED; the digits satisfy the NAF conditions and
      sum naf[k]*2^k = V   EXACTLY (integers, no modular slack)
  for any scalar whose LE byte value V is below 2^253.

And the campaign's brick 4, `Proofs/DsmMulSpec.lean`:
- `run_basepoint` — the transpiled ED25519_BASEPOINT_POINT is the standard
  base point: valid extended coordinates (X*Y = Z*T) and the curve equation,
  kernel-checked via denominator-free 121666-scaled witnesses. Includes the
  generic witness lemmas fp_mul_eq_of_witness / onCurve_of_witness.
- `vartime_double_base_mul_spec` — THE PHASE-1 COMPUTATIONAL SPEC of
  vartime_double_base::mul: for canonical scalars and a valid on-curve A,
  the result is valid, on-curve, and denotes
      dsmFold (naf a) (naf b) (edPt A) edBasePt edId 256
  with both digit arrays proven exact NAF encodings. Phase 2 (group
  semantics [a]A + [b]B) requires Edwards associativity — deferred and
  documented; nothing assumes it.

Also: removed a vestigial pre-re-extraction axiom stub
(backend.serial.scalar_mul.vartime_double_base.mul) from FunsExternal —
a root-level leftover that shadowed the real namespaced definition during
name resolution in proof files. Never referenced by any certificate (the
#print-axioms audit guards against that); deleted for hygiene.

CERTS += naf_load_spec, naf_exit, naf_digit_loop_spec,
non_adjacent_form_spec, run_basepoint, vartime_double_base_mul_spec —
each audited to exactly [propext, Classical.choice, Quot.sound].
Full check.sh green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:52:10 +02:00

84 lines
4.6 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/- ──────────────────────────────────────────────────────────────────────────────
Proofs/DsmNafSpec.lean — NAF campaign, stage 4: the public spec of
`Scalar::non_adjacent_form(5)`.
Composes the proven stages: both entry masserts DISCHARGED (w = 5 is in
[2,8]), the LE byte→word load (DsmNafLoadSpec), width = 1<<<5 = 32 and
window_mask = 31 computed, and the digit loop (DsmNafLoopSpec) seeded
with the all-zeros state whose invariant is trivial.
POST: the 256 digits satisfy the NAF conditions (odd-or-zero, |d| < 16 —
exactly `NafDigits`, what dsm_loop_spec consumes) and their signed sum
reconstructs the scalar's little-endian byte value EXACTLY:
nafSum res 256 = V (as integers, no modular slack).
Requires V < 2^253 — canonical scalars, which the mul call sites provide.
────────────────────────────────────────────────────────────────────────────── -/
import Proofs.DsmNafLoadSpec
import Proofs.DsmNafLoopSpec
open Aeneas Aeneas.Std Result ControlFlow
open curve25519_dalek
set_option maxHeartbeats 8000000
set_option linter.unusedSimpArgs false
set_option maxRecDepth 8000
namespace CurveFieldProofs
open Aeneas.Std.WP
/-- **Scalar::non_adjacent_form(5)**: for a scalar whose 32-byte LE value V
is below 2^253, the result is a 256-entry NAF digit array — every digit
odd or zero with |d| < 16, and Σ naf[k]·2^k = V exactly. Both entry
masserts are discharged. -/
theorem non_adjacent_form_spec (self : scalar.Scalar)
(b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15 b16 b17 b18 b19 b20 b21 b22 b23 b24 b25 b26 b27 b28 b29 b30 b31 : Std.U8)
(hb : (↑self.bytes : List Std.U8) = [b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31])
(V : )
(hVbytes : V = b0.val + b1.val * 2^8 + b2.val * 2^16 + b3.val * 2^24 + b4.val * 2^32 + b5.val * 2^40 + b6.val * 2^48 + b7.val * 2^56 + b8.val * 2^64 + b9.val * 2^72 + b10.val * 2^80 + b11.val * 2^88 + b12.val * 2^96 + b13.val * 2^104 + b14.val * 2^112 + b15.val * 2^120 + b16.val * 2^128 + b17.val * 2^136 + b18.val * 2^144 + b19.val * 2^152 + b20.val * 2^160 + b21.val * 2^168 + b22.val * 2^176 + b23.val * 2^184 + b24.val * 2^192 + b25.val * 2^200 + b26.val * 2^208 + b27.val * 2^216 + b28.val * 2^224 + b29.val * 2^232 + b30.val * 2^240 + b31.val * 2^248)
(hV : V < 2^253) :
scalar.Scalar.non_adjacent_form self 5#usize ⦃ res =>
NafDigits res ∧ nafSum res 256 = (V : ) ⦄ := by
unfold scalar.Scalar.non_adjacent_form
step with (massert_spec (5#usize ≥ 2#usize) (by scalar_tac)) as ⟨h2⟩
step with (massert_spec (5#usize ≤ 8#usize) (by scalar_tac)) as ⟨h8⟩
-- the LE load fills x_u64[0..3]; word 4 stays 0
step with (naf_load_spec self (Array.repeat 5#usize 0#u64)
b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 b14 b15 b16 b17 b18 b19 b20 b21 b22 b23 b24 b25 b26 b27 b28 b29 b30 b31
hb (by simp [List.replicate])) as ⟨v0, v1, v2, v3, ws, hws, hv0, hv1, hv2, hv3⟩
-- width ← 1 <<< 5 (= 32), window_mask ← width 1 (= 31)
step as ⟨wd, hwd⟩
have hwdv : wd = 32#u64 := by clear * - hwd; scalar_tac
rw [hwdv]
step as ⟨mk, hmk⟩
have hmkv : mk = 31#u64 := by clear * - hmk; scalar_tac
rw [hmkv]
-- the initial all-zeros digit state
have hz : ∀ k, k < 256 → nafDigit (Array.repeat 256#usize 0#i8) k = 0 := by
intro k hk
unfold nafDigit
rw [getElem!_pos (↑(Array.repeat 256#usize 0#i8) : List Std.I8) k
(by simp; omega)]
simp only [Array.repeat_val, List.getElem_replicate]
simp
have hsum0 : nafSum (Array.repeat 256#usize 0#i8) 256 = 0 := by
unfold nafSum
apply Finset.sum_eq_zero
intro k hk
rw [hz k (Finset.mem_range.mp hk)]
ring
-- the word-form value
have hVw : V = v0.val + 2^64 * (v1.val + 2^64 * (v2.val + 2^64 * (v3.val + 2^64 * 0))) := by
rw [hVbytes, hv0, hv1, hv2, hv3]
ring
-- the digit loop from the trivial invariant
apply naf_digit_loop_spec ws v0 v1 v2 v3 V hws hVw hV 256
(Array.repeat 256#usize 0#i8) 0#usize 0#u64
(by scalar_tac)
(by scalar_tac)
(by intro h; simp at h)
(fun k _ hk => hz k hk)
(fun k hk => ⟨Or.inl (hz k hk), by rw [hz k hk]; norm_num,
by rw [hz k hk]; norm_num⟩)
(by simp [hsum0])
end CurveFieldProofs