Secure Messaging

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).

Definition6.2.1
Group: FrodoKEM, a Learning-With-Errors key encapsulation mechanism ( ). (2)
Group member previews
Preview
Theorem 6.2.2
Loading preview
Group member preview content is loaded from the rendered-fragment cache.
uses 0
Used by 2
Reverse dependency previews
Preview
Theorem 6.2.2
Loading preview
Reverse dependency preview content is loaded from the rendered-fragment cache.
✓L∃∀N

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

\ell, r, t

Bit lengths of the message/shared secret, error seed, and salt, respectively.

H(x,L)

SHAKE applied to x, producing L bits.

P(M), U(b)

Pack matrix M into a bit string, or recover a matrix from packed bits b.

x \| y

Concatenate two bit strings.

x \sample \{0,1\}^{L}

Sample an independent, uniformly random L-bit string.

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.ell
Lean code for Definition6.2.1●1 definition
  • 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. 
Theorem6.2.2
Group: FrodoKEM, a Learning-With-Errors key encapsulation mechanism ( ). (2)
Group member previews
Preview
Definition 6.2.1
Loading preview
Group member preview content is loaded from the rendered-fragment cache.
uses 1used by 0XL∃∀N

\todo

LeanLean anchor pending
Theorem6.2.3
Group: FrodoKEM, a Learning-With-Errors key encapsulation mechanism ( ). (2)
Group member previews
Preview
Definition 6.2.1
Loading preview
Group member preview content is loaded from the rendered-fragment cache.
uses 1used by 0XL∃∀N

\todo

LeanLean anchor pending

References: