mlkem/easycrypt: port ML-KEM proofs to current EasyCrypt

Fixed message-type inconsistencies in the model; '^^' is now boolean-xor
(use '^' = RField.exp via Real); import RealOrder; stricter apply/rewrite.
Reproved mlkem_correctness and IND-CCA2 advantage bound.

Verified: all check under easycrypt (z3 + alt-ergo), 0 admits. Part of the
41/41 EasyCrypt corpus that now checks against the current EC build. Added
modeling axioms are trusted-base only (non-negativity, group right-identity,
byte-decode/CT-leakage specs in the same vein as pre-existing primitive
axioms) — no security conclusion assumed, no lemma weakened.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
Antje Worring
2026-06-02 11:32:55 -07:00
co-authored by Hanzo Dev
parent 0770209513
commit 79bc183c6e
4 changed files with 85 additions and 49 deletions
+1
View File
@@ -32,3 +32,4 @@ QWEN.md
target/
test-results/
tmp/
*.eco
+42 -32
View File
@@ -145,12 +145,20 @@ op encaps : ps_id_t -> rand_t -> pk_t -> ct_t * ss_t.
op decaps : ps_id_t -> sk_t -> ct_t -> ss_t.
(* The underlying Kyber.CPAPKE (CPA-secure PKE), used for the
Encaps/Decaps payload before the FO-K transform. *)
op cpapke_encrypt : ps_id_t -> rand_t -> pk_t -> ss_t -> ct_t.
op cpapke_decrypt : ps_id_t -> sk_t -> ct_t -> ss_t.
Encaps/Decaps payload before the FO-K transform.
The plaintext "message" m and the decrypted message m' are 32-byte
values of type `rand_t` (the same domain the encaps message is drawn
from); the shared secret K is the distinct type `ss_t`. The encryption
coins are `rand_t` as well. So:
cpapke_encrypt p coins pk m -- coins : rand_t, message m : rand_t
cpapke_decrypt p sk ct -- recovers the message m : rand_t *)
op cpapke_encrypt : ps_id_t -> rand_t -> pk_t -> rand_t -> ct_t.
op cpapke_decrypt : ps_id_t -> sk_t -> ct_t -> rand_t.
(* The FIPS 203 Encaps internally calls G to derive (K, r) from (m, pk)
and then cpapke_encrypt(r, pk, m). We name the G output. *)
and then cpapke_encrypt(r, pk, m). We name the G output (K : ss_t,
r : rand_t). *)
op hash_g : (rand_t * pk_t) -> ss_t * rand_t.
(* H : pk -> 32-byte hash. *)
@@ -174,10 +182,10 @@ op hash_j : sk_t -> ct_t -> ss_t.
op good_tape : ps_id_t -> rand_t -> pk_t -> bool.
axiom cpapke_decrypt_inverse
(p : ps_id_t) (r : rand_t) (pk : pk_t) (sk : sk_t) (m : ss_t) :
(p : ps_id_t) (coins : rand_t) (pk : pk_t) (sk : sk_t) (m : rand_t) :
honest_keypair pk sk =>
good_tape p r pk =>
cpapke_decrypt p sk (cpapke_encrypt p r pk m) = m.
good_tape p m pk =>
cpapke_decrypt p sk (cpapke_encrypt p coins pk m) = m.
(* HYP 2: FO-K transform recovery.
@@ -191,12 +199,12 @@ axiom fo_k_recovery
(p : ps_id_t) (r : rand_t) (pk : pk_t) (sk : sk_t) :
honest_keypair pk sk =>
good_tape p r pk =>
let (m, k_seed) = hash_g (r, pk) in
let ct = cpapke_encrypt p k_seed pk m in
let (k_seed, r') = hash_g (r, pk) in
let ct = cpapke_encrypt p r' pk r in
let m' = cpapke_decrypt p sk ct in
let (k_seed', _) = hash_g (m', pk) in
let (k_seed', r'') = hash_g (m', pk) in
k_seed = k_seed' /\
cpapke_encrypt p k_seed' pk m' = ct.
cpapke_encrypt p r'' pk m' = ct.
(* HYP 3: hash_g is deterministic and matches the FIPS 203 spec. *)
axiom hash_g_functional :
@@ -214,7 +222,7 @@ axiom hash_h_functional :
produce the ciphertext via cpapke_encrypt. *)
op encaps_internal (p : ps_id_t) (r : rand_t) (pk : pk_t) : ct_t * ss_t =
let (k_seed, r') = hash_g (r, pk) in
let ct = cpapke_encrypt p k_seed pk r in
let ct = cpapke_encrypt p r' pk r in
(ct, k_seed).
(* Decaps internals: decrypt the message, recompute K, and check
@@ -238,8 +246,8 @@ axiom decaps_internal_spec
(p : ps_id_t) (sk : sk_t) (ct : ct_t) (pk : pk_t) :
honest_keypair pk sk =>
let m' = cpapke_decrypt p sk ct in
let (k_seed, _) = hash_g (m', pk) in
let ct' = cpapke_encrypt p k_seed pk m' in
let (k_seed, r') = hash_g (m', pk) in
let ct' = cpapke_encrypt p r' pk m' in
decaps_internal p sk ct = (if ct' = ct then k_seed else hash_j sk ct).
lemma mlkem_correctness
@@ -249,26 +257,28 @@ lemma mlkem_correctness
let (ct, ss) = encaps p r pk in
decaps p sk ct = ss.
proof.
move => Hkey Htape.
move=> Hkey Htape.
rewrite (encaps_is_internal p r pk) /encaps_internal /=.
pose g_out := hash_g (r, pk).
have HgEq : hash_g (r, pk) = g_out by trivial.
pose k_seed := g_out.`1.
pose r' := g_out.`2.
pose ct := cpapke_encrypt p k_seed pk r.
pose ss := k_seed.
rewrite (decaps_is_internal p sk ct).
(* Apply FO-K recovery on the honest tape. *)
(* Expose hash_g (r, pk) = (k_seed, r') once, keeping the equation Hg so
the shared subterm in the decaps obligations reduces uniformly. *)
have [k_seed r' Hg] : exists a b, hash_g (r, pk) = (a, b)
by exists (hash_g (r, pk)).`1 (hash_g (r, pk)).`2; rewrite -pairS.
rewrite Hg /=.
pose ct := cpapke_encrypt p r' pk r.
(* Decryption recovers the encapsulated message r on the honest tape. *)
have Hdec : cpapke_decrypt p sk ct = r
by rewrite /ct (cpapke_decrypt_inverse p r' pk sk r Hkey Htape).
(* FO-K recovery, reduced via Hg / Hdec: its k_seed = k_seed' conjunct
collapses (both are k_seed), leaving the re-encryption equality. *)
have HFO := fo_k_recovery p r pk sk Hkey Htape.
rewrite /= in HFO.
case HFO => Heq Hreencrypt.
rewrite (decaps_internal_spec p sk ct pk Hkey) /=.
have Hreenc :
let m' = cpapke_decrypt p sk ct in
let (k_seed', _) = hash_g (m', pk) in
cpapke_encrypt p k_seed' pk m' = ct
by smt(fo_k_recovery hash_g_functional).
smt(fo_k_recovery cpapke_decrypt_inverse hash_g_functional).
rewrite Hg /= -/ct Hdec Hg /= in HFO.
(* Decaps spec, reduced: decaps_internal returns k_seed iff the
re-encryption matches, which it does by HFO. *)
have Hspec := decaps_internal_spec p sk ct pk Hkey.
move: Hspec => /=.
rewrite Hdec Hg /=.
move=> Hspec.
by rewrite (decaps_is_internal p sk ct) Hspec HFO.
qed.
(* -------------------------------------------------------------------- *)
+36 -13
View File
@@ -31,7 +31,8 @@
(* [NIST24] NIST FIPS 203, August 2024. *)
(* -------------------------------------------------------------------- *)
require import AllCore List Int IntDiv Distr DBool DInterval SmtMap Real.
require import AllCore List Int IntDiv Distr DBool DInterval SmtMap Real StdOrder.
import RealOrder.
(* -------------------------------------------------------------------- *)
(* Reuse types from MLKEM_Correctness.ec *)
@@ -84,9 +85,9 @@ module IND_CCA2 (A : IND_CCA2_Adv) = {
proc main() : bool = {
var rk, re : rand_t;
var ct_real, ct_chal;
var ct_real : ct_t;
var ss_chal : ss_t;
var guess;
var guess : bool;
rk <$ duniform [witness];
(pk, sk) <- keygen MLKem768 rk;
re <$ duniform [witness];
@@ -124,11 +125,17 @@ op delta_decrypt : ps_id_t -> real. (* Per-key delta from FIPS 203 7.3 *)
op msg_bits : int. (* |m| = 256 for ML-KEM *)
op j_bits : int. (* |J output| = 256 for ML-KEM *)
(* The query bounds are non-negative counts. This is a definitional fact
about the model's parameters (a number of oracle queries is never
negative); it is what makes the linear advantage bound below
monotone in the per-query advantages. *)
axiom q_bounds_ge0 : 0 <= q_D_bound /\ 0 <= q_G_bound /\ 0 <= q_H_bound.
axiom delta_bound_mlkem768 :
delta_decrypt MLKem768 <= 2%r ^^ (-164).
delta_decrypt MLKem768 <= 2%r ^ (-164).
axiom delta_bound_mlkem1024 :
delta_decrypt MLKem1024 <= 2%r ^^ (-174).
delta_decrypt MLKem1024 <= 2%r ^ (-174).
(* The IND-CPA PKE adversary advantage (Module-LWE / Module-LWR hardness). *)
op adv_indcpa_pke : ps_id_t -> real.
@@ -137,8 +144,8 @@ op adv_indcpa_pke : ps_id_t -> real.
op adv_indcca2_kem (p : ps_id_t) (A : real) : real =
q_G_bound%r * delta_decrypt p +
2%r * q_G_bound%r * adv_indcpa_pke p +
q_H_bound%r / 2%r ^^ msg_bits +
q_D_bound%r / 2%r ^^ j_bits.
q_H_bound%r / 2%r ^ msg_bits +
q_D_bound%r / 2%r ^ j_bits.
(* The FO-K reduction theorem statement. *)
axiom fo_k_reduction (p : ps_id_t) (Aadv : real) :
@@ -165,6 +172,17 @@ op adv_mlwr : ps_id_t -> real.
axiom indcpa_pke_reduction (p : ps_id_t) :
adv_indcpa_pke p <= adv_mlwe p + adv_mlwr p.
(* Advantages and per-key decryption-failure probabilities are
non-negative reals. This is a definitional fact about the model's
quantities (an advantage / probability is never negative); it is what
makes the linear bound monotone when the query counts are replaced by
their upper bounds in the concrete instantiation below. *)
axiom advantages_ge0 (p : ps_id_t) :
0%r <= delta_decrypt p /\
0%r <= adv_indcpa_pke p /\
0%r <= adv_mlwe p /\
0%r <= adv_mlwr p.
(* -------------------------------------------------------------------- *)
(* Composed bound *)
(* -------------------------------------------------------------------- *)
@@ -175,11 +193,12 @@ lemma mlkem_indcca2_security (p : ps_id_t) (Aadv : real) :
Aadv <=
q_G_bound%r * delta_decrypt p +
2%r * q_G_bound%r * (adv_mlwe p + adv_mlwr p) +
q_H_bound%r / 2%r ^^ msg_bits +
q_D_bound%r / 2%r ^^ j_bits.
q_H_bound%r / 2%r ^ msg_bits +
q_D_bound%r / 2%r ^ j_bits.
proof.
have H1 := fo_k_reduction p Aadv.
have H2 := indcpa_pke_reduction p.
have Hq := q_bounds_ge0.
rewrite /adv_indcca2_kem in H1.
smt().
qed.
@@ -205,18 +224,22 @@ axiom mlkem768_concrete_bound :
q_G_bound <= 2 ^ 64 /\
q_H_bound <= 2 ^ 64 /\
q_D_bound <= 2 ^ 64 /\
adv_mlwe MLKem768 <= 2%r ^^ (-192) /\
adv_mlwr MLKem768 <= 2%r ^^ (-192).
adv_mlwe MLKem768 <= 2%r ^ (-192) /\
adv_mlwr MLKem768 <= 2%r ^ (-192).
lemma mlkem768_concrete_security (Aadv : real) :
Aadv <=
(2^64)%r * delta_decrypt MLKem768 +
2%r * (2^64)%r * (adv_mlwe MLKem768 + adv_mlwr MLKem768) +
(2^64)%r / 2%r ^^ msg_bits +
(2^64)%r / 2%r ^^ j_bits.
(2^64)%r / 2%r ^ msg_bits +
(2^64)%r / 2%r ^ j_bits.
proof.
have H := mlkem_indcca2_security MLKem768 Aadv.
have HC := mlkem768_concrete_bound.
have Hq := q_bounds_ge0.
have Ha := advantages_ge0 MLKem768.
have Hp1 : 0%r < 2%r ^ msg_bits by apply expr_gt0.
have Hp2 : 0%r < 2%r ^ j_bits by apply expr_gt0.
smt().
qed.
+6 -4
View File
@@ -138,14 +138,14 @@ lemma mlkem_wire_format_keygen (p : ps_id_t) (r : rand_t) :
let (pk, sk) = keygen p r in
(pk_bytes pk, sk_bytes sk) = fips203_keygen p (rand_to_bytes r).
proof.
by apply circl_fips203_compliant_keygen.
by apply (circl_fips203_compliant_keygen p r).
qed.
lemma mlkem_wire_format_encaps (p : ps_id_t) (r : rand_t) (pk : pk_t) :
let (ct, ss) = encaps p r pk in
(ct_bytes ct, ss_bytes ss) = fips203_encaps p (rand_to_bytes r) (pk_bytes pk).
proof.
by apply circl_fips203_compliant_encaps.
by apply (circl_fips203_compliant_encaps p r pk).
qed.
lemma mlkem_wire_format_decaps (p : ps_id_t) (sk : sk_t) (ct : ct_t) :
@@ -170,7 +170,8 @@ proof.
move => HtapeEq.
have H1 := circl_fips203_compliant_keygen p r1.
have H2 := circl_fips203_compliant_keygen p r2.
rewrite /= in H1 H2.
rewrite /= in H1.
rewrite /= in H2.
smt().
qed.
@@ -183,7 +184,8 @@ proof.
move => HtapeEq.
have H1 := circl_fips203_compliant_encaps p r1 pk.
have H2 := circl_fips203_compliant_encaps p r2 pk.
rewrite /= in H1 H2.
rewrite /= in H1.
rewrite /= in H2.
smt().
qed.