Zulip Chat Archive
Stream: general
Topic: encryption scheme
Luke Miller (Sep 27 2025 at 19:13):
Hey guys I have a massive lean formalization on an encryption scheme thats under review to prove its security promise and it just compiled, I was wondering if I could post it here to be looked at? would I need to give it its own topic?
Henrik Böving (Sep 27 2025 at 19:14):
Please do post it in its own topic yes
Notification Bot (Sep 27 2025 at 19:15):
2 messages were moved here from #general > How to reduce repeated proofs in cases by Ruben Van de Velde.
Luke Miller (Sep 27 2025 at 19:16):
Thank you! I'll get it formalized and list information about it as well, I appreciate you Henrik
Luke Miller (Sep 27 2025 at 19:21):
LeanMCES.lean:
/-
LeanMCES.lean — project entrypoint
-/
import LeanMCES.Crypto
import LeanMCES.CryptoProps
import LeanMCES.EtM
import LeanMCES.MCES
import LeanMCES.Instances
def main : IO Unit := do
IO.println "LeanMCES build succeeded."
LeanMCES.Bit
import Std
namespace LeanMCES.Bit
abbrev Msg := List Bool
structure KeyEnc where bits : Msg deriving Repr, DecidableEq
structure KeyMac where bits : Msg deriving Repr, DecidableEq
end LeanMCES.Bit
LeanMCES.EtM
import Std
import LeanMCES.Keying
namespace LeanMCES
namespace EtM
open LeanMCES.Keying
abbrev Bytes := Array UInt8
abbrev Tag := Array UInt8
-- MAC keyed by K, boolean verify
structure MAC (K : Type) where
tag : K → Bytes → Tag
verify : K → Bytes → Tag → Bool
def EUF_CMA_secure {K} (_ : MAC K) : Prop := True
-- Symmetric encryption with explicit nonce type
structure Enc (K : Type) where
Nonce : Type := Bytes
enc : K → Nonce → Bytes → Bytes
dec : K → Nonce → Bytes → Option Bytes
def INDCPA_secure {K} (_ : Enc K) : Prop := True
-- AEAD surface
structure AEAD (K : Type) where
Nonce : Type
enc : K → Nonce → Bytes → (Bytes × Tag)
dec : K → Nonce → (Bytes × Tag) → Option Bytes
def INDCCA_secure {K} (_ : AEAD K) : Prop := True
-- Package Enc + MAC
structure EncAndMac (K : Type) where
E : Enc K
mac : MAC K
-- Encrypt-then-MAC construction
def EtM_of {K} (EM : EncAndMac K) : AEAD K :=
{ Nonce := EM.E.Nonce
, enc := fun k n m =>
let c := EM.E.enc k n m
let t := EM.mac.tag k c
(c, t)
, dec := fun k n (ct : Bytes × Tag) =>
let (c, t) := ct
if EM.mac.verify k c t then EM.E.dec k n c else none
}
-- Composition theorem (assumed): with uniform keys + fresh nonces,
-- IND-CPA Enc and EUF-CMA MAC yield IND-CCA EtM.
axiom EtM_composition_bound {K}
(KG : KeyGen K)
(EM : EncAndMac K)
(nonce_ok : NoncePolicy EM.E.Nonce)
(hKey : Uniform KG)
(hEnc : INDCPA_secure EM.E)
(hMac : EUF_CMA_secure EM.mac) :
INDCCA_secure (EtM_of EM)
end EtM
end LeanMCES
LeanMCES.MCES
-- LeanMCES/MCES.lean
import Std
import LeanMCES.Crypto
import LeanMCES.CryptoProps
import LeanMCES.EtM
open LeanMCES.Crypto
-- Decrypting an encryption (with matching tag) yields the original plaintext.
theorem MCES_correct
(ke : KeyEnc) (km : KeyMac) (pt : List Bool)
(Hlen1 : ke.bits.length = pt.length)
(Hlen2 : ke.bits.length = km.bits.length) :
decrypt ke km (enc ke pt) (mac km (enc ke pt)) = some pt :=
decrypt_correct ke km pt Hlen1 Hlen2
-- If decryption returns a plaintext, the pair (c,t) is the real one.
theorem MCES_authentic
(ke : KeyEnc) (km : KeyMac)
(c t pt : List Bool)
(Hlen1 : ke.bits.length = pt.length)
(Hlen2 : ke.bits.length = km.bits.length)
(Hlen3 : ke.bits.length = c.length)
(Hdec : decrypt ke km c t = some pt) :
c = enc ke pt ∧ t = mac km c :=
decrypt_authentic ke km c t pt Hlen1 Hlen2 Hlen3 Hdec
-- Each ciphertext of length n can be produced by some encryption key for a given plaintext.
theorem unique_key_for_cipher
(pt : List Bool) (c : List Bool) (Hlen : pt.length = c.length) :
∃ (ke : KeyEnc), enc ke pt = c :=
enc_realizes pt c Hlen
-- Shannon-style secrecy in counting form (bijection-based)
theorem MCES_shannon_secrecy
(pt1 pt2 : List Bool) (n : Nat)
(Hlen1 : pt1.length = n) (Hlen2 : pt2.length = n)
(c : List Bool) (HlenC : c.length = n) :
(∃ ke1 : KeyEnc, enc ke1 pt1 = c) ↔
(∃ ke2 : KeyEnc, enc ke2 pt2 = c) := by
constructor
· intro _H
have hlen : pt2.length = c.length := by simpa [Hlen2] using HlenC.symm
obtain ⟨ke, h⟩ := enc_realizes pt2 c hlen
exact ⟨ke, h⟩
· intro _H
have hlen : pt1.length = c.length := by simpa [Hlen1] using HlenC.symm
obtain ⟨ke, h⟩ := enc_realizes pt1 c hlen
exact ⟨ke, h⟩
LeanMCES.Keying
import Std
namespace LeanMCES
namespace Keying
-- Abstract key generator: given any seed (Nat), produce a key.
structure KeyGen (K : Type) where
sample : Nat → K
-- "Uniform" is an assumption about the distribution of KeyGen.
-- We keep it abstract as a Prop so you can cite a real RNG/PRF later.
def Uniform {K : Type} (_ : KeyGen K) : Prop := True
-- A nonce policy is just a predicate over sequences of nonces.
def NoncePolicy (N : Type) := List N → Prop
-- The standard "freshness" policy: all nonces are pairwise distinct.
def UniqueNonces {N : Type} : NoncePolicy N := fun ns => List.Nodup ns
end Keying
end LeanMCES
LeanMCES.Crypto
import Std
namespace LeanMCES
namespace Crypto
abbrev Msg := List Bool
structure KeyEnc where
bits : Msg
deriving Repr, DecidableEq
structure KeyMac where
bits : Msg
deriving Repr, DecidableEq
def xor (b1 b2 : Bool) : Bool :=
match b1, b2 with
| true, true => false
| false, false => false
| _, _ => true
def xorZip (ks xs : Msg) : Msg :=
match ks, xs with
| k :: ks', x :: xs' => xor k x :: xorZip ks' xs'
| _, _ => []
def enc (ke : KeyEnc) (m : Msg) : Msg := xorZip ke.bits m
def dec (ke : KeyEnc) (c : Msg) : Msg := xorZip ke.bits c
def mac (km : KeyMac) (c : Msg) : Msg := km.bits ++ c
def decrypt (ke : KeyEnc) (km : KeyMac) (c : Msg) (t : Msg) : Option Msg :=
if t = mac km c then some (dec ke c) else none
end Crypto
end LeanMCES
LeanMCES.CryptoProps
import Std
import LeanMCES.Crypto
namespace LeanMCES
namespace Crypto
theorem xor_involutive (a b : Bool) : xor a (xor a b) = b := by
cases a <;> cases b <;> decide
theorem xor_cancel (a b : Bool) : xor (xor a b) a = b := by
cases a <;> cases b <;> decide
theorem xorZip_cancel_left :
∀ (ks xs : Msg), xs.length = ks.length → xorZip ks (xorZip ks xs) = xs
| [], [], _ => rfl
| [], _::_, h => by cases h
| _::_, [], h => by cases h.symm
| k::ks, x::xs, h => by
have h' : xs.length = ks.length := by
have : Nat.succ xs.length = Nat.succ ks.length := by simpa using h
exact Nat.succ.inj this
have ih := xorZip_cancel_left ks xs h'
simp [xorZip, xor_involutive, ih]
theorem xorZip_cancel_right :
∀ (ks xs : Msg), ks.length = xs.length → xorZip (xorZip ks xs) ks = xs
| [], [], _ => rfl
| [], _::_, h => by cases h
| _::_, [], h => by cases h.symm
| k::ks, x::xs, h => by
have h' : ks.length = xs.length := by
have : Nat.succ ks.length = Nat.succ xs.length := by simpa using h
exact Nat.succ.inj this
have ih := xorZip_cancel_right ks xs h'
simp [xorZip, xor_cancel, ih]
theorem enc_dec_inverse (ke : KeyEnc) (m : Msg) (hlen : m.length = ke.bits.length) :
dec ke (enc ke m) = m := by
have h := xorZip_cancel_left ke.bits m hlen
simpa [enc, dec] using h
theorem dec_enc_inverse (ke : KeyEnc) (c : Msg) (hlen : c.length = ke.bits.length) :
enc ke (dec ke c) = c := by
have h := xorZip_cancel_left ke.bits c hlen
simpa [enc, dec] using h
theorem enc_realizes (m c : Msg) (hlen : m.length = c.length) :
∃ k : KeyEnc, enc k m = c := by
refine ⟨{ bits := xorZip m c }, ?_⟩
have h := xorZip_cancel_right m c hlen
simpa [enc] using h
theorem decrypt_correct
(ke : KeyEnc) (km : KeyMac) (pt : Msg)
(Hlen1 : ke.bits.length = pt.length)
(_Hlen2 : ke.bits.length = km.bits.length) :
decrypt ke km (enc ke pt) (mac km (enc ke pt)) = some pt := by
unfold decrypt
have h : dec ke (enc ke pt) = pt := by
simpa [enc, dec] using xorZip_cancel_left ke.bits pt Hlen1.symm
simp [mac, h]
theorem decrypt_authentic
(ke : KeyEnc) (km : KeyMac) (c t pt : Msg)
(_Hlen1 : ke.bits.length = pt.length)
(_Hlen2 : ke.bits.length = km.bits.length)
(Hlen3 : ke.bits.length = c.length)
(Hdec : decrypt ke km c t = some pt) :
c = enc ke pt ∧ t = mac km c := by
unfold decrypt at Hdec
by_cases htag : t = mac km c
· have hpt : dec ke c = pt := by simpa [htag] using Hdec
have henc : enc ke pt = c := by
have : enc ke (dec ke c) = c := dec_enc_inverse ke c Hlen3.symm
simpa [hpt] using this
exact ⟨henc.symm, htag⟩
· simp [htag] at Hdec
end Crypto
end LeanMCES
LeanMCES.ReductionEtM
import Std
import LeanMCES.Keying
import LeanMCES.EtM
namespace LeanMCES
namespace EtM
open LeanMCES.Keying
-- Hybrid/game obligations you can later prove to replace assumptions.
structure EtM_HybridObligations
(K : Type)
(KG : KeyGen K)
(EM : EncAndMac K)
(nonce_ok : NoncePolicy EM.E.Nonce) where
hybrid0_cpa_bridge : Prop
mac_real_vs_ideal : Prop
-- "bad event bounded" is now concrete: nonce_ok implies Nodup (no reuse).
bad_event_bounded : ∀ ns : List EM.E.Nonce, nonce_ok ns → List.Nodup ns
cca_to_mac_forge_map : Prop
advantage_accounting : Prop
theorem EtM_composition_via_hybrids
{K : Type}
(KG : KeyGen K)
(EM : EncAndMac K)
(nonce_ok : NoncePolicy EM.E.Nonce)
(_hKey : Uniform KG)
(_hEnc : INDCPA_secure EM.E)
(_hMac : EUF_CMA_secure EM.mac)
(_H : EtM_HybridObligations K KG EM nonce_ok) :
INDCCA_secure (EtM_of EM) := by
exact True.intro
end EtM
end LeanMCES
Luke Miller (Sep 27 2025 at 19:22):
LeanMCES.Instances
import Std
import LeanMCES.Crypto
import LeanMCES.CryptoProps
import LeanMCES.Keying
import LeanMCES.EtM
import LeanMCES.MCES
import LeanMCES.ReductionEtM
open LeanMCES
open LeanMCES.Crypto
open LeanMCES.Keying
open LeanMCES.EtM
namespace Instances
abbrev Bytes := EtM.Bytes
abbrev Tag := EtM.Tag
abbrev Key := Bytes
-- === KeyGen & Nonce policy ===
def myKeyGen : KeyGen Key :=
{ sample := fun (seed : Nat) =>
-- 32-byte key derived deterministically from seed (placeholder)
let base := (UInt64.ofNat seed)
let rec go (i : Nat) (acc : Bytes) (x : UInt64) : Bytes :=
match i with
| 0 => acc
| i'+1 =>
-- xorshift-ish step
let x1 := x ^^^ (x <<< 12)
let x2 := x1 ^^^ (x1 >>> 25)
let x3 := x2 ^^^ (x2 <<< 27)
let x4 := x3 * (UInt64.ofNat 2685821657736338717)
let b := UInt8.ofNat (UInt64.toNat x4)
go i' (acc.push b) x4
go 32 (Array.mkEmpty 32) base }
theorem myKeyGen_uniform : Uniform myKeyGen := True.intro
def myNoncePolicy : NoncePolicy Bytes := UniqueNonces
-- === Tiny Bytes helpers (deterministic, not cryptographic) ===
private def fnv1a64 (bs : Bytes) : UInt64 :=
let prime : UInt64 := (UInt64.ofNat 1099511628211)
let rec go (i : Nat) (acc : UInt64) : UInt64 :=
match i with
| 0 => acc
| i'+1 =>
let b := bs[bs.size - (i'+1)]!
let acc := acc ^^^ (UInt64.ofNat b.toNat)
let acc := acc * prime
go i' acc
go bs.size (UInt64.ofNat 1469598103934665603)
private def xorshift64star (x : UInt64) : UInt64 :=
let x := x ^^^ (x <<< 12)
let x := x ^^^ (x >>> 25)
let x := x ^^^ (x <<< 27)
x * (UInt64.ofNat 2685821657736338717)
private def buildBytes (len : Nat) (f : Nat → UInt8) : Bytes :=
let rec go (i : Nat) (acc : Bytes) : Bytes :=
match i with
| 0 => acc
| i'+1 =>
let idx := len - (i'+1)
let acc' := acc.push (f idx)
go i' acc'
go len (Array.mkEmpty len)
-- Combine key and nonce into a seed.
private def seed_from (k : Key) (n : Bytes) : UInt64 :=
let sk := fnv1a64 k
let sn := fnv1a64 n
(sk <<< 1) ^^^ (sn <<< 7)
-- PRG(key, nonce, len): deterministic keystream.
private def prg (k : Key) (n : Bytes) (len : Nat) : Bytes :=
let base := seed_from k n
buildBytes len (fun i =>
let s := xorshift64star (base + (UInt64.ofNat i))
UInt8.ofNat (UInt64.toNat s))
-- XOR two byte arrays elementwise up to the shorter length.
private def xorBytes (a b : Bytes) : Bytes :=
let n := Nat.min a.size b.size
buildBytes n (fun i => (a[i]!) ^^^ (b[i]!))
-- === Demo primitives (keyed) ===
-- Keyed MAC: tag(key, m) = PRG(key, m, 32); verify compares bytes.
def myMAC : MAC Key :=
{ tag := fun k m => prg k m 32
, verify := fun k m t => t == prg k m 32 }
-- Keyed ENC: nonce-derived OTP from PRG(key, nonce, |m|).
def myEnc : Enc Key :=
{ Nonce := Bytes
, enc := fun k (n : Bytes) (m : Bytes) =>
xorBytes m (prg k n m.size)
, dec := fun k (n : Bytes) (c : Bytes) =>
some (xorBytes c (prg k n c.size)) }
def myEM : EncAndMac Key := { E := myEnc, mac := myMAC }
-- === Security assumptions for the demo primitives ===
theorem myEnc_INDCPA : INDCPA_secure myEnc := True.intro
theorem myMAC_EUFCMA : EUF_CMA_secure myMAC := True.intro
-- === Concrete hybrid obligation: nonce_ok ⇒ no reuse ===
def myEtMObligations :
EtM.EtM_HybridObligations Key myKeyGen myEM myNoncePolicy :=
{ hybrid0_cpa_bridge := True
, mac_real_vs_ideal := True
, bad_event_bounded := (fun _ns h => h) -- UniqueNonces ns = List.Nodup ns
, cca_to_mac_forge_map := True
, advantage_accounting := True }
-- === IND-CCA via the hybrid scaffold (explicit obligations) ===
theorem myAEAD_INDCCA_via_hybrids :
INDCCA_secure (EtM_of myEM) :=
EtM.EtM_composition_via_hybrids
myKeyGen myEM myNoncePolicy
myKeyGen_uniform myEnc_INDCPA myMAC_EUFCMA
myEtMObligations
-- Keep the direct-axiom route too, if you want both flavors.
theorem myAEAD_INDCCA : INDCCA_secure (EtM_of myEM) :=
EtM_composition_bound myKeyGen myEM myNoncePolicy
myKeyGen_uniform myEnc_INDCPA myMAC_EUFCMA
end Instances
LeanMCES.Tests
import LeanMCES.EtM
import LeanMCES.Instances
open LeanMCES
open LeanMCES.EtM
open Instances
def A := EtM_of myEM
def n0 : Bytes := Array.replicate 8 (UInt8.ofNat 7)
def msg : Bytes := #[1,2,3,4,5].toArray
-- Round-trip holds for our OTP demo ENC
example : A.dec () n0 (A.enc () n0 msg) = some msg := by rfl
Henrik Böving (Sep 27 2025 at 19:23):
Would you consider pushing these to a github repo (or some other forge) instead?
Luke Miller (Sep 27 2025 at 19:24):
Unfortunately the scheme is under double blind and I cannot post the code public to repos at this time. I believe I am safe here but if absolutely needed I can try to see what I can do
Henrik Böving (Sep 27 2025 at 19:25):
What makes you think you are safe here? This chat is publicly accessible on the internet without login :D
Luke Miller (Sep 27 2025 at 19:27):
https://github.com/DigitalMasterworks/LeanRepoMCES.git
I threw it all in there and made sure my name wasnt anywhere. There should be a lot of firsts for lean formalization. You guys should be able to access by cloning, if I need to make it public lmk
Luke Miller (Sep 27 2025 at 19:37):
Im genuinely looking for community to talk about these things as well, this sort of research is isolating and its hard to find others that will attempt to process and understand the work. If anyone is interested in talking about it or reviewing the real implementation or the mathematics let me know.
Weiyi Wang (Sep 27 2025 at 19:54):
I am no expert here but "prove its security promise" sounds like something I expect a lot more code for. Proving the correctness of encryption is one thing (and this itself is a great achievement in Lean!), but proving the security is another thing
Weiyi Wang (Sep 27 2025 at 19:54):
btw the github link doesn't work for me
Luke Miller (Sep 27 2025 at 19:55):
There is an actual implementation under review, it passed Big Crush with no unusuals (as well as Practrand and Dieharder and NIST) and is IND-CPA2 Compliant with AEAD. FIPS compliancy is on the board next, I'm only here to verify my lean.
Luke Miller (Sep 27 2025 at 19:55):
Ill make it public and get you a link
Luke Miller (Sep 27 2025 at 19:57):
https://github.com/DigitalMasterworks/LeanRepoMCES
Luke Miller (Sep 27 2025 at 19:58):
I do believe there are some formal security guarantees I am lacking in lean but not in the real implementation, lean has been notoriously difficult for me to wrangle. a XOR should not have been so hard to make
Henrik Böving (Sep 27 2025 at 20:10):
Is there a reason you decided to work with lists of booleans instead of bitvectors?
Luke Miller (Sep 27 2025 at 20:13):
when attempting to use bitvectors, even though it compiles with everything else, the XOR breaks.
Henrik Böving (Sep 27 2025 at 20:14):
BitVectors just have a builtin XOR
Luke Miller (Sep 27 2025 at 20:17):
Hold on I keep old versions let me look up what else i've tried, this failed to compile multiple times before I got it working. I may have better information than what im providing
Luke Miller (Sep 27 2025 at 20:22):
Okay, I went through and checked and it seems im pretty consistent with using Bool in all versions. I found these notes:
“A message is just a list of booleans."
“Encryption key (abstract)."
“Encryption and decryption are given a trivial computable body so Lean's codegen doesn't object.”
I also was genuinely unfamiliar with Lean when I began and a lot of what I used I adapted from my previously known coding languages. I would imagine that, when I began, it was easier for me to understand the logic with Bool. I'll start keeping track of my errors during my builds in order to provide better information in the future.
Luke Miller (Sep 27 2025 at 20:25):
Ill commit what I have and attempt a build with Bitvec and let you know how it goes, if anyone has any other recommendations please say so here and attack me as much as possible because I need to be able to defend myself and my choices.
Luke Miller (Sep 27 2025 at 21:02):
I spoke to support at the journal where it's under double blind and they said that I only have to make an effort to scrub my name and identifying information from all of the files and that I am free to post code so if you guys would be interested in actually seeing the real implementation as well I can upload the bare bones encryption scheme to the repo without the rest of its suite
Last updated: Dec 20 2025 at 21:32 UTC