Skip to content
PQ Stealth Addresses

KEMAnonymity

Proved: KEM (VCVio's KEMScheme over ProbComp) and its anonymity game -- key privacy, which VCVio does not ship; ofKEM, whose unlinkability EQUALS the KEM's anonymity advantage; ofKEMFull, the faithful scheme whose announcement also carries auxGen sharedSecret pk and whose scan recomputes it, with unlinkAdvantage_ofKEMFull_le bounding unlinkability by two shared-secret-hiding terms, auxKeyIndependence, and anonymity; and detection completeness of ofKEMFull from VCVio's KEMScheme.PerfectlyCorrect.

Assumed: nothing here. anonymity → MLWE for ML-KEM is the open piece (AnonymityFromSPR). See docs/announcement-model.md.

KEM

A key encapsulation mechanism over ProbComp: key generation, encapsulation (producing a ciphertext and a shared secret), and decapsulation.

abbrev KEM (PK SK C K : Type) := KEMScheme ProbComp K PK SK C

KEMAnonymity.lean:30-31 ↗

An abstract KEM

probOutput_true_eq_one_of_imp

Transfer a probability-one Boolean verdict along a pointwise implication: VCVio's probEvent_mono read through probOutput_map. Used to derive detection completeness from the strictly stronger verdict of KEM correctness.

theorem probOutput_true_eq_one_of_imp {α : Type} (oa : ProbComp α) {p q : α → Bool}
    (h : ∀ a ∈ support oa, p a = true → q a = true)
    (hp : Pr[= true | (do let a ← oa; pure (p a))] = 1) :
    Pr[= true | (do let a ← oa; pure (q a))] = 1 := by
proof
  rw [bind_pure_comp, probOutput_map] at hp ⊢
  exact le_antisymm probEvent_le_one (hp ▸ probEvent_mono h)

KEMAnonymity.lean:37-43 ↗

A Pr[= true | …] = 1 transfer lemma

run

The draw prefix common to KEM correctness and to detection completeness: a keypair, an encapsulation to its public key, and a decapsulation of the result.

def run : ProbComp ((PK × SK) × (C × K) × Option K) := do
  let ks ← kem.keygen
  let ck ← kem.encaps ks.1
  let k? ← kem.decaps ks.2 ck.1
  pure (ks, ck, k?)

KEMAnonymity.lean:57-62 ↗

correctExp_eq

theorem correctExp_eq [DecidableEq K] :
    kem.CorrectExp = (do let a ← kem.run; pure (decide (a.2.2 = some a.2.1.2))) := by
proof
  simp only [KEMScheme.CorrectExp, KEM.run, bind_assoc, pure_bind]

KEMAnonymity.lean:65-68 ↗

Correctness

anonSetup

Shared prefix: two keypairs, both public keys published.

def anonSetup : ProbComp (PK × PK) := do
  let (pk0, _) ← kem.keygen
  let (pk1, _) ← kem.keygen
  pure (pk0, pk1)

KEMAnonymity.lean:75-79 ↗

anonBranch

def anonBranch (b : Bool) (a : PK × PK) : ProbComp Bool := do
  let (c, _) ← kem.encaps (if b then a.2 else a.1)
  adv a.1 a.2 c

KEMAnonymity.lean:81-84 ↗

AnonExp

def AnonExp : ProbComp Bool := do
  let a ← kem.anonSetup
  let b ← ($ᵗ Bool)
  let z ← kem.anonBranch adv b a
  pure (b == z)

KEMAnonymity.lean:86-91 ↗

anonAdvantage

noncomputable def anonAdvantage : ℝ := (kem.AnonExp adv).boolBiasAdvantage

KEMAnonymity.lean:93-94 ↗

anonAdvantage_eq_branchDistAdvantage

axioms: propext, Classical.choice, Quot.sound · sorry-free ✓

theorem anonAdvantage_eq_branchDistAdvantage :
    kem.anonAdvantage adv =
      (kem.anonSetup >>= kem.anonBranch adv true).boolDistAdvantage
      (kem.anonSetup >>= kem.anonBranch adv false) :=
  ProbComp.boolBiasAdvantage_bind_uniformBool_branch kem.anonSetup (kem.anonBranch adv)

KEMAnonymity.lean:97-102 ↗

Anonymity (key privacy)

StealthScheme.ofKEM

Stealth scheme from a KEM: announcement = ciphertext, detection = "did decapsulation succeed". A teaching model, not the scheme -- on an implicit-rejection KEM its scan is the constant true.

def StealthScheme.ofKEM (kem : KEM PK SK C K) : StealthScheme PK SK C where
  keygen := kem.keygen
  announce pk := do
    let (c, _) ← kem.encaps pk
    pure c
  scan sk c := do
    let k ← kem.decaps sk c
    pure k.isSome

KEMAnonymity.lean:112-120 ↗

unlinkAdvantage_ofKEM_eq_anonAdvantage

axioms: propext, Classical.choice, Quot.sound · sorry-free ✓

theorem unlinkAdvantage_ofKEM_eq_anonAdvantage
    (kem : KEM PK SK C K) (adv : StealthScheme.UnlinkAdv PK C) :
    (StealthScheme.ofKEM kem).unlinkAdvantage adv = kem.anonAdvantage adv := by
proof
  unfold StealthScheme.unlinkAdvantage KEM.anonAdvantage
  congr 1
  simp only [StealthScheme.UnlinkExp, KEM.AnonExp, StealthScheme.unlinkSetup,
    KEM.anonSetup, StealthScheme.unlinkBranch, KEM.anonBranch,
    StealthScheme.ofKEM, bind_assoc, pure_bind]

KEMAnonymity.lean:124-132 ↗

The KEM-based stealth scheme and the reduction

StealthScheme.ofKEMFull

Stealth scheme from a KEM with a full announcement: ciphertext plus auxGen sharedSecret pk. Detection recomputes the auxiliary data from the decapsulated secret and the recipient's OWN key, hence the SK × PK state.

def StealthScheme.ofKEMFull [DecidableEq Aux] (kem : KEM PK SK C K)
    (auxGen : K → PK → Aux) : StealthScheme PK (SK × PK) (C × Aux) where
  keygen := do
    let ks ← kem.keygen
    pure (ks.1, (ks.2, ks.1))
  announce pk := do
    let ck ← kem.encaps pk
    pure (ck.1, auxGen ck.2 pk)
  scan sk ca := do
    let k? ← kem.decaps sk.1 ca.1
    pure (k?.elim false fun k => decide (ca.2 = auxGen k sk.2))

KEMAnonymity.lean:145-156 ↗

perfectlyComplete_ofKEMFull

axioms: propext, Classical.choice, Quot.sound · sorry-free ✓

theorem perfectlyComplete_ofKEMFull [DecidableEq K] [DecidableEq Aux]
    (kem : KEM PK SK C K) (auxGen : K → PK → Aux)
    (hkem : kem.PerfectlyCorrect ProbCompRuntime.probComp) :
    (StealthScheme.ofKEMFull kem auxGen).PerfectlyComplete := by
proof
  have hCE : (StealthScheme.ofKEMFull kem auxGen).CorrectExp =
      (do let a ← kem.run
          pure (a.2.2.elim false fun k => decide (auxGen a.2.1.2 a.1.1 = auxGen k a.1.1))) := by
    simp only [StealthScheme.CorrectExp, StealthScheme.ofKEMFull, KEM.run,
      bind_assoc, pure_bind]
  rw [StealthScheme.PerfectlyComplete, hCE]
  refine probOutput_true_eq_one_of_imp (p := fun a => decide (a.2.2 = some a.2.1.2))
    kem.run ?_ ?_
  · intro a _ ha
    simp only [decide_eq_true_eq] at ha
    simp only [ha, Option.elim_some, decide_true]
  · rw [← kem.correctExp_eq]
    exact hkem

KEMAnonymity.lean:160-177 ↗

unlinkSetup_ofKEMFull

theorem unlinkSetup_ofKEMFull [DecidableEq Aux] (kem : KEM PK SK C K)
    (auxGen : K → PK → Aux) :
    (StealthScheme.ofKEMFull kem auxGen).unlinkSetup = kem.anonSetup := by
proof
  simp only [StealthScheme.unlinkSetup, KEM.anonSetup, StealthScheme.ofKEMFull,
    bind_assoc, pure_bind]

KEMAnonymity.lean:180-185 ↗

StealthScheme.UnlinkAdv.cipherOf

The ciphertext-anonymity adversary induced by a full-announcement one: it synthesizes the auxiliary data from a FRESH random shared secret and a FIXED public key, recipient 0's. See docs/announcement-model.md.

def StealthScheme.UnlinkAdv.cipherOf
    (adv : StealthScheme.UnlinkAdv PK (C × Aux)) (auxGen : K → PK → Aux) :
    StealthScheme.UnlinkAdv PK C :=
  fun pk0 pk1 c => do
    let k' ← ($ᵗ K)
    adv pk0 pk1 (c, auxGen k' pk0)

KEMAnonymity.lean:191-197 ↗

randAuxBranch

The intermediate game on branch b: same ciphertext and same public key, but the auxiliary data is built from a fresh random shared secret.

def randAuxBranch (b : Bool) (a : PK × PK) : ProbComp Bool := do
  let (c, _) ← kem.encaps (if b then a.2 else a.1)
  let k' ← ($ᵗ K)
  adv a.1 a.2 (c, auxGen k' (if b then a.2 else a.1))

KEMAnonymity.lean:203-207 ↗

randAuxBranch_false

theorem randAuxBranch_false :
    randAuxBranch kem auxGen adv false = kem.anonBranch (adv.cipherOf auxGen) false := rfl

KEMAnonymity.lean:211-213 ↗

sharedSecretHiding

Shared-secret-hiding advantage on branch b: real shared secret versus a fresh random key, from the SAME public key on both sides. A KEM IND-CPA advantage (sharedSecretHiding_eq_indCpaAdvantage).

noncomputable def sharedSecretHiding (b : Bool) : ℝ :=
  ((StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
      (StealthScheme.ofKEMFull kem auxGen).unlinkBranch adv b).boolDistAdvantage
    ((StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
      randAuxBranch kem auxGen adv b)

KEMAnonymity.lean:219-224 ↗

auxKeyIndependence

noncomputable def auxKeyIndependence : ℝ :=
  ((StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
      randAuxBranch kem auxGen adv true).boolDistAdvantage
    (kem.anonSetup >>= kem.anonBranch (adv.cipherOf auxGen) true)

KEMAnonymity.lean:228-232 ↗

unlinkAdvantage_ofKEMFull_le

axioms: propext, Classical.choice, Quot.sound · sorry-free ✓

theorem unlinkAdvantage_ofKEMFull_le :
    (StealthScheme.ofKEMFull kem auxGen).unlinkAdvantage adv ≤
      sharedSecretHiding kem auxGen adv true
      + auxKeyIndependence kem auxGen adv
      + kem.anonAdvantage (adv.cipherOf auxGen)
      + sharedSecretHiding kem auxGen adv false := by
proof
  have hQf : kem.anonSetup >>= kem.anonBranch (adv.cipherOf auxGen) false =
      (StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
        randAuxBranch kem auxGen adv false := by
    rw [randAuxBranch_false, unlinkSetup_ofKEMFull]
  rw [StealthScheme.unlinkAdvantage_eq_branchDistAdvantage,
    KEM.anonAdvantage_eq_branchDistAdvantage]
  unfold sharedSecretHiding auxKeyIndependence
  rw [hQf, ProbComp.boolDistAdvantage_comm
    ((StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
      (StealthScheme.ofKEMFull kem auxGen).unlinkBranch adv false)]
  set Pt := (StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
    (StealthScheme.ofKEMFull kem auxGen).unlinkBranch adv true
  set Pf := (StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
    (StealthScheme.ofKEMFull kem auxGen).unlinkBranch adv false
  set Mt := (StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
    randAuxBranch kem auxGen adv true
  set Mf := (StealthScheme.ofKEMFull kem auxGen).unlinkSetup >>=
    randAuxBranch kem auxGen adv false
  set Qt := kem.anonSetup >>= kem.anonBranch (adv.cipherOf auxGen) true
  calc Pt.boolDistAdvantage Pf
      ≤ Pt.boolDistAdvantage Mt + Mt.boolDistAdvantage Pf :=
        ProbComp.boolDistAdvantage_triangle Pt Mt Pf
    _ ≤ Pt.boolDistAdvantage Mt + (Mt.boolDistAdvantage Qt + Qt.boolDistAdvantage Pf) := by
        gcongr
        exact ProbComp.boolDistAdvantage_triangle Mt Qt Pf
    _ ≤ Pt.boolDistAdvantage Mt + (Mt.boolDistAdvantage Qt
          + (Qt.boolDistAdvantage Mf + Mf.boolDistAdvantage Pf)) := by
        gcongr
        exact ProbComp.boolDistAdvantage_triangle Qt Mf Pf
    _ = Pt.boolDistAdvantage Mt + Mt.boolDistAdvantage Qt
          + Qt.boolDistAdvantage Mf + Mf.boolDistAdvantage Pf := by
        ring

KEMAnonymity.lean:236-274 ↗