6.2. FrodoKEM
FrodoKEM is a post-quantum key-encapsulation mechanism which is based on the learning with errors (LWE) problem. Its distinguishing choice is to use plain LWE, without the additional ring or module structure used by many lattice schemes (Glabush et al. (2025), Introduction).
The construction turns an underlying public-key encryption scheme called FrodoPKE into a KEM using a Fujisaki–Okamoto transform. Encapsulation produces a ciphertext and a shared secret; decapsulation uses the secret key to recover that shared secret. FrodoKEM has two variants: salted and ephemeral. Each variant offers three parameter sizes—640, 976, and 1344—with AES128 or SHAKE128 used to generate the public matrix. The two variants, three sizes, and two matrix generators give twelve combinations in total (Glabush et al. (2025), §6.2). The salted variant adds a public salt and longer error seeds to address attacks targeting many ciphertexts (Glabush et al. (2025), §§1.1–1.3). The ephemeral variant, eFrodoKEM, omits the salt and requires fewer than 256 ciphertexts per public key (Longa et al. (2026), §8).
The algorithms below describe key generation, encapsulation, and decapsulation for a fixed parameter set. FrodoPKE supplies the underlying encryption routines; its public key is a seed and a matrix, and its secret key is a transposed matrix.
Notation | Meaning |
|---|---|
| Bit lengths of the message/shared secret, error seed, and salt, respectively. |
|
SHAKE applied to |
|
Pack matrix |
| Concatenate two bit strings. |
|
Sample an independent, uniformly random |
P(M) uses the draft's packing convention, representing each packed byte
least-significant-bit first. U(b) reverses this conversion. In Lean, these
operations are implemented by packBits and unpackBits.
\mathsf{KeyGen}()Generate a key pair and store a fallback secret for ciphertexts that fail validation.
Here \mathsf{PKE.KeyGen}(a,\mathsf{seed}_{SE}) denotes key generation with supplied
seeds, implemented by PKE.keygenFromSeeds.
\begin{array}{l}
s \sample \{0,1\}^{\ell};\quad
\mathsf{seed}_{SE} \sample \{0,1\}^{r};\quad z \sample \{0,1\}^{128} \\[0.35em]
a \gets H(z,128) \\[0.35em]
((a,B),S^{\mathsf T}) \gets \mathsf{PKE.KeyGen}(a,\mathsf{seed}_{SE}) \\[0.35em]
b \gets P(B);\quad \mathsf{pk} \gets (a,b) \\[0.35em]
\mathsf{pkh} \gets H(a\|b,\ell) \\[0.35em]
\mathsf{sk} \gets (s,\mathsf{pk},S^{\mathsf T},\mathsf{pkh}) \\[0.35em]
\Return (\mathsf{pk},\mathsf{sk})
\end{array}
Lean specification
def keygen {ps : ParameterSet} (ops : Operations ps) :
ProbComp (PublicKey ps × SecretKey ps) := do
let fallback ← $ᵗ (SharedSecretBits ps)
let seedSE ← $ᵗ (SeedSEBits ps)
let z ← $ᵗ (Bits lenZ)
let p := ps.params
let seedA := ops.shake z.toList lenSeedA
let a := ops.gen seedA
let stream := ops.shake
(keygenPrefix.toList ++ seedSE.toList)
(nbar * p.n * lenChi + p.n * nbar * lenChi)
let samples := splitBits
(a := nbar * p.n * lenChi)
(b := p.n * nbar * lenChi) stream
let st := (SampleMatrix ps.errorTable nbar p.n samples.1).map
(fun x : ℤ => (x : ZMod p.q))
let e := (SampleMatrix ps.errorTable p.n nbar samples.2).map
(fun x : ℤ => (x : ZMod p.q))
let b := a * st.transpose + e
let pk : PublicKey ps :=
{ seedA := seedA
b := packBits ps p.n nbar (publicKeyBits_mod_eight ps) b }
return (pk,
{ fallback := fallback
publicKey := pk
secretTranspose := st
publicKeyHash := hashPublicKey ops pk })\mathsf{Encaps}(\mathsf{pk}=(a,b))
Encrypt a fresh random message, then derive the shared secret from the ciphertext
and intermediate key. In eFrodoKEM, t=0 and the salt is empty.
The hash output is split into an r-bit seed and an \ell-bit key.
\begin{array}{l}
\mu \sample \{0,1\}^{\ell};\quad \mathsf{salt} \sample \{0,1\}^{t} \\[0.35em]
\mathsf{pkh} \gets H(a\|b,\ell) \\[0.35em]
(\mathsf{seed}_{SE},k) \gets H(\mathsf{pkh}\|\mu\|\mathsf{salt},r+\ell) \\[0.35em]
(B',C) \gets \mathsf{PKE.Enc}((a,U(b)),\mu;\mathsf{seed}_{SE}) \\[0.35em]
c_1 \gets P(B');\quad c_2 \gets P(C);\quad c \gets (c_1,c_2,\mathsf{salt}) \\[0.35em]
\mathsf{ss} \gets H(c_1\|c_2\|\mathsf{salt}\|k,\ell) \\[0.35em]
\Return (c,\mathsf{ss})
\end{array}
Lean specification
def encaps {ps : ParameterSet} (ops : Operations ps)
(pk : PublicKey ps) :
ProbComp (Ciphertext ps × SharedSecretBits ps) := do
let message ← $ᵗ (MessageBits ps)
let salt ← $ᵗ (SaltBits ps)
let p := ps.params
let derived := deriveSeedAndKey ops (hashPublicKey ops pk) message salt
let stream := ops.shake
(encryptionPrefix.toList ++ derived.1.toList)
(mbar * p.n * lenChi +
(mbar * p.n * lenChi + mbar * nbar * lenChi))
let first := splitBits
(a := mbar * p.n * lenChi)
(b := mbar * p.n * lenChi + mbar * nbar * lenChi) stream
let rest := splitBits
(a := mbar * p.n * lenChi)
(b := mbar * nbar * lenChi) first.2
let sp := (SampleMatrix ps.errorTable mbar p.n first.1).map
(fun x : ℤ => (x : ZMod p.q))
let ep := (SampleMatrix ps.errorTable mbar p.n rest.1).map
(fun x : ℤ => (x : ZMod p.q))
let a := ops.gen pk.seedA
let bp := sp * a + ep
let c1 := packBits ps mbar p.n (ciphertext1Bits_mod_eight ps) bp
let epp := (SampleMatrix ps.errorTable mbar nbar rest.2).map
(fun x : ℤ => (x : ZMod p.q))
let b := unpackBits ps p.n nbar (publicKeyBits_mod_eight ps) pk.b
let v := sp * b + epp
let c2 := packBits ps mbar nbar (ciphertext2Bits_mod_eight ps)
(v + Encode p (message.cast (messageBits_length ps)))
let c : Ciphertext ps := { c1 := c1, c2 := c2, salt := salt }
return (c, ops.shake
(c.c1.toList ++ c.c2.toList ++ c.salt.toList ++ derived.2.toList)
p.ell)\mathsf{Decaps}(\mathsf{sk},c)Recover a candidate message, then derive an error seed and an intermediate key as in encapsulation. Reencrypt the message and compare both reconstructed matrices with those received. If either differs, replace the intermediate key with the fallback secret. Finally, hash the received ciphertext with the selected key.
\begin{array}{l}
(s,(a,b),S^{\mathsf T},\mathsf{pkh}) \gets \mathsf{sk};\quad
(c_1,c_2,\mathsf{salt}) \gets c \\[0.35em]
(B',C) \gets (U(c_1),U(c_2)) \\[0.35em]
\mu' \gets \mathsf{PKE.Dec}(S^{\mathsf T},(B',C)) \\[0.35em]
(\mathsf{seed}'_{SE},k') \gets H(\mathsf{pkh}\|\mu'\|\mathsf{salt},r+\ell) \\[0.35em]
(B'',C') \gets \mathsf{PKE.Enc}((a,U(b)),\mu';\mathsf{seed}'_{SE}) \\[0.35em]
\widehat{k} \gets \begin{cases}
k' & \text{if } B'=B'' \text{ and } C=C', \\[0.35em]
s & \text{otherwise} \end{cases} \\[0.35em]
\Return H(c_1\|c_2\|\mathsf{salt}\|\widehat{k},\ell)
\end{array}
Lean specification
def decaps {ps : ParameterSet} (ops : Operations ps)
(sk : SecretKey ps) (c : Ciphertext ps) : SharedSecretBits ps :=
let p := ps.params
let bp := unpackBits ps mbar p.n (ciphertext1Bits_mod_eight ps) c.c1
let cm := unpackBits ps mbar nbar (ciphertext2Bits_mod_eight ps) c.c2
let m := cm - bp * sk.secretTranspose.transpose
let message := (Decode p m).cast (messageBits_length ps).symm
let derived := deriveSeedAndKey ops sk.publicKeyHash message c.salt
let stream := ops.shake
(encryptionPrefix.toList ++ derived.1.toList)
(mbar * p.n * lenChi +
(mbar * p.n * lenChi + mbar * nbar * lenChi))
let first := splitBits
(a := mbar * p.n * lenChi)
(b := mbar * p.n * lenChi + mbar * nbar * lenChi) stream
let rest := splitBits
(a := mbar * p.n * lenChi)
(b := mbar * nbar * lenChi) first.2
let sp := (SampleMatrix ps.errorTable mbar p.n first.1).map
(fun x : ℤ => (x : ZMod p.q))
let ep := (SampleMatrix ps.errorTable mbar p.n rest.1).map
(fun x : ℤ => (x : ZMod p.q))
let a := ops.gen sk.publicKey.seedA
let bpp := sp * a + ep
let epp := (SampleMatrix ps.errorTable mbar nbar rest.2).map
(fun x : ℤ => (x : ZMod p.q))
let b := unpackBits ps p.n nbar (publicKeyBits_mod_eight ps) sk.publicKey.b
let v := sp * b + epp
let cp := v + Encode p (message.cast (messageBits_length ps))
let selected := if bp = bpp ∧ cm = cp then derived.2 else sk.fallback
ops.shake
(c.c1.toList ++ c.c2.toList ++ c.salt.toList ++ selected.toList)
p.ellLean code for Definition6.2.1●1 definition
Associated Lean declarations
-
FrodoKEM.KEM.asKEMScheme[complete]
-
FrodoKEM.KEM.asKEMScheme[complete]
-
defdefined in ToVCVio/LatticeCrypto/FrodoKEM/Construction.leancomplete
def FrodoKEM.KEM.asKEMScheme {ps : FrodoKEM.ParameterSet} (ops : FrodoKEM.Operations ps) : KEMScheme ProbComp (FrodoKEM.SharedSecretBits ps) (FrodoKEM.PublicKey ps) (FrodoKEM.SecretKey ps) (FrodoKEM.Ciphertext ps)
def FrodoKEM.KEM.asKEMScheme {ps : FrodoKEM.ParameterSet} (ops : FrodoKEM.Operations ps) : KEMScheme ProbComp (FrodoKEM.SharedSecretBits ps) (FrodoKEM.PublicKey ps) (FrodoKEM.SecretKey ps) (FrodoKEM.Ciphertext ps)
Package the FrodoKEM algorithms as a VCVio `KEMScheme`. Implicit rejection returns a shared secret, so decapsulation always returns `some`. For ephemeral parameter sets, Section 8 of `[LBES26]` requires fewer than 256 ciphertexts per public key. This adapter does not check that usage restriction. This adapter alone asserts no correctness or security guarantee.
\todo
\todo
References: