KEM-DEM Hybrid Public Key Encryption
The Chained Symmetric Encryption worked example used a single primitive twice, with two reductions both aimed at the same assumption.
In this worked example we examine KEM-DEM hybrid public key encryption, which combines a key encapsulation mechanism (KEM) and a symmetric encryption scheme into a public key encryption scheme . To encrypt a message under a public key , the construction encapsulates a fresh shared secret under and then uses as a one-time symmetric key to encrypt . The ciphertext is:
We prove that satisfies CPA security, assuming that is CPA-secure, that satisfies one-time secrecy, and that ’s key generation algorithm produces keys uniformly distributed over its key space. The proof uses five reductions: two to KEM CPA security (one forward, one back), two to key uniformity of (one forward, one back), and one to one-time secrecy of in the middle.
This worked example introduces three patterns beyond chained encryption: multi-primitive composition (a KEM and a SymEnc combine into a PKE), reductions in opposite directions (the same assumption is invoked twice, once forward and once backward), and bridging between key distributions with a key-uniformity assumption.
The full proof is available at Proofs/PubKeyEnc/HybridKEMDEM_INDCPA_MultiChal.proof.
- 1. What this proves
- 2. The primitives and the security games
- 3. The KEMDEM scheme
- 4. The proof structure
- 5. Reduction : Game 0 → Game 1
- 6. Reduction : Game 1 → Game 2
- 7. Reduction : Game 2 → Game 3
- 8. Reduction : Game 3 → Game 4
- 9. Reduction : Game 4 → Game 5
- 10. The full games block
- 11. Verifying
- 12. Lessons learned
1. What this proves
KEM-DEM hybrid public key encryption is the standard construction that turns a key encapsulation mechanism (KEM) and a symmetric encryption scheme into a public key encryption (PKE) scheme . The KEM is used to establish a per-message shared secret under the recipient’s public key, and the symmetric scheme is then used to encrypt the actual message under that shared secret. The ciphertext is the pair . Decryption reverses this: decapsulate to recover the shared secret, then symmetrically decrypt . The structural requirement is — the KEM’s shared secret space must be a subset of the symmetric encryption scheme’s key space — so that the shared secret can be used directly as a symmetric key.
The theorem proved here is: if is CPA-secure, has one-time secrecy (OTS), and ’s key generation algorithm produces uniformly distributed keys, then is CPA-secure as a public key encryption scheme.
Why the key-uniformity assumption is needed. The KEMDEM scheme uses the KEM’s shared secret directly as the symmetric key — it never calls E.KeyGen(). But the one-time secrecy game for does call E.KeyGen() to sample its one-time key. So at the point where the proof wants to invoke one-time secrecy, the “symmetric key” in the current game is a uniform sample from the KEM’s shared secret space (which equals ), while the OTS challenger samples its key via E.KeyGen(). These two distributions need to be lined up before the OTS assumption can be applied. The proof does this with a key-uniformity assumption: a small left/right game asserting that E.KeyGen() is indistinguishable from uniform sampling over E.Key. Two extra hops — one forward, one back — bridge the gap around the one-time secrecy hop.
Joy of Cryptography / Boneh-Shoup parallel. The KEM-DEM construction and its CPA-security proof appear in Mike Rosulek’s Joy of Cryptography and as Exercise 11.9 in Boneh and Shoup’s A Graduate Course in Applied Cryptography. The proof here follows the same game-hopping argument described in those references: replace the real KEM shared secret with a random key (using KEM CPA), switch the ciphertext from encrypting the left message to encrypting the right message (using SymEnc one-time secrecy), and then restore the real KEM shared secret (using KEM CPA again, in the reverse direction). ProofFrog mechanically verifies each hop of this argument.
2. The primitives and the security games
This proof involves three different cryptographic primitives — symmetric encryption , key encapsulation , and public key encryption — each with its own security game, plus a small key-uniformity game on . We recall all of them before looking at the construction.
Symmetric encryption primitive
A symmetric encryption scheme is, as in the chained encryption worked example, a triple of algorithms over a key space , message space , and ciphertext space :
Notice that Dec here is total (, not ), reflecting the simplifying assumption that decryption never fails. This is a different primitive from the one used in the chained encryption example, so it has its own file. The FrogLang primitive file is Primitives/NonNullableSymEnc.primitive:
Primitive SymEnc(Set MessageSpace, Set CiphertextSpace, Set KeySpace) {
Set Message = MessageSpace;
Set Ciphertext = CiphertextSpace;
Set Key = KeySpace;
Key KeyGen();
Ciphertext Enc(Key k, Message m);
deterministic Message Dec(Key k, Ciphertext c);
}
Try it. From the examples/ directory, run proof_frog check Primitives/NonNullableSymEnc.primitive to type-check the primitive file, or open it in the web editor and click Type Check.
One-time secrecy game (left/right)
The one-time secrecy game used in this proof is phrased as a left/right indistinguishability game, in contrast to the real/random formulation used in the chained encryption worked example. The adversary submits two messages and , and the challenger encrypts either (Left game) or (Right game) under a freshly sampled key. The adversary must guess which side it is interacting with. As in any “one-time” experiment, the key is sampled fresh on every oracle call via E.KeyGen():
The FrogLang security game file is Games/SymEnc/INDOT.game:
import '../../Primitives/SymEnc.primitive';
Game Left(SymEnc E) {
E.Ciphertext Eavesdrop(E.Message mL, E.Message mR) {
E.Key k = E.KeyGen();
E.Ciphertext c = E.Enc(k, mL);
return c;
}
}
Game Right(SymEnc E) {
E.Ciphertext Eavesdrop(E.Message mL, E.Message mR) {
E.Key k = E.KeyGen();
E.Ciphertext c = E.Enc(k, mR);
return c;
}
}
export as INDOT;
Try it. From the examples/ directory, run proof_frog check Games/SymEnc/INDOT.game, or open the file in the web editor and click Type Check.
Key uniformity game
The second assumption on is key uniformity: the distribution of keys produced by E.KeyGen() is indistinguishable from a uniform sample over E.Key. This is also phrased as a left/right game, with the two sides using the two sampling methods:
This assumption is the bridge that lets the proof cross from a “symmetric key sampled uniformly from ” world to a “symmetric key generated by E.KeyGen()” world (which is what the OTS game uses). It will be invoked twice: once on each side of the OTS hop. The FrogLang security game file is Games/SymEnc/KeyUniformity.game:
import '../../Primitives/SymEnc.primitive';
Game Real(SymEnc E) {
E.Key Challenge() {
E.Key k = E.KeyGen();
return k;
}
}
Game Random(SymEnc E) {
E.Key Challenge() {
E.Key k <- E.Key;
return k;
}
}
export as KeyUniformity;
Try it. From the examples/ directory, run proof_frog check Games/SymEnc/KeyUniformity.game, or open the file in the web editor and click Type Check.
KEM primitive
A key encapsulation mechanism is a triple of algorithms over a public key space , secret key space , ciphertext space , and shared secret space :
Encaps produces a pair: a shared secret plus a ciphertext that travels to the recipient. Decaps recovers the shared secret given the ciphertext and the recipient’s secret key. This version assumes that decapsulation never fails. The FrogLang primitive file is Primitives/KEM.primitive:
Primitive KEM(Set SharedSecretSpace, Set CiphertextSpace, Set PKeySpace, Set SKeySpace) {
Set SharedSecret = SharedSecretSpace;
Set Ciphertext = CiphertextSpace;
Set PublicKey = PKeySpace;
Set SecretKey = SKeySpace;
[PublicKey, SecretKey] KeyGen();
[SharedSecret, Ciphertext] Encaps(PublicKey pk);
deterministic SharedSecret Decaps(SecretKey sk, Ciphertext m);
}
Try it. From the examples/ directory, run proof_frog check Primitives/KEM.primitive, or open the file in the web editor and click Type Check.
KEM CPA game
CPA security for a KEM captures the indistinguishability of real and random shared secrets, given the corresponding KEM ciphertext. The adversary’s Initialize call returns the public key, and it may then call a Challenge oracle that returns a pair. In the Real game, is the genuine shared secret produced by encapsulation. In the Random game, the ciphertext is still a genuine encapsulation but the shared secret is replaced by a fresh uniform sample, completely independent of :
(Initialize is shared between both sides.) The FrogLang security game file is Games/KEM/INDCPA_MultiChal.game:
import '../../Primitives/KEM.primitive';
Game Real(KEM K) {
K.PublicKey pk;
K.SecretKey sk;
K.PublicKey Initialize() {
[K.PublicKey, K.SecretKey] k = K.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
[K.SharedSecret, K.Ciphertext] Challenge() {
[K.SharedSecret, K.Ciphertext] rsp = K.Encaps(pk);
K.SharedSecret ss = rsp[0];
K.Ciphertext ctxt = rsp[1];
return [ss, ctxt];
}
}
Game Random(KEM K) {
K.PublicKey pk;
K.SecretKey sk;
K.PublicKey Initialize() {
[K.PublicKey, K.SecretKey] k = K.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
[K.SharedSecret, K.Ciphertext] Challenge() {
[K.SharedSecret, K.Ciphertext] rsp = K.Encaps(pk);
K.SharedSecret ss <- K.SharedSecret;
K.Ciphertext ctxt = rsp[1];
return [ss, ctxt];
}
}
export as KEM_INDCPA_MultiChal;
Try it. From the examples/ directory, run proof_frog check Games/KEM/INDCPA_MultiChal.game, or open the file in the web editor and click Type Check.
PKE primitive
The public key encryption primitive is the goal of the construction: a triple of algorithms over a message space , ciphertext space , public key space , and secret key space :
The FrogLang primitive file is Primitives/PubKeyEnc.primitive:
Primitive PubKeyEnc(Set MessageSpace, Set CiphertextSpace, Set PKeySpace, Set SKeySpace) {
Set Message = MessageSpace;
Set Ciphertext = CiphertextSpace;
Set PublicKey = PKeySpace;
Set SecretKey = SKeySpace;
[PublicKey, SecretKey] KeyGen();
Ciphertext Enc(PublicKey pk, Message m);
deterministic Message? Dec(SecretKey sk, Ciphertext m);
}
Try it. From the examples/ directory, run proof_frog check Primitives/PubKeyEnc.primitive, or open the file in the web editor and click Type Check.
PKE CPA game
The PKE CPA game is, like SymEnc one-time secrecy here, a left/right indistinguishability game. The adversary obtains the public key from Initialize and may call the Challenge(mL, mR) oracle multiple times — multi-message CPA — and each call returns either an encryption of or an encryption of under the long-term public key:
The FrogLang security game file is Games/PubKeyEnc/INDCPA_MultiChal.game:
import '../../Primitives/PubKeyEnc.primitive';
Game Left(PubKeyEnc E) {
E.PublicKey pk;
E.SecretKey sk;
E.PublicKey Initialize() {
[E.PublicKey, E.SecretKey] k = E.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
E.Ciphertext Challenge(E.Message mL, E.Message mR) {
return E.Enc(pk, mL);
}
}
Game Right(PubKeyEnc E) {
E.PublicKey pk;
E.SecretKey sk;
E.PublicKey Initialize() {
[E.PublicKey, E.SecretKey] k = E.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
E.Ciphertext Challenge(E.Message mL, E.Message mR) {
return E.Enc(pk, mR);
}
}
export as INDCPA_MultiChal;
Try it. From the examples/ directory, run proof_frog check Games/PubKeyEnc/INDCPA_MultiChal.game, or open the file in the web editor and click Type Check.
3. The KEMDEM scheme
takes a KEM and a symmetric encryption scheme and produces a public key encryption scheme. Its key pair is ’s key pair, its messages come from , and its ciphertexts are pairs of a KEM ciphertext and a symmetric ciphertext. It requires so that the KEM’s shared secret can serve directly as a symmetric key. In the proof we will further impose equality by binding both sets to the same Set variable in the proof’s let: block; see §4 below.
The sets needed for the scheme definition are:
The algorithms comprising the scheme are:
KeyGen simply delegates to the KEM. Enc encapsulates under the public key to produce the shared secret and KEM ciphertext , then uses that shared secret directly as the DEM key to encrypt the message. Dec reverses the process. The FrogLang scheme file is Schemes/PubKeyEnc/HybridKEMDEM.scheme:
import '../../Primitives/KEM.primitive';
import '../../Primitives/NonNullableSymEnc.primitive';
import '../../Primitives/PubKeyEnc.primitive';
Scheme KEMDEM(KEM K, SymEnc E) extends PubKeyEnc {
requires K.SharedSecret subsets E.Key;
Set Message = E.Message;
Set Ciphertext = [K.Ciphertext, E.Ciphertext];
Set PublicKey = K.PublicKey;
Set SecretKey = K.SecretKey;
[PublicKey, SecretKey] KeyGen() {
return K.KeyGen();
}
Ciphertext Enc(PublicKey pk, Message m) {
[K.SharedSecret, K.Ciphertext] x = K.Encaps(pk);
E.Key k_sym = x[0];
K.Ciphertext c_kem = x[1];
E.Ciphertext c_sym = E.Enc(k_sym, m);
return [c_kem, c_sym];
}
deterministic Message? Dec(SecretKey sk, Ciphertext c) {
K.Ciphertext c_kem = c[0];
E.Ciphertext c_sym = c[1];
K.SharedSecret k_sym = K.Decaps(sk, c_kem);
return E.Dec(k_sym, c_sym);
}
}
Try it. From the examples/ directory, run proof_frog check Schemes/PubKeyEnc/HybridKEMDEM.scheme, or open the file in the web editor and click Type Check.
Note that Dec returns Message? (nullable) because KEMDEM extends PubKeyEnc, whose primitive declares Dec as returning an optional type. The underlying NonNullableSymEnc primitive’s Dec returns plain Message (non-nullable), but the KEMDEM scheme’s Dec signature must match the PubKeyEnc primitive it extends.
The requires K.SharedSecret subsets E.Key; clause is an essential part of the entire construction. It states that the KEM’s shared secret space must be a subset of the SymEnc’s key space. Without it, the assignment E.Key k_sym = x[0]; in Enc would be a type error — x[0] has type K.SharedSecret, not E.Key. The requires clause makes the subtype relationship explicit, so the type checker can accept the code.
The ciphertext type [K.Ciphertext, E.Ciphertext] is a tuple. In FrogLang, [T1, T2] is the two-component product type, and components are accessed by constant index: c[0] is the KEM ciphertext, c[1] is the symmetric ciphertext.
4. The proof structure
The games: block lists twelve game steps, producing eleven hops. Five of those hops are assumption hops (one per reduction); the other six are interchangeability hops verified by the engine. Conceptually the proof moves through six games, which we’ll call through :
- — the scheme encrypting the left message under the real KEM shared secret. This is
INDCPA_MultiChal(KD).Leftwith the scheme inlined. - — encrypting under a DEM key sampled uniformly from , while the KEM ciphertext is still produced by a real
Encapscall. - — encrypting under a DEM key produced by
E.KeyGen(). - — encrypting the right message under a DEM key produced by
E.KeyGen(). - — encrypting under a DEM key sampled uniformly from .
- — encrypting under the real KEM shared secret. This is
INDCPA_MultiChal(KD).Rightwith the scheme inlined.
Five reductions bridge these six games:
- (KEM CPA, ): reduces to KEM CPA security in the Real → Random direction. The shared secret starts as the genuine output of
Encapsand becomes a fresh uniform sample. - (KeyUniformity, ): reduces to key uniformity of in the Random → Real direction. The symmetric key distribution moves from uniform over to the output of
E.KeyGen(). - (OneTimeSecrecy, ): reduces to one-time secrecy of . The encrypted message switches from to under a
KeyGen-sampled key. - (KeyUniformity, ): structurally identical to but encrypts , and invokes key uniformity in the Real → Random direction.
- (KEM CPA, ): structurally identical to but encrypts , and invokes KEM CPA in the Random → Real direction.
Reductions and both reduce to KEM CPA security, but they invoke it in opposite directions: goes Real → Random, goes Random → Real. Similarly and invoke key uniformity in opposite directions. Both directions of each assumption are valid because indistinguishability is symmetric: if no adversary can distinguish the two sides in one direction, no adversary can distinguish them in the other.
This proof file does not write out explicit intermediate game definitions. Each conceptual appears in the games: list only as a composed form — either AssumptionGame compose Reduction or as an inlined side of INDCPA_MultiChal(KD). The engine verifies the interchangeability hops by canonicalizing the adjacent composed forms directly. You can add these intermediate game definitions if you find it helpful; ProofFrog will check they match their neighbouring games.
The proof’s let: block binds and to the same Set variable (KEMSharedSecretSpace), imposing the equality that the proof relies on whenever a uniform sample from one side must match a uniform sample from the other.
5. Reduction : Game 0 → Game 1
The first reduction adapts a adversary so that it can play against a challenger. It has no key pair of its own — all key material comes from the challenger. Its Challenge oracle delegates to challenger.Challenge() to obtain a pair, then uses as the symmetric key to encrypt the left message:
Note that has no Initialize method of its own: the compose machinery automatically forwards Initialize from the outer game to the inner KEM_INDCPA_MultiChal(K) challenger, so the challenger’s Initialize — which generates the KEM key pair and returns pk — becomes the Initialize of the composed game.
When the external challenger is , is the genuine shared secret produced by Encaps, and produces the same distribution as : the scheme encrypting under a real shared secret. When the challenger switches to , becomes a uniformly random sample from , independent of , and produces the same distribution as .
In FrogLang:
// Reduction for hop from Game 0 to Game 1
// - Reduction to CPA security of the KEM. The reduction uses the shared secret
// from the KEM CPA challenger, which is either real (= Game 0) or random (= Game 1).
Reduction R1(SymEnc E, KEM K, KEMDEM KD) compose KEM_INDCPA_MultiChal(K) against INDCPA_MultiChal(KD).Adversary {
KD.Ciphertext Challenge(KD.Message mL, KD.Message mR) {
[K.SharedSecret, K.Ciphertext] y = challenger.Challenge();
K.SharedSecret k_sym = y[0];
K.Ciphertext c_kem = y[1];
E.Ciphertext c_sym = E.Enc(k_sym, mL);
return [c_kem, c_sym];
}
}
Try it. lives inside HybridKEMDEM_INDCPA_MultiChal.proof along with the rest of the proof. From the examples/ directory, run proof_frog prove Proofs/PubKeyEnc/HybridKEMDEM_INDCPA_MultiChal.proof to verify the whole proof, or open the file in the web editor and click Run Proof. See §11 Verifying for the expected output.
Notice that has no state fields: all key material lives inside the challenger that composes against. This contrasts with through below, which have to manage their own KEM key pair because they compose against a different challenger.
6. Reduction : Game 1 → Game 2
After the assumption hop on KEM CPA, the proof is in a state where the symmetric key is a uniform sample from . But the next hop — one-time secrecy of — will want the symmetric key to be the output of E.KeyGen() instead (because that’s what the OTS game’s challenger does internally). So we first bridge the two distributions with a key-uniformity hop.
composes against a challenger. It has no KEM challenger available — the assumption was consumed in the previous hop — and so must generate the KEM key pair itself and answer Initialize queries from its own state. Its Challenge oracle encapsulates under its own public key to produce (discarding the shared secret), asks the KeyUniformity(E) challenger for a symmetric key, and uses that key to encrypt :
When the KeyUniformity(E) challenger is in mode, is a uniform sample from , which equals (imposed by the proof’s let: block). So matches , which in turn matches (the closing step of the previous pattern) under canonicalization. When the challenger switches to mode, is the output of E.KeyGen(), and matches .
In FrogLang:
// Reduction for hop from Game 1 to Game 2
// - Reduction to key uniformity of the symmetric encryption scheme. The reduction uses
// the symmetric key from the key uniformity challenger, which is either real (= Game 2)
// or random (= Game 1).
Reduction R2(SymEnc E, KEM K, KEMDEM KD) compose KeyUniformity(E) against INDCPA_MultiChal(KD).Adversary {
K.PublicKey pk;
K.SecretKey sk;
K.PublicKey Initialize() {
[K.PublicKey, K.SecretKey] k = K.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
KD.Ciphertext Challenge(KD.Message mL, KD.Message mR) {
[K.SharedSecret, K.Ciphertext] x = K.Encaps(pk);
K.SharedSecret k_sym = challenger.Challenge();
K.Ciphertext c_kem = x[1];
E.Ciphertext c_sym = E.Enc(k_sym, mL);
return [c_kem, c_sym];
}
}
This is a recurring pattern in multi-primitive proofs: a reduction that targets one assumption must internally simulate every other primitive that the surrounding game depends on. plays against a KeyUniformity(E) challenger but still needs a real KEM key pair — because the scheme under attack uses K.KeyGen() and K.Encaps() as part of its Enc algorithm — so generates that KEM key pair in its own Initialize.
7. Reduction : Game 2 → Game 3
With the DEM key now distributed as E.KeyGen(), the proof is ready to invoke one-time secrecy of . composes against an challenger. Like , it generates the KEM key pair in its own Initialize. Its Challenge oracle encapsulates to produce (discarding the shared secret), and asks the OTS(E) challenger to produce the DEM ciphertext on either or under a fresh key:
Crucially, discards the shared secret produced by Encaps and lets the OTS challenger sample its own fresh DEM key via E.KeyGen(). When the OTS challenger is in mode, the DEM ciphertext is an encryption of under a KeyGen-sampled key, matching . When the OTS challenger is in mode, the DEM ciphertext is an encryption of , matching . This is the central hop of the whole proof — the point where the “which message is encrypted” bit actually flips.
In FrogLang:
// Reduction for hop from Game 2 to Game 3
// - Reduction to one-time secrecy of the symmetric encryption scheme. The reduction uses the
// challenger to encrypt either mL (= Game 2) or mR (= Game 3).
Reduction R3(SymEnc E, KEM K, KEMDEM KD) compose INDOT(E) against INDCPA_MultiChal(KD).Adversary {
K.PublicKey pk;
K.SecretKey sk;
K.PublicKey Initialize() {
[K.PublicKey, K.SecretKey] k = K.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
KD.Ciphertext Challenge(KD.Message mL, KD.Message mR) {
[K.SharedSecret, K.Ciphertext] x = K.Encaps(pk);
K.Ciphertext c_kem = x[1];
E.Ciphertext c_sym = challenger.Eavesdrop(mL, mR);
return [c_kem, c_sym];
}
}
8. Reduction : Game 3 → Game 4
The rest of the proof “undoes” the earlier distributional bridges, but on the right-message side. is structurally identical to but encrypts instead of , and the direction of the assumption flips: now the proof uses key uniformity to move back from “key from E.KeyGen()” to “key sampled uniformly from ”.
When the KeyUniformity(E) challenger is in mode, matches (closing the OTS pattern). When the challenger switches to mode, matches (opening the closing KEM CPA pattern).
In FrogLang:
// Reduction for hop from Game 3 to Game 4
// - Reduction to key uniformity of the symmetric encryption scheme. The reduction uses
// the symmetric key from the key uniformity challenger, which is either real (= Game 3)
// or random (= Game 4).
Reduction R4(SymEnc E, KEM K, KEMDEM KD) compose KeyUniformity(E) against INDCPA_MultiChal(KD).Adversary {
K.PublicKey pk;
K.SecretKey sk;
K.PublicKey Initialize() {
[K.PublicKey, K.SecretKey] k = K.KeyGen();
pk = k[0];
sk = k[1];
return pk;
}
KD.Ciphertext Challenge(KD.Message mL, KD.Message mR) {
[K.SharedSecret, K.Ciphertext] x = K.Encaps(pk);
K.SharedSecret k_sym = challenger.Challenge();
K.Ciphertext c_kem = x[1];
E.Ciphertext c_sym = E.Enc(k_sym, mR);
return [c_kem, c_sym];
}
}
9. Reduction : Game 4 → Game 5
The fifth and final reduction is structurally identical to — it lets its composed challenger supply the key pair and the pair — but it encrypts instead of :
When the KEM challenger is in mode, is a random shared secret and matches . When the challenger switches to , is the genuine shared secret and matches , which is in turn interchangeable with INDCPA_MultiChal(KD).Right.
This is the Random → Real direction of the KEM CPA assumption: the proof has finished using the random-key intermediate world and is restoring the real KEM shared secret, but on the right-message side. Both directions of the assumption are valid because indistinguishability is symmetric.
In FrogLang:
// Reduction for hop from Game 4 to Game 5
// - Reduction to CPA security of the KEM. The reduction uses the shared secret
// from the KEM CPA challenger, which is either real (= Game 5) or random (= Game 4).
Reduction R5(SymEnc E, KEM K, KEMDEM KD) compose KEM_INDCPA_MultiChal(K) against INDCPA_MultiChal(KD).Adversary {
KD.Ciphertext Challenge(KD.Message mL, KD.Message mR) {
[K.SharedSecret, K.Ciphertext] y = challenger.Challenge();
K.SharedSecret k_sym = y[0];
K.Ciphertext c_kem = y[1];
E.Ciphertext c_sym = E.Enc(k_sym, mR);
return [c_kem, c_sym];
}
}
10. The full games block
Putting all twelve game steps together, the games: section of HybridKEMDEM_INDCPA_MultiChal.proof reads:
games:
// Game 0
INDCPA_MultiChal(KD).Left against INDCPA_MultiChal(KD).Adversary;
KEM_INDCPA_MultiChal(K).Real compose R1(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
// Game 1
KEM_INDCPA_MultiChal(K).Random compose R1(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
KeyUniformity(E).Random compose R2(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
// Game 2
KeyUniformity(E).Real compose R2(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
INDOT(E).Left compose R3(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
// Game 3
INDOT(E).Right compose R3(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
KeyUniformity(E).Real compose R4(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
// Game 4
KeyUniformity(E).Random compose R4(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
KEM_INDCPA_MultiChal(K).Random compose R5(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
// Game 5
KEM_INDCPA_MultiChal(K).Real compose R5(E, K, KD) against INDCPA_MultiChal(KD).Adversary;
INDCPA_MultiChal(KD).Right against INDCPA_MultiChal(KD).Adversary;
The twelve entries produce eleven hops. Five are assumption hops — one per reduction — at positions 2 → 3 (, CPAKEM Real → Random), 4 → 5 (, KeyUniformity Random → Real), 6 → 7 (, OneTimeSecrecy Left → Right), 8 → 9 (, KeyUniformity Real → Random), and 10 → 11 (, CPAKEM Random → Real). The other six transitions (1 → 2, 3 → 4, 5 → 6, 7 → 8, 9 → 10, 11 → 12) are interchangeability hops verified by the engine.
Notice how each reduction occupies two consecutive entries in the games list — one for each side of the composed assumption game — and interchangeability hops connect adjacent reductions at their conceptual boundary . The interchangeability hop at positions 3 → 4, for example, is the engine verifying that KEM_INDCPA_MultiChal(K).Random compose R1 and KeyUniformity(E).Random compose R2 both canonicalize to , even though they get there via very different-looking source programs.
The full let:, assume:, and theorem: blocks, together with the five reductions above, complete the proof file, which you can find at Proofs/PubKeyEnc/HybridKEMDEM_INDCPA_MultiChal.proof.
11. Verifying
Activate your Python virtual environment first if it is not already active in this terminal: source .venv/bin/activate on macOS/Linux (bash/zsh), source .venv/bin/activate.fish on fish, or .venv\Scripts\Activate.ps1 on Windows PowerShell. See Installation.
From the examples/ directory:
proof_frog prove Proofs/PubKeyEnc/HybridKEMDEM_INDCPA_MultiChal.proof
Expected output:
Proof Succeeded!
The full step-by-step output shows 11 hops over 12 game steps: 6 interchangeability hops (verified by code canonicalization) and 5 assumption hops (one for each of through ).
In the web editor, open Proofs/PubKeyEnc/HybridKEMDEM_INDCPA_MultiChal.proof and click Run Proof. The output panel turns green with the same step-by-step report.
12. Lessons learned
-
Multi-primitive composition. The proof reasons about three distinct primitives simultaneously — a KEM, a SymEnc, and a PKE — each with its own security game and its own type namespace. Reductions must carefully distinguish which primitive’s methods and sets they are invoking. The
requiresclause on the scheme is what makes the types align across primitive boundaries. -
Reductions in opposite directions. and both reduce to the same underlying assumption (), but invokes it in the Real → Random direction and invokes it in the Random → Real direction. Similarly, and invoke key uniformity in opposite directions. Both directions are valid because indistinguishability is symmetric. This “go-and-come-back” pattern often comes up in security proofs: here we use the KEM assumption to move into a world with a random key, do the message-switching argument, then use the same KEM assumption to move back out. Neither direction is privileged; the same security game serves both roles.
-
Bridging distributions with a key-uniformity assumption. The
KEMDEMscheme never callsE.KeyGen()— it uses the KEM’s shared secret directly as the symmetric key — while the one-time secrecy game for does callE.KeyGen(). These two key distributions aren’t always identical, so the proof needs a key-uniformity assumption (that “E.KeyGen()is indistinguishable from uniform sampling overE.Key”) to bridge them. The bridge is invoked symmetrically — once on each side of the OTS hop — which is why there are five reductions instead of three. -
Generic construction parameter handling. The scheme is parameterized by , and the proof is parameterized by the same values in its
let:block. Every reduction carries(E, K, KD)as parameters. This is how ProofFrog proves theorems about generic constructions rather than concrete instantiations: the proof holds for any choice of and satisfying the stated assumptions and the constraint. -
Proofs without explicit intermediate games. Unlike proofs that write out each as a standalone
Gamedefinition, this proof keeps its intermediate games implicit — every entry in thegames:list is either a side ofINDCPA_MultiChal(KD)or acomposeexpression. The engine verifies the interchangeability hops by canonicalizing adjacent composed forms directly. This keeps the proof file shorter, at the cost of making each conceptual game only visible in the reader’s head (or in the line comments that label them). You can add them if you want, or you can use the web editor’s game hop detail view to see what the intermediate games are.
13. Next steps
This concludes the tutorial and worked examples section of the ProofFrog manual. From here you have a few options:
- Build your own proof! You now have the tools to start modelling your own ideas in ProofFrog.
- Explore the examples catalogue to see other examples of cryptographic schemes and proofs modelled in ProofFrog.
- Read the language reference to dive deeper into FrogLang.
- Since ProofFrog is a new tool and is targeted at introduction proofs, it’s important to understand the limitations. There’s also information about troubleshooting when you get stuck. You can ask questions on the ProofFrog discussion boards on GitHub.
- See how proofs in ProofFrog compare to proofs in more advanced formal verification tools like EasyCrypt via the Proof Ladders project, which includes an example showing CPA security of KEM-DEM in both ProofFrog and EasyCrypt. That version of the ProofFrog proof uses a slightly different formulation of the one-time secrecy game — one phrased with uniform sampling
E.Key k <- E.Key;instead ofE.KeyGen()— which lets the proof sidestep the key-uniformity assumption entirely and collapses the five reductions on this page down to three. - Learn more about the technical background of ProofFrog in the for researchers section.
- Get in touch! We’d love to hear about your use of ProofFrog. Contributions of examples or improvements to the ProofFrog software are also welcome. The best way to reach us is on the ProofFrog GitHub repositories.