Some KEMs and Some Proofs

In this post, I'd like to provide a technical introduction to key encapsulation mechanisms (KEMs), with a focus on proving the security of various constructions.

A key encapsulation mechanism is like a public key encryption scheme, tailored to the specific use case of sending a key to the other party. This can be used to establish shared key between two parties. One of them runs the KEM, getting a shared key and a ciphertext, and then sends that ciphertext to the other person. That person can unwrap the ciphertext to recover the shared key. From that point on, the parties can use that shared key to communicate privately.

In this way, you can think of KEMs as a generalization of key exchanges, which don't have to be symmetric. In fact, you can use a key exchange to construct a KEM, as we'll see later.

In this post, we'll go over:

There are other important topics around KEMs that I'd also like to touch upon, but this sample is already quite a hefty serving if you include all of the proofs; those will have to wait for a future post.

Background

For a high level overview of KEMs, I'd recommend Neil Madden's post on the subject.

This post is more focused on the technical side of things, and on provable security. I'll be using state separable proofs pervasively throughout this post, so I'd recommend reading my post on the subject if you'd like to get up to speed.

Defining KEMs

To begin, let's formally define what a KEM is.

A key encapsulation mechanism (KEM), is a scheme similar to public key encryption. This scheme consists of three algorithms:

Gen:()(SK,PK)Encap:PK(K,C)Decap:(SK,C)K\begin{aligned} \text{Gen} &: () \to (\bold{SK}, \bold{PK})\cr \text{Encap} &: \bold{PK} \to (\bold{K}, \bold{C})\cr \text{Decap} &: (\bold{SK}, \bold{C}) \to \bold{K} \end{aligned}

The first algorithm, Gen\text{Gen}, creates a new key pair, consisting of a private key, and its corresponding public key.

The second algorithm, Encap\text{Encap}, takes in a public key, for the recipient, and returns a symmetric key and an encapsulation (also called a ciphertext).

With Decap\text{Decap}, a recipient can use their private key to extract a symmetric key from the ciphertext.

This scheme also needs to satisfy some notion of correctness. Intuitively, decapsulating should return the same key that was encapsulated.

More formally, the following procedure must always succeed:

(sk,pk)Gen()(kS,c)Encap(pk)kRDecap(sk,c)kS=?kR\begin{aligned} &(\text{sk}, \text{pk}) \gets \text{Gen}()\cr &(k_S, c) \gets \text{Encap}(\text{pk})\cr &k_R \gets \text{Decap}(\text{sk}, c)\cr &k_S \stackrel{?}{=} k_R \end{aligned}

No matter what key pair comes out of Gen\text{Gen}, encapsulating and then decapsulating must return the same key. If this didn't hold, your KEM wouldn't be very useful for establishing a shared key between two parties, or for public key encryption, which are two of the main use cases for KEMs.

IND\text{IND} Security

Naturally, having a correct scheme is easy if you don't care about security. The usual notion of security is similar to that of public key encryption. The basic idea is that you shouldn't be able to tell whether or not a specific key is hidden inside of the ciphertext. In particular, you shouldn't be able to distinguish between receiving a random key, and receiving the key inside of a ciphertext.

We formalize this as a pair of games, in the state separable style:

INDb (sk,pk)RGen()GetPk(): return pkChallenge(): (k0,c)Encap(pk) k1RK return (kb,c)\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND}_b$ }\cr \cr &(\text{sk}, \text{pk}) \xleftarrow{R} \text{Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } \text{pk} \cr \cr &\underline{\mathtt{Challenge}():}\cr &\ (k_0, c) \gets \text{Encap}(\text{pk}) \cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \texttt{return } (k_b, c) \cr \end{aligned} }

In one version of the game, we get the key inside the encapsulation, and in the other version of the game, we get a completely random key. An adversary should not be able to distinguish between the two games.

This means that the adversary can't learn any information about the key from the ciphertext.

[!note] Note: In this variant, you can query the challenge multiple times. This is equivalent to only being able to query once, although the security gets worse as a linear function of the number of queries you do. For simplicity, I'll stick with the multi-query variants for the rest of this post.

This variant of security can also be referred to as IND-CPA\text{IND-CPA}. This is because the adversary is able to create encapsulations themselves, because they know the public key. This kind of query is a chosen plaintext, hence the chosen plaintext attack (CPA) in the name.

With symmetric encryption, on the other hand, you need to have a secret key to even encrypt data, so the CPA\text{CPA} capability is a meaningful distinction.

IND-CCA\text{IND-CCA} Security

An even stronger variant of security also allows the adversary to make decapsulation queries on ciphertexts of their choice. We call these chosen ciphertext attacks (CCA). This models situations where we might know the keys inside certain ciphertexts. While in practice these leakages could be quite limited, having a more expansive model of security covers many more situations, and we can construct schemes which satisfy this general model.

We model this with a pair of games, like before:

IND-CCAb seen(sk,pk)RGen()GetPk(): return pkChallenge(): (k0,c)Encap(pk) k1RK seenseen{c} return (kb,c)Decap(c): assert cseen return Decap(sk,c)\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND-CCA}_b$ }\cr \cr &\text{seen} \gets \emptyset\cr &(\text{sk}, \text{pk}) \xleftarrow{R} \text{Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } \text{pk} \cr \cr &\underline{\mathtt{Challenge}():}\cr &\ (k_0, c) \gets \text{Encap}(\text{pk}) \cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \text{seen} \gets \text{seen} \cup \{c\}\cr &\ \texttt{return } (k_b, c) \cr \cr &\underline{\mathtt{Decap}(c):}\cr &\ \texttt{assert } c \notin \text{seen}\cr &\ \texttt{return } \text{Decap}(\text{sk}, c) \cr \end{aligned} }

This is the same as the previous IND\text{IND} game, except that we now have the ability to make decapsulation queries. In order to make the game not trivially easy to win, we keep track of which challenge ciphertexts have been produced, and refuse decapsulation queries for that set. Otherwise, you could ask for the decapsulation for a ciphertext you saw earlier, and compare the result with the key you received before.

Equivalence with Other Definitions

[!note] Note: You can skip this section. It's mainly concerned with resolving a small discrepancy in definitions between this post and the broader literature.

In the games we've seen so far, the adversary only sees one of the keys. We can also model a situation where the adversary sees both of the keys:

IND-Both-CCAb seen(sk,pk)RGen()GetPk(): return pkChallenge(): (kb,c)Encap(pk) k_(1b)RK seenseen{c} return (k0,k1,c)Decap(c): assert cseen return Decap(sk,c)\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND-Both-CCA}_b$ }\cr \cr &\text{seen} \gets \emptyset\cr &(\text{sk}, \text{pk}) \xleftarrow{R} \text{Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } \text{pk} \cr \cr &\underline{\mathtt{Challenge}():}\cr &\ (k_b, c) \gets \text{Encap}(\text{pk}) \cr &\ k\_{(1 - b)} \xleftarrow{R} \bold{K} \cr &\ \text{seen} \gets \text{seen} \cup \{c\}\cr &\ \texttt{return } (k_0, k_1, c) \cr \cr &\underline{\mathtt{Decap}(c):}\cr &\ \texttt{assert } c \notin \text{seen}\cr &\ \texttt{return } \text{Decap}(\text{sk}, c) \cr \end{aligned} }

Instead of giving them one of the keys like before, we give them both, but we swap their order. The adversary has to tell which key is which.

One natural question is whether or not this new game is equivalent to our previous definitions.

It is.

As a bit of a warmup, let's prove this equivalence.

IND-Both-CCA2IND-CCA\text{IND-Both-CCA} \leq 2 \cdot \text{IND-CCA}

The idea of this reduction is that we can replace k0k_0 from the first encapsulation with a random key, because of IND-CCA\text{IND-CCA} security. This then allows us to swap k0k_0 with k1k_1, and then walk our way backwards, giving us a bound for ϵ(IND-Both-CCAb)\epsilon(\text{IND-Both-CCA}_b).

We start by extracting out the encapsulation in IND-Both-CCAb\text{IND-Both-CCA}_b, using IND-CCA\text{IND-CCA}:

IND-Both-CCAb=Wb Challenge(): (kb,c)super.Challenge() k_(1b)RK seenseen{c} return (k0,k1,c)1{GetPk,Decap}IND-CCA0\text{IND-Both-CCA}_b = \begin{aligned} &\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{W}_b$ }\cr \cr &\underline{\mathtt{Challenge}():}\cr &\ (k_b, c) \gets \texttt{super.Challenge}() \cr &\ k\_{(1 - b)} \xleftarrow{R} \bold{K} \cr &\ \text{seen} \gets \text{seen} \cup \{c\}\cr &\ \texttt{return } (k_0, k_1, c) \cr \end{aligned} }\cr &\otimes 1\{\texttt{GetPk}, \texttt{Decap}\} \end{aligned} \circ \text{IND-CCA}_0

Using this, we can already march some of the way forward, giving us:

IND-Both-CCAb=WbIND-CCA0ϵ1WbIND-CCA1\text{IND-Both-CCA}_b = W_b \circ \text{IND-CCA}_0 \stackrel{\epsilon_1}{\approx} W_b \circ \text{IND-CCA}_1

Now, one thing we can note is that:

W0IND-CCA1=W1IND-CCA1W_0 \circ \text{IND-CCA}_1 = W_1 \circ \text{IND-CCA}_1

To notice this, first expand out the game:

WbIND-CCA1=seen(sk,pk)RGen()GetPk(): return pkChallenge(): (,c)Encap(pk) kbRK k_(1b)RK seenseen{c} return (k0,k1,c)Decap(c): assert cseen return Decap(sk,c)W_b \circ \text{IND-CCA}_1 = \begin{aligned} \boxed{ \begin{aligned} &\text{seen} \gets \emptyset\cr &(\text{sk}, \text{pk}) \xleftarrow{R} \text{Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } \text{pk} \cr \cr &\underline{\mathtt{Challenge}():}\cr &\ (\bullet, c) \gets \text{Encap}(\text{pk}) \cr &\ k_b \xleftarrow{R} \bold{K}\cr &\ k\_{(1 - b)} \xleftarrow{R} \bold{K}\cr &\ \text{seen} \gets \text{seen} \cup \{c\}\cr &\ \texttt{return } (k_0, k_1, c) \cr \cr &\underline{\mathtt{Decap}(c):}\cr &\ \texttt{assert } c \notin \text{seen}\cr &\ \texttt{return } \text{Decap}(\text{sk}, c) \cr \end{aligned} } \end{aligned}

Because both k0k_0 and k1k_1 get sampled at random, it doesn't matter which order they have.

We can now align these togethers to get:

IND-Both-CCA0=W0IND-CCA0ϵ1W0IND-CCA1=W1IND-CCA1ϵ1W1IND-CCA0=IND-Both-CCA1\begin{aligned} \text{IND-Both-CCA}_0 &= W_0 \circ \text{IND-CCA}_0\cr &\stackrel{\epsilon_1}{\approx} W_0 \circ \text{IND-CCA}_1\cr &= W_1 \circ \text{IND-CCA}_1\cr &\stackrel{\epsilon_1}{\approx} W_1 \circ \text{IND-CCA}_0\cr &= \text{IND-Both-CCA}_1\cr \cr \end{aligned}

which gives us our result.

\square

Now, for the other direction.

IND-Both-CCAIND-CCA\text{IND-Both-CCA} \leq \text{IND-CCA}

The idea of this proof is that we can emulate IND-CCA\text{IND-CCA} using IND-Both-CCA\text{IND-Both-CCA} by dropping one of the keys. The question is: which key do we drop? Because we can't distinguish between k0k_0 and k1k_1, it doesn't matter which one we choose, essentially.

First, let's define the following wrapper package:

Wb:=Challenge(): (k0,k1,c)super.Challenge() return (kb,c)1{GetPk,Decap}W_b := \boxed{ \begin{aligned} &\underline{\mathtt{Challenge}():}\cr &\ (k_0, k_1, c) \gets \texttt{super.Challenge}() \cr &\ \texttt{return } (k_b, c) \cr \end{aligned} } \otimes 1\{\texttt{GetPk}, \texttt{Decap}\}

This perfectly emulates IND-CCAb\text{IND-CCA}_b when composed with IND-Both-CCA0\text{IND-Both-CCA}_0:

WbIND-Both-CCA0=IND-CCAbW_b \circ \text{IND-Both-CCA}_0 = \text{IND-CCA}_b

With IND-Both-CCA1\text{IND-Both-CCA}_1 on the other hand, the bit is flipped:

WbIND-Both-CCA1=IND-CCA_(1b)W_b \circ \text{IND-Both-CCA}_1 = \text{IND-CCA}\_{(1 - b)}

This is enough for us to write:

IND-CCA0=W0IND-Both-CCA0ϵ1W0IND-Both-CCA1=IND-CCA1\begin{aligned} \text{IND-CCA}_0 &= W_0 \circ \text{IND-Both-CCA}_0\cr &\stackrel{\epsilon_1}{\approx} W_0 \circ \text{IND-Both-CCA}_1\cr &= \text{IND-CCA}_1\cr \end{aligned}

Which gives us the reduction we sought.

\square

Real-or-Random is Essential

These definitions turned out to be equivalent, but both fit into a kind of "real-or-random" paradigm: one of the keys is randomly selected, while the other is related to the ciphertext. This is a general kind of paradigm, which can be used to define the security of other schemes as well. It's useful to stay within this paradigm, since phrasing all of our security definitions in terms of real-or-random makes it easier to do reductions.

One alternative paradigm, that gets used for public key encryption, is "left-or-right". With this approach, instead of having a random output and a real output, you instead have two outputs, and you try and distinguish between them.

For the KEM case, this would give the following game:

IND-LRb (sk,pk)RGen()GetPk(): return pkChallenge(): (k0,c0)Encap(pk) (k1,c1)Encap(pk) return (k0,k1,cb)\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND-LR}_b$ }\cr \cr &(\text{sk}, \text{pk}) \xleftarrow{R} \text{Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } \text{pk} \cr \cr &\underline{\mathtt{Challenge}():}\cr &\ (k_0, c_0) \gets \text{Encap}(\text{pk}) \cr &\ (k_1, c_1) \gets \text{Encap}(\text{pk}) \cr &\ \texttt{return } (k_0, k_1, c_b) \cr \end{aligned} }

Unfortunately, this definition fails to capture a useful notion. The issue is that we have no guarantee that the key is sufficiently random, which makes it not usable for encryption later.

As an example, consider the following KEM:

Encap(pk): cRC return (0,c)Decap(sk,c): return 0\begin{aligned} &\underline{\text{Encap}(\text{pk}):}\cr &\ c \xleftarrow{R} \bold{C} \cr &\ \texttt{return } (0, c)\cr \cr &\underline{\text{Decap}(\text{sk}, c):}\cr &\ \texttt{return } 0 \end{aligned}

This KEM always returns the same key for encapsulation and decapsulation, which makes it completely useless to construct a secure public key encryption scheme. It does, however, satisfy the IND-LR\text{IND-LR} definition of security above. This is because (0,0,cb)(0, 0, c_b) contains no information about bb.

Constructing KEMs

We've now seen a bit of how KEMs work in theory, but we've yet to actually see an example of making one. In this section, we go over a few potential constructions of KEMs, and show that they're secure, under appropriate assumptions.

From RSA

Let's start with RSA. There are thousands of good explanations of RSA, so I'll settle for a bad, but short, explanation.

In RSA, your public key consists of a modulus NN, and a number ee. Your secret key consists of a factorization p,qp, q such that N=pqN = p \cdot q, along with a secret exponent dd, such that

de1modφ(N)d \cdot e \equiv 1 \mod \varphi(N)

These parameters give us a trapdoor permutation for the set Z/(N)\mathbb{Z}/(N):

F(x):=xemodNF1(y):=ydmodN\begin{aligned} F(x) &:= x^e \mod N\cr F^{-1}(y) &:= y^d \mod N\cr \end{aligned}

The function FF is a permutation, with F1F^{-1} its inverse. Computing the inverse should be hard without knowing the secret exponent dd, or being able to factor NN.

We make this security notion precise with the following game:

RSAb (d,(N,e))RGen()xRZ/(N)yxemodNInstance(): return (N,e,y)Guess(x^): return b=0x^=x\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{RSA}_b$ }\cr \cr &(d, (N, e)) \xleftarrow{R} \text{Gen}()\cr &x \xleftarrow{R} \mathbb{Z}/(N)\cr &y \gets x^e \mod N\cr \cr &\underline{\mathtt{Instance}():}\cr &\ \texttt{return } (N, e, y) \cr \cr &\underline{\mathtt{Guess}(\hat{x}):}\cr &\ \texttt{return } b = 0 \land \hat{x} = x\cr \end{aligned} }

The adversary makes guesses for the pre-image of the permutation, and it should be difficult for them to succeed in their guess. If the adversary has a good strategy for guessing, then they'll be able to figure out what bb is.

We can use RSA to create a KEM. The pre-image xx will be our key, and then y:=xemodNy := x^e \mod N is our ciphertext, hiding the key. The recipient can use their secret to unwrap xx from this ciphertext. Now, because Z/(N)\mathbb{Z}/(N) may not be a very useful key by itself, we also make use of a hash function H:Z/(N)KH : \mathbb{Z}/(N) \to \bold{K}, so that we can derive a more useful key from xx.

For example, if we wanted to use our key for encryption with AES, or ChaCha20, we'd want to use a hash function from integers to 256 bit keys.

More formally, we define the following KEM:

Gen(): (d,(N,e))RSA.Gen() return ((N,d),(N,e))Encap((N,e)): xRZ/(N) yxemodN return (H(x),y)Decap((N,d),y): xydmodN return H(x)\begin{aligned} &\underline{\text{Gen}():}\cr &\ (d, (N, e)) \gets \text{RSA}.\text{Gen}()\cr &\ \texttt{return } ((N, d), (N, e)) \cr \cr &\underline{\text{Encap}((N, e)):}\cr &\ x \xleftarrow{R} \mathbb{Z}/(N)\cr &\ y \gets x^e \mod N\cr &\ \texttt{return } (H(x), y)\cr \cr &\underline{\text{Decap}((N, d), y):}\cr &\ x \gets y^d \mod N\cr &\ \texttt{return } H(x)\cr \end{aligned}

Because the RSA function is a permutation, correctness is satisfied.

As for security, this isn't a trivial matter. First, we'll model our hash function HH as a random oracle, where the outputs will be generated perfectly at random, on demand. Second, rather than considering normal IND-CCA\text{IND-CCA} security, instead we'll consider IND-CCA-1\text{IND-CCA-1} security, where the adversary can only make a single challenge query. Making QQ challenge queries instead of just a single one only decreases security by a factor of QQ, as can be shown via a hybrid argument. (My post on state-separable proofs contains a proof of this in the general case).

IND-CCA-12RSA\text{IND-CCA-1} \leq 2 \cdot \text{RSA}

First, let's write down the IND-CCA-1\text{IND-CCA-1} game in this context.

IND-CCA-1b seen((N,d),(N,e))RGen()GetPk(): return (N,e)(1) Challenge(): xRZ/(N) yxemodN k0H(x) k1RK seenseen{y} return (kb,y)Decap(y): assert yseen return H(ydmodN)h[]H(x): if xh h[x]RK return h[x]\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND-CCA-1}_b$ }\cr \cr &\text{seen} \gets \emptyset\cr &((N, d), (N, e)) \xleftarrow{R} \text{Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } (N, e) \cr \cr &\underline{(1)\ \mathtt{Challenge}():}\cr &\ x \xleftarrow{R} \mathbb{Z}/(N)\cr &\ y \gets x^e \mod N\cr &\ k_0 \gets \texttt{H}(x)\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \text{seen} \gets \text{seen} \cup \{y\}\cr &\ \texttt{return } (k_b, y) \cr \cr &\underline{\mathtt{Decap}(y):}\cr &\ \texttt{assert } y \notin \text{seen}\cr &\ \texttt{return } \mathtt{H}(y^d \mod N) \cr \cr &h[\cdot] \gets \bot\cr \cr &\underline{\mathtt{H}(x)}:\cr &\ \texttt{if } x \notin h\cr &\quad\ h[x] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[x]\cr \end{aligned} }

The (1)(1) notation means that Challenge\texttt{Challenge} can only be queried a single time. We've also replaced the hash function with a lazily initialized table of random values. This is why we're doing our proof in the "random oracle model", since the hash function is being modelled this way.

Next, we follow the same strategy as in Theorem 12.2 of Boneh and Shoup.

Rather than having our table hh be used for the pre-images xx, instead we'll setup a table hh' for the images yy. This will allow us to minimize our use of the secret key dd in decapsulation queries, which makes it easier to extract out the RSA functionality for our package.

Since we only have a single challenge query, we can generate the instance before Challenge\texttt{Challenge} is called. We'll also be able to do a lot of tricks with the decapsulation queries, but it's easier to explain those after seeing the new game:

Γb0 ((N,d),(N,e))RGen() x^RZ/(N) y^x^emodN k0RKGetPk(): return (N,e)(1) Challenge(): k1RK return (kb,y^)h[]Decap(y): assert yy^ if yh h[y]RK return h[y]H(x): if x=x^ return k0 yxemodN if yh h[y]RK return h[y]\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^0_b$ }\cr \cr &((N, d), (N, e)) \xleftarrow{R} \text{Gen}()\cr &\ \hat{x} \xleftarrow{R} \mathbb{Z}/(N)\cr &\ \hat{y} \gets \hat{x}^e \mod N\cr &\ k_0 \xleftarrow{R} \bold{K}\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } (N, e) \cr \cr &\underline{(1)\ \mathtt{Challenge}():}\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \texttt{return } (k_b, \hat{y}) \cr \cr &h[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}(y):}\cr &\ \texttt{assert } y \neq \hat{y}\cr &\ \texttt{if } y \notin h\cr &\quad\ h[y] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[y] \cr \cr &\underline{\mathtt{H}(x)}:\cr &\ \texttt{if } x = \hat{x}\cr &\quad\ \texttt{return } k_0\cr &\ y \gets x^e \mod N\cr &\ \texttt{if } y \notin h\cr &\quad\ h[y] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[y]\cr \end{aligned} }

The big trick we've pulled in this game is in our decapsulation function. By having a hash table for the images yy, we avoid the need to make use of the secret key dd when decapsulating. Whereas we'd normally compute xydmodNx \gets y^d \mod N, and then query H(x)\texttt{H}(x), because our hash function then computes xemodNx^e \mod N to index into our table, we can avoid all of that, and use the table ourselves.

Naturally, we have IND-CCA-1b=Γb0\text{IND-CCA-1}_b = \Gamma^0_b.

From this point on, it's relatively straight sailing.

We start by extracting out the RSA functionality:

Γb0=Γb1 (N,e,y^)Instance() k0RKGetPk(): return (N,e)(1) Challenge(): k1RK return (kb,y^)h[]Decap(y): assert yy^ if yh h[y]RK return h[y]H(x): if Guess(x) return k0 yxemodN if yh h[y]RK return h[y]RSA0\Gamma^0_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^1_b$ }\cr \cr &(N, e, \hat{y}) \gets \texttt{Instance}()\cr &\ k_0 \xleftarrow{R} \bold{K}\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } (N, e) \cr \cr &\underline{(1)\ \mathtt{Challenge}():}\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \texttt{return } (k_b, \hat{y}) \cr \cr &h[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}(y):}\cr &\ \texttt{assert } y \neq \hat{y}\cr &\ \texttt{if } y \notin h\cr &\quad\ h[y] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[y] \cr \cr &\underline{\mathtt{H}(x)}:\cr &\ \texttt{if } \texttt{Guess}(x)\cr &\quad\ \texttt{return } k_0\cr &\ y \gets x^e \mod N\cr &\ \texttt{if } y \notin h\cr &\quad\ h[y] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[y]\cr \end{aligned} } \circ \text{RSA}_0

Now we use the RSA0\text{RSA}_0 game to determine if we need to return k0k_0 in our hash function. Next, note that if we switch to using RSA1\text{RSA}_1, we get the following game:

Γb1RSA1=Γb2 (N,e,y^)Instance()GetPk(): return (N,e)(1) Challenge(): k0RK k1RK return (kb,y^)h[]Decap(y): assert yy^ if yh h[y]RK return h[y]H(x): yxemodN if yh h[y]RK return h[y]\Gamma^1_b \circ \text{RSA}_1 = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^2_b$ }\cr \cr &(N, e, \hat{y}) \gets \texttt{Instance}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } (N, e) \cr \cr &\underline{(1)\ \mathtt{Challenge}():}\cr &\ k_0 \xleftarrow{R} \bold{K}\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \texttt{return } (k_b, \hat{y}) \cr \cr &h[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}(y):}\cr &\ \texttt{assert } y \neq \hat{y}\cr &\ \texttt{if } y \notin h\cr &\quad\ h[y] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[y] \cr \cr &\underline{\mathtt{H}(x)}:\cr &\ y \gets x^e \mod N\cr &\ \texttt{if } y \notin h\cr &\quad\ h[y] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[y]\cr \end{aligned} }

And in this game, the difference between k0k_0 and k1k_1 is just a matter of naming. Because of this, Γ02=Γ12\Gamma^2_0 = \Gamma^2_1.

We can now tie everything together, to get:

IND-CCA-10=Γ00=Γ01RSA0ϵ1Γ01RSA1=Γ02=Γ12=Γ11RSA1ϵ1Γ11RSA0=Γ10=IND-CCA-11\begin{aligned} \text{IND-CCA-1}_0 &= \Gamma^0_0\cr &= \Gamma^1_0 \circ \text{RSA}_0\cr &\stackrel{\epsilon_1}{\approx} \Gamma^1_0 \circ \text{RSA}_1\cr &= \Gamma^2_0\cr &= \Gamma^2_1\cr &= \Gamma^1_1 \circ \text{RSA}_1\cr &\stackrel{\epsilon_1}{\approx} \Gamma^1_1 \circ \text{RSA}_0\cr &= \Gamma^0_1\cr &= \text{IND-CCA-1}_1\cr \end{aligned}

And this yields our result.

\square

From Groups

Another way to construct a KEM is with a cryptographic group, like a suitable elliptic curve.

This is a group G\mathbb{G} of prime order qq, generated by GG, and with an associated field of scalars Fq\mathbb{F}_q.

A key pair for the KEM will be a scalar aa, for the private key, and a point A:=aG{A := a \cdot G}, for the public key. In order to encapsulate a key, we generate a random scalar bb, set B:=bG{B := b \cdot G} as our encapsulation, and H(bA)H(b \cdot A) as the key. The receiver can set H(aB)H(a \cdot B) as their key, arriving at the same result because of commutativity:

aB=abG=baG=bAa \cdot B = a \cdot b \cdot G = b \cdot a \cdot G = b \cdot A

More formally, given a cryptographic group G\mathbb{G}, and a hash function H:G2KH : \mathbb{G}^2 \to \bold{K}, we define the KEM as follows:

Gen(): aRFq AaG return (a,A)Encap(A): bRFq BbG CbA return (H(B,C),B)Decap(a,B): CaB return H(B,C)\begin{aligned} &\underline{\text{Gen}():}\cr &\ a \xleftarrow{R} \mathbb{F}_q\cr &\ A \gets a \cdot G\cr &\ \texttt{return } (a, A) \cr \cr &\underline{\text{Encap}(A):}\cr &\ b \xleftarrow{R} \mathbb{F}_q\cr &\ B \gets b \cdot G\cr &\ C \gets b \cdot A\cr &\ \texttt{return } (H(B, C), B)\cr \cr &\underline{\text{Decap}(a, B):}\cr &\ C \gets a \cdot B\cr &\ \texttt{return } H(B, C)\cr \end{aligned}

One slight difference from the brief presentation above is that we hash both BB and CC, rather than just CC. This makes the security proof easier.

To characterize the security of the KEM, we need to have some kind notion of security for the group itself. The notion we'll be relying on (at least, in our proof), is the "interactive computational Diffie-Hellman" problem. This states, essentially, that given two points A=aGA = a \cdot G and B=bGB = b \cdot G, it should be hard to find C=abGC = ab \cdot G, even given access to an oracle which tells you whether or not two points B^\hat{B} and C^\hat{C} satisfy the relation aB^=C^a \cdot \hat{B} = \hat{C}.

We capture this notion through a guessing game:

ICDHb a,bRGen()AaGBbGCaBInstance(): return (A,B)Query(B^,C^): return aB^=C^Guess(C^): return b=0C^=C\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{ICDH}_b$ }\cr \cr &a, b \xleftarrow{R} \text{Gen}()\cr &A \gets a \cdot G\cr &B \gets b \cdot G\cr &C \gets a \cdot B\cr \cr &\underline{\mathtt{Instance}():}\cr &\ \texttt{return } (A, B) \cr \cr &\underline{\mathtt{Query}(\hat{B}, \hat{C}):}\cr &\ \texttt{return } a \cdot \hat{B} = \hat{C}\cr \cr &\underline{\mathtt{Guess}(\hat{C}):}\cr &\ \texttt{return } b = 0 \land \hat{C} = C\cr \end{aligned} }

This notion suffices to prove IND-CCA\text{IND-CCA} security of the KEM:

IND-CCA-12ICDHb\text{IND-CCA-1} \leq 2 \cdot \text{ICDH}_b

We'll model our hash function as a random oracle, like with RSA.

Let's start by explicitly writing down the IND-CCA-1\text{IND-CCA-1} game in this context:

IND-CCA-1b seenaRFqAaGGetPk(): return A(1)Challenge(): bRFq BbG CcC k0H(B,C) k1RK seenseen{B} return (kb,B)Decap(B): assert Bseen return H(B,aB)h[]H(B,C): if (B,C)h h[B,C]RK return h[B,C]\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND-CCA-1}_b$ }\cr \cr &\text{seen} \gets \emptyset\cr &a \xleftarrow{R} \mathbb{F}_q\cr &A \gets a \cdot G\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } A\cr \cr &\underline{(1) \mathtt{Challenge}():}\cr &\ b \xleftarrow{R} \mathbb{F}_q\cr &\ B \gets b \cdot G\cr &\ C \gets c \cdot C\cr &\ k_0 \gets \texttt{H}(B, C)\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \text{seen} \gets \text{seen} \cup \{B\}\cr &\ \texttt{return } (k_b, B) \cr \cr &\underline{\mathtt{Decap}(B):}\cr &\ \texttt{assert } B \notin \text{seen}\cr &\ \texttt{return } \texttt{H}(B, a \cdot B) \cr \cr &h[\cdot] \gets \bot\cr &\underline{\mathtt{H}(B, C):}\cr &\ \texttt{if } (B, C) \notin h\cr &\quad\ h[B, C] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[B, C]\cr \end{aligned} }

Next, like with the RSA kem proof, we need to minimize our use of the secret key when decapsulating. We follow the same strategy as in Theorem 12.4 of Boneh and Shoup.

We replace our hash table for pairs (B,C)(B, C) with a table for BB, when C=aBC = a \cdot B, and a table for (B,C)(B, C), when BB and CC are not related. Since we only have a single challenge query, we can also lift generation of those values outside the challenge.

This gives us the following game:

IND-CCA-1b=Γb0 a,bRFqAaGBbGCaB k0RKGetPk(): return A(1)Challenge(): k1RK return (kb,B)h[],h[]Decap(B^): assert B^B if B^h h[B^]RK return h[B^]H(B^,C^): if (B^,C^)h if aB^=C^ if C^=C h[B^]k0 if B^h h[B^]RK h[B^,C^]h[B^] else  h[B^,C^]RK return h[B^,C^]\text{IND-CCA-1}_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^0_b$ }\cr \cr &a, b \xleftarrow{R} \mathbb{F}_q\cr &A \gets a \cdot G\cr &B \gets b \cdot G\cr &C \gets a \cdot B\cr &\ k_0 \xleftarrow{R} \bold{K}\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } A\cr \cr &\underline{(1) \mathtt{Challenge}():}\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \texttt{return } (k_b, B) \cr \cr &h[\cdot], h'[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}(\hat{B}):}\cr &\ \texttt{assert } \hat{B} \neq B\cr &\ \texttt{if } \hat{B} \notin h'\cr &\quad\ h'[\hat{B}] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h'[\hat{B}] \cr \cr &\underline{\mathtt{H}(\hat{B}, \hat{C}):}\cr &\ \texttt{if } (\hat{B}, \hat{C}) \notin h \cr &\quad\ \texttt{if } a \cdot \hat{B} = \hat{C}\cr &\quad\quad\ \texttt{if } \hat{C} = C \cr &\quad\quad\quad\ h'[\hat{B}] \gets k_0\cr &\quad\quad\ \texttt{if } \hat{B} \notin h' \cr &\quad\quad\quad\ h'[\hat{B}] \xleftarrow{R} \bold{K}\cr &\quad\quad\ h[\hat{B}, \hat{C}] \gets h'[\hat{B}]\cr &\quad\ \texttt{else }\cr &\quad\quad\ h[\hat{B}, \hat{C}] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[\hat{B}, \hat{C}]\cr \end{aligned} }

Basically, we maintain the invariant that aB^=C^h[B^,C^]=h[B^]a \cdot \hat{B} = \hat{C} \implies h[\hat{B}, \hat{C}] = h'[\hat{B}]. this allows us to make decapsulation queries without making use of the secret key. This will make extracting out the ICDH\text{ICDH} game much easier. This extraction is also made easier in that the query aB^=C^a \cdot \hat{B} = \hat{C} is precisely the kind of query we can do in this game.

The next step is to extract out ICDH\text{ICDH}:

Γb0=Γb1 (A,B)Instance() k0RKGetPk(): return A(1)Challenge(): k1RK return (kb,B)h[],h[]Decap(B^): assert B^B if B^h h[B^]RK return h[B^]H(B^,C^): if (B^,C^)h if Query(B^,C^) if Guess(C^) h[B^]k0 if B^h h[B^]RK h[B^,C^]h[B^] else  h[B^,C^]RK return h[B^,C^]ICDH0\Gamma^0_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^1_b$ }\cr \cr &(A, B) \gets \texttt{Instance()}\cr &\ k_0 \xleftarrow{R} \bold{K}\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } A\cr \cr &\underline{(1) \mathtt{Challenge}():}\cr &\ k_1 \xleftarrow{R} \bold{K}\cr &\ \texttt{return } (k_b, B) \cr \cr &h[\cdot], h'[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}(\hat{B}):}\cr &\ \texttt{assert } \hat{B} \neq B\cr &\ \texttt{if } \hat{B} \notin h'\cr &\quad\ h'[\hat{B}] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h'[\hat{B}] \cr \cr &\underline{\mathtt{H}(\hat{B}, \hat{C}):}\cr &\ \texttt{if } (\hat{B}, \hat{C}) \notin h \cr &\quad\ \texttt{if } \texttt{Query}(\hat{B}, \hat{C})\cr &\quad\quad\ \texttt{if } \texttt{Guess}(\hat{C}) \cr &\quad\quad\quad\ h'[\hat{B}] \gets k_0\cr &\quad\quad\ \texttt{if } \hat{B} \notin h' \cr &\quad\quad\quad\ h'[\hat{B}] \xleftarrow{R} \bold{K}\cr &\quad\quad\ h[\hat{B}, \hat{C}] \gets h'[\hat{B}]\cr &\quad\ \texttt{else }\cr &\quad\quad\ h[\hat{B}, \hat{C}] \xleftarrow{R} \bold{K}\cr &\ \texttt{return } h[\hat{B}, \hat{C}]\cr \end{aligned} } \circ \text{ICDH}_0

Now, notice that in the game Γb1ICDH1\Gamma^1_b \circ \text{ICDH}_1, k0k_0 isn't used anywhere but inside of Challenge\texttt{Challenge}, because Guess\texttt{Guess} always returns 00. This means that the difference between k0k_0 and k1k_1 becomes a matter of naming, so:

Γ1_0ICDH_1=Γ1_1ICDH_1\Gamma^1\_0 \circ \text{ICDH}\_1 = \Gamma^1\_1 \circ \text{ICDH}\_1

This is enough to make our full walk, and tie everything together:

IND-CCA-10=Γ00=Γ01ICDH0ϵ1Γ01ICDH1=Γ11ICDH1ϵ1Γ11ICDH0=Γ01=IND-CCA-11\begin{aligned} \text{IND-CCA-1}_0 &= \Gamma^0_0\cr &= \Gamma^1_0 \circ \text{ICDH}_0\cr &\stackrel{\epsilon_1}{\approx} \Gamma^1_0 \circ \text{ICDH}_1\cr &= \Gamma^1_1 \circ \text{ICDH}_1\cr &\stackrel{\epsilon_1}{\approx} \Gamma^1_1 \circ \text{ICDH}_0\cr &= \Gamma^1_0\cr &= \text{IND-CCA-1}_1\cr \end{aligned}

\square

And Other Methods

Another method that's going to be increasingly important is using lattices to construct KEMs.

Both of the schemes I've mentioned in this post, using RSA, and using elliptic curves, will be broken by future quantum computers. This is why NIST started a competition in order to standardize KEMs secure against quantum computers.

Many of the candidates were based on lattices, including the finalist, Kyber.

Other candidates used different primitives, like isogenies, or random codes.

Composing KEMs

Talking about the post-quantum KEMs segues nicely into this next topic. Right now, we have very well tested and trusted KEMs, which aren't secure against quantum computers. We also have a new KEM which should be secure against quantum computers, but given its novelty, you may not trust it that much yet.

Because of this, many people are trying to deploy both schemes together in a hybrid fashion. This means that you can get post-quantum security if the new KEM stands the test of time, but you also don't sacrifice classical security if the new KEM you include happens to be flawed.

To achieve this, what you want is a way to combine KEMs, mixing together KEMs AA and BB to create a new KEM, which should be secure as long as at least one of the two ingredients is. If AA is broken, that's fine as long as BB is secure, and vice versa.

This construction is called a KEM combiner, and it can combine two KEMs in this way, without having to inspect how the KEMs work internally at all. The paper [^[GHP18]] goes over this notion of combiners, and presents a very elegant construction.

To encapsulate, you call each of the individual KEMs first, giving you (kA,cA)(k_A, c_A) and (kB,cB)(k_B, c_B). Next, (cA,cB)(c_A, c_B) becomes your ciphertext. Now, you need some way to combine all of this information to derive a single key kk. One idea would be to simply xor the two keys, giving you kkAkBk \gets k_A \oplus k_B. Unfortunately, this is not IND-CCA secure, because the resulting key is malleable.

The idea in this paper is instead to use a kind of pseudo-random function (PRF) to derive the result:

F(kA,kB,(cA,cB))F(k_A, k_B, (c_A, c_B))

This is a special kind of PRF called a split-key PRF.

Split-Key PRFs in Theory

More formally, a split-key PRF is a function:

F:K0×K1×XYF : \bold{K}_0 \times \bold{K}_1 \times \bold{X} \to \bold{Y}

We have two types for keys, K0\bold{K}_0 and K1\bold{K}_1, as well as an input type X\bold{X}, and an output type Y\bold{Y}. The intuition for this function is that it should behave like a random function XY\bold{X} \to \bold{Y} as long as the adversary doesn't know either of the keys. Even if the adversary controls one of the keys, and can query the function FF on different values for this key, they still shouldn't be able to distinguish this function from a random one.

We formalize this with a game:

SPLIT-PRFb(σ)  k_σRK0 seenQueryF(k_(1σ),x): assert xseen y0F(k0,k1,x) y1RY return yb\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{SPLIT-PRF}_b(\sigma)$ }\cr &\ k\_{\sigma} \xleftarrow{R} \bold{K}_0\cr &\ \text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(k\_{(1 - \sigma)}, x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y_0 \gets F(k_0, k_1, x)\cr &\ y_1 \xleftarrow{R} \bold{Y}\cr &\ \texttt{return } y_b \end{aligned} }

One subtlety is that we don't allow the adversary to query the same input point twice, even with different keys. A stronger security notion would be to allow queries to the same input with a different key, but we won't need that for proving the security of our KEM combination scheme.

Split-Key PRFs are Secure PRFs

[!note] Note: If you're convinced of this just from reading the section header, feel free to skip this section.

With a split-key PRF, we model security in a situation where the adversary can control one of the keys. When looking at the security of a PRF, the adversary doesn't control the key at all. One interesting fact is that a split-key PRF is necessarily a secure PRF, considering (k0,k1)(k_0, k_1) as a single "key".

PRFSPLIT-PRF(σ)\text{PRF} \leq \text{SPLIT-PRF}(\sigma)

Let's recall the PRF game (in this context), briefly:

PRFb  k0RK0 k1RK1 seenQueryF(x): assert xseen y0F(k0,k1,x) y1RY return yb\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{PRF}_b$ }\cr \cr &\ k_0 \xleftarrow{R} \bold{K}_0\cr &\ k_1 \xleftarrow{R} \bold{K}_1\cr &\ \text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y_0 \gets F(k_0, k_1, x)\cr &\ y_1 \xleftarrow{R} \bold{Y}\cr &\ \texttt{return } y_b \end{aligned} }

From here you just need to extract out the split-key game:

PRFb=Γ  k_(1σ)RK_(1σ)QueryF(x): return super.QueryF(k_(1σ),x)SPLIT-PRFb(σ)  k_σRK0 seenQueryF(k_(1σ),x): assert xseen y0F(k0,k1,x) y1RY return yb\text{PRF}_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma$ }\cr \cr &\ k\_{(1 - \sigma)} \xleftarrow{R} \bold{K}\_{(1 - \sigma)}\cr \cr &\underline{\texttt{QueryF}(x):}\cr &\ \texttt{return } \texttt{super.QueryF}(k\_{(1 - \sigma)}, x) \end{aligned} } \circ \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{SPLIT-PRF}_b(\sigma)$ }\cr \cr &\ k\_{\sigma} \xleftarrow{R} \bold{K}_0\cr &\ \text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(k\_{(1 - \sigma)}, x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y_0 \gets F(k_0, k_1, x)\cr &\ y_1 \xleftarrow{R} \bold{Y}\cr &\ \texttt{return } y_b \end{aligned} }

This gives us:

PRF0=ΓSPLIT-PRF0(σ)ϵ1ΓSPLIT-PRF1(σ)=PRF1\text{PRF}_0 = \Gamma \circ \text{SPLIT-PRF}_0(\sigma) \stackrel{\epsilon_1}{\approx} \Gamma \circ \text{SPLIT-PRF}_1(\sigma) = \text{PRF}_1

concluding our proof.

\square

KEM Combination with PRF

It turns out that split-key PRFs precisely capture what we need to compose KEMs together. In this section, we formally define the composition of two KEMs using such a PRF, and prove that it's IND-CCA\text{IND-CCA} secure, assuming the PRF is split-key secure, and one of the underlying KEMs is IND-CCA\text{IND-CCA} secure.

We start with two KEMs, AA, and BB. The composition will be like the informal idea we saw earlier: in order to encapsulate, we use the encapsulation from each KEM, and then combine them together as a pair (cA,cB)(c_A, c_B). When decapsulating, we end up with two keys kAk_A and kBk_B. We'll use a split-key PRF to combine them, including the ciphertexts, to give us:

kF(kA,kB,(cA,cB))k \gets F(k_A, k_B, (c_A, c_B))

A bit more formally, given two KEMs AA and BB, and a function

F:A.K×B.K×(A.C×B.C)KF : A.\bold{K} \times B.\bold{K} \times (A.\bold{C} \times B.\bold{C}) \to \bold{K}

we can define the combined KEM A×FBA \times_F B as follows:

Gen(): (skA,pkA)A.Gen() (skB,pkB)B.Gen() return ((skA,skB),(pkA,pkB))Encap((pkA,pkB): (kA,cA)A.Encap(pkA) (kB,cB)B.Encap(pkB) kF(kA,kB,(cA,cB)) return (k,(cA,cB))Decap((skA,skB),(cA,cB)): kAA.Decap(skA,cA) kBB.Decap(skB,cB) kF(kA,kB,(cA,cB)) return k\begin{aligned} &\underline{\text{Gen}():}\cr &\ (\text{sk}_A, \text{pk}_A) \gets \text{A.Gen}()\cr &\ (\text{sk}_B, \text{pk}_B) \gets \text{B.Gen}()\cr &\ \texttt{return } ((\text{sk}_A, \text{sk}_B), (\text{pk}_A, \text{pk}_B)) \cr \cr &\underline{\text{Encap}((\text{pk}_A, \text{pk}_B):}\cr &\ (k_A, c_A) \gets \text{A.Encap}(\text{pk}_A)\cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B)\cr &\ k \gets F(k_A, k_B, (c_A, c_B))\cr &\ \texttt{return } (k, (c_A, c_B))\cr \cr &\underline{\text{Decap}((\text{sk}_A, \text{sk}_B), (c_A, c_B)):}\cr &\ k_A \gets \text{A.Decap}(\text{sk}_A, c_A)\cr &\ k_B \gets \text{B.Decap}(\text{sk}_B, c_B)\cr &\ k \gets F(k_A, k_B, (c_A, c_B))\cr &\ \texttt{return } k\cr \end{aligned}

In terms of encapsulation, this works in the way you'd expect for a product construction. The only real trick comes from decapsulation, where we use our function FF to combine both keys and the ciphertexts to produce a final key.

Security Proof

It turns out that if FF is a split-key PRF, then the security of this scheme reduces to that of either one of the KEMs.

Because our combined KEM definition is completely symmetric with respect to AA or BB, we prove this, without loss of generality, just by treating the case where AA is IND-CCA\text{IND-CCA} secure, and show that:

IND-CCA-1(A×FB)2IND-CCA-1(A)+2SPLIT-PRF\text{IND-CCA-1}(A \times_F B) \leq 2 \cdot \text{IND-CCA-1}(A) + 2 \cdot \text{SPLIT-PRF}

Let's start, as we've done a few times in this post, by explicitly writing out the IND-CCA-1\text{IND-CCA-1} game using our KEM:

IND-CCA-1b seen(skA,pkA)RA.Gen()(skB,pkB)RB.Gen()GetPk(): return (pkA,pkB)(1)Challenge(): (kA,cA)A.Encap(pkA) (kB,cB)B.Encap(pkB) k0F(kA,kB,(cA,cB)) k1RK seenseen{(cA,cB)} return (kb,(cA,cB))Decap((c^A,c^B)): assert (c^A,c^B)seen k^AA.Decap(skA,c^A) k^BB.Decap(skB,c^B) if k^A=k^B= return  return F(k^A,k^B,(c^A,c^B))\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{IND-CCA-1}_b$ }\cr \cr &\text{seen} \gets \emptyset\cr &(\text{sk}_A, \text{pk}_A) \xleftarrow{R} \text{A.Gen}()\cr &(\text{sk}_B, \text{pk}_B) \xleftarrow{R} \text{B.Gen}()\cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } (\text{pk}_A, \text{pk}_B) \cr \cr &\underline{(1)\mathtt{Challenge}():}\cr &\ (k_A, c_A) \gets \text{A.Encap}(\text{pk}_A) \cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B) \cr &\ k_0 \gets F(k_A, k_B, (c_A, c_B))\cr &\ k_1 \xleftarrow{R} \bold{K} \cr &\ \text{seen} \gets \text{seen} \cup \{(c_A, c_B)\}\cr &\ \texttt{return } (k_b, (c_A, c_B)) \cr \cr &\underline{\mathtt{Decap}((\hat{c}_A, \hat{c}_B)):}\cr &\ \texttt{assert } (\hat{c}_A, \hat{c}_B) \notin \text{seen}\cr &\ \hat{k}_A \gets \text{A.Decap}(\text{sk}_A, \hat{c}_A)\cr &\ \hat{k}_B \gets \text{B.Decap}(\text{sk}_B, \hat{c}_B)\cr &\ \texttt{if } \hat{k}_A = \bot \lor \hat{k}_B = \bot\cr &\ \quad \texttt{return } \bot\cr &\ \texttt{return } F(\hat{k}_A, \hat{k}_B, (\hat{c}_A, \hat{c}_B)) \cr \end{aligned} }

Next, we pull the usual trick of pulling out code from Challenge\texttt{Challenge}, since it only gets called a single time. We also want to change the decapsulation function to minimize the use of skA\text{sk}_A, since we'll be replacing explicit calls to Decap\text{Decap} with oracle calls to IND-CCA-1(A)\text{IND-CCA-1}(A).

IND-CCA-1b=Γb0 (skA,pkA)RA.Gen()(skB,pkB)RB.Gen() (kA,cA)A.Encap(pkA) (kB,cB)B.Encap(pkB) k0F(kA,kB,(cA,cB)) k1RKGetPk(): return (pkA,pkB)(1)Challenge(): return (kb,(cA,cB))d[]Decap((c^A,c^B)): assert (c^A,c^B)(cA,cB) k^BB.Decap(skB,c^B) if k^B= return  if c^A=cA k^AkA else  k^AA.Decap(skA,c^A) if k^A= return  if (c^A,c^B)d d[(c^A,c^B)]F(k^A,k^B,(c^A,c^B)) return d[(c^A,c^B)]\text{IND-CCA-1}_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^0_b$ }\cr \cr &(\text{sk}_A, \text{pk}_A) \xleftarrow{R} \text{A.Gen}()\cr &(\text{sk}_B, \text{pk}_B) \xleftarrow{R} \text{B.Gen}()\cr &\ (k_A, c_A) \gets \text{A.Encap}(\text{pk}_A) \cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B) \cr &\ k_0 \gets F(k_A, k_B, (c_A, c_B))\cr &\ k_1 \xleftarrow{R} \bold{K} \cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \texttt{return } (\text{pk}_A, \text{pk}_B) \cr \cr &\underline{(1)\mathtt{Challenge}():}\cr &\ \texttt{return } (k_b, (c_A, c_B)) \cr \cr &d[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}((\hat{c}_A, \hat{c}_B)):}\cr &\ \texttt{assert } (\hat{c}_A, \hat{c}_B) \neq (c_A, c_B)\cr &\ \hat{k}_B \gets \text{B.Decap}(\text{sk}_B, \hat{c}_B)\cr &\ \texttt{if } \hat{k}_B = \bot\cr &\ \quad \texttt{return } \bot\cr &\ \texttt{if } \hat{c}_A = c_A\cr &\ \quad \hat{k}_A \gets k_A\cr &\ \texttt{else }\cr &\ \quad \hat{k}_A \gets \text{A.Decap}(\text{sk}_A, \hat{c}_A)\cr &\ \quad \texttt{if } \hat{k}_A = \bot\cr &\ \quad\quad \texttt{return } \bot\cr &\ \texttt{if } (\hat{c}_A, \hat{c}_B) \notin d\cr &\ \quad d[(\hat{c}_A, \hat{c}_B)] \gets F(\hat{k}_A, \hat{k}_B, (\hat{c}_A, \hat{c}_B))\cr &\ \texttt{return } d[(\hat{c}_A, \hat{c}_B)] \cr \end{aligned} }

The behavior of decapsulation is the same, we just make a few preparations for later.

First, we cache the output of the decapsulation in dd. This is because the PRF oracle we'll use later can't be queried twice on the same input, so we'll need to use this table to avoid doing that.

Second, we check whether c^A=cA\hat{c}_A = c_A in order to explictly use the hardcoded kAk_A. This will make extracting out the AA KEM much easier.

Our next step is to extract out this KEM.

Γb0=Γb1 (skB,pkB)RB.Gen() (kA,cA)super.Challenge() (kB,cB)B.Encap(pkB) k0F(kA,kB,(cA,cB)) k1RKGetPk(): pkAsuper.GetPk() return (pkA,pkB)(1)Challenge(): return (kb,(cA,cB))d[]Decap((c^A,c^B)): assert (c^A,c^B)(cA,cB) k^BB.Decap(skB,c^B) if k^B= return  if c^A=cA k^AkA else  k^Asuper.Decap(c^A) if k^A= return  if (c^A,c^B)d d[(c^A,c^B)]F(k^A,k^B,(c^A,c^B)) return d[(c^A,c^B)]IND-CCA-10(A)\Gamma^0_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^1_b$ }\cr \cr &(\text{sk}_B, \text{pk}_B) \xleftarrow{R} \text{B.Gen}()\cr &\ (k_A, c_A) \gets \texttt{super.Challenge}() \cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B) \cr &\ k_0 \gets F(k_A, k_B, (c_A, c_B))\cr &\ k_1 \xleftarrow{R} \bold{K} \cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \text{pk}_A \gets \texttt{super.GetPk}()\cr &\ \texttt{return } (\text{pk}_A, \text{pk}_B) \cr \cr &\underline{(1)\mathtt{Challenge}():}\cr &\ \texttt{return } (k_b, (c_A, c_B)) \cr \cr &d[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}((\hat{c}_A, \hat{c}_B)):}\cr &\ \texttt{assert } (\hat{c}_A, \hat{c}_B) \neq (c_A, c_B)\cr &\ \hat{k}_B \gets \text{B.Decap}(\text{sk}_B, \hat{c}_B)\cr &\ \texttt{if } \hat{k}_B = \bot\cr &\ \quad \texttt{return } \bot\cr &\ \texttt{if } \hat{c}_A = c_A\cr &\ \quad \hat{k}_A \gets k_A\cr &\ \texttt{else }\cr &\ \quad \hat{k}_A \gets \texttt{super.Decap}(\hat{c}_A)\cr &\ \quad \texttt{if } \hat{k}_A = \bot\cr &\ \quad\quad \texttt{return } \bot\cr &\ \texttt{if } (\hat{c}_A, \hat{c}_B) \notin d\cr &\ \quad d[(\hat{c}_A, \hat{c}_B)] \gets F(\hat{k}_A, \hat{k}_B, (\hat{c}_A, \hat{c}_B))\cr &\ \texttt{return } d[(\hat{c}_A, \hat{c}_B)] \cr \end{aligned} } \circ \text{IND-CCA-1}_0(A)

With this game, we've extracted out the AA KEM completely. One key thing which enabled us to do this was checking that c^A=cA\hat{c}_A = c_A ourselves, to avoid hitting the assertion inside of AA's Decap\texttt{Decap} oracle.

Naturally, we'll be able to make the jump from IND-CCA-10(A)\text{IND-CCA-1}_0(A) to IND-CCA-11(A)\text{IND-CCA-1}_1(A), but that won't be enough to complete our proof. We still need to make use of our PRF assumption.

To that effect, let's write a game which explicitly captures what happens after making the switch to an ideal KEM:

Γb1IND-CCA-11(A)=Γb2 (skB,pkB)RB.Gen() (,cB)A.Encap(pkA) kARA.K (kB,cB)B.Encap(pkB) k0F(kA,kB,(cA,cB)) k1RKGetPk(): pkAsuper.GetPk() return (pkA,pkB)(1)Challenge(): return (kb,(cA,cB))d[]Decap((c^A,c^B)): assert (c^A,c^B)(cA,cB) k^BB.Decap(skB,c^B) if k^B= return  if c^A=cA if (c^A,c^B)d d[(c^A,c^B)]F(kA,k^B,(c^A,c^B)) return d[(c^A,c^B)] else  k^Asuper.Decap(c^A) if k^A= return  return F(k^A,k^B,(c^A,c^B))\Gamma^1_b \circ \text{IND-CCA-1}_1(A) = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^2_b$ }\cr \cr &(\text{sk}_B, \text{pk}_B) \xleftarrow{R} \text{B.Gen}()\cr &\ (\cdot, c_B) \gets \text{A.Encap}(\text{pk}_A) \cr &\ k_A \xleftarrow{R} A.\bold{K}\cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B) \cr &\ k_0 \gets F(k_A, k_B, (c_A, c_B))\cr &\ k_1 \xleftarrow{R} \bold{K} \cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \text{pk}_A \gets \texttt{super.GetPk}()\cr &\ \texttt{return } (\text{pk}_A, \text{pk}_B) \cr \cr &\underline{(1)\mathtt{Challenge}():}\cr &\ \texttt{return } (k_b, (c_A, c_B)) \cr \cr &d[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}((\hat{c}_A, \hat{c}_B)):}\cr &\ \texttt{assert } (\hat{c}_A, \hat{c}_B) \neq (c_A, c_B)\cr &\ \hat{k}_B \gets \text{B.Decap}(\text{sk}_B, \hat{c}_B)\cr &\ \texttt{if } \hat{k}_B = \bot\cr &\ \quad \texttt{return } \bot\cr &\ \texttt{if } \hat{c}_A = c_A\cr &\ \quad\texttt{if } (\hat{c}_A, \hat{c}_B) \notin d\cr &\ \quad\quad d[(\hat{c}_A, \hat{c}_B)] \gets F(k_A, \hat{k}_B, (\hat{c}_A, \hat{c}_B))\cr &\ \quad\texttt{return } d[(\hat{c}_A, \hat{c}_B)] \cr &\ \texttt{else }\cr &\ \quad \hat{k}_A \gets \texttt{super.Decap}(\hat{c}_A)\cr &\ \quad \texttt{if } \hat{k}_A = \bot\cr &\ \quad\quad\texttt{return } \bot \cr &\ \quad \texttt{return } F(\hat{k}_A, \hat{k}_B, (\hat{c}_A, \hat{c}_B))\cr \end{aligned} }

At the start of the game, we can ignore whatever key we get from AA's encapsulation, and instead use a random kAk_A.

At the end of the game, in decapsulation, we've shifted things around to make how the PRF is queried more clear. In one branch, we query the PRF using that initial kAk_A key, and so we want to make sure to cache results. In the other branch, we use an unrelated key, and so we don't care.

Our next trick is going to be to replace calls to F(kA,)F(k_A, \ldots) with oracle queries to the SPLIT-PRF\text{SPLIT-PRF} game.

Γb2=Γb3 (skB,pkB)RB.Gen() (,cB)A.Encap(pkA) (kB,cB)B.Encap(pkB) k0super.QueryF(kB,(cA,cB)) k1RKGetPk(): pkAsuper.GetPk() return (pkA,pkB)(1)Challenge(): return (kb,(cA,cB))d[]Decap((c^A,c^B)): assert (c^A,c^B)(cA,cB) k^BB.Decap(skB,c^B) if k^B= return  if c^A=cA if (c^A,c^B)d d[(c^A,c^B)]super.QueryF(k^B,(c^A,c^B)) return d[(c^A,c^B)] else  k^Asuper.Decap(c^A) if k^A= return  return F(k^A,k^B,(c^A,c^B))SPLIT-PRF0\Gamma^2_b = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^3_b$ }\cr \cr &(\text{sk}_B, \text{pk}_B) \xleftarrow{R} \text{B.Gen}()\cr &\ (\cdot, c_B) \gets \text{A.Encap}(\text{pk}_A) \cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B) \cr &\ k_0 \gets \texttt{super.QueryF}(k_B, (c_A, c_B))\cr &\ k_1 \xleftarrow{R} \bold{K} \cr \cr &\underline{\mathtt{GetPk}():}\cr &\ \text{pk}_A \gets \texttt{super.GetPk}()\cr &\ \texttt{return } (\text{pk}_A, \text{pk}_B) \cr \cr &\underline{(1)\mathtt{Challenge}():}\cr &\ \texttt{return } (k_b, (c_A, c_B)) \cr \cr &d[\cdot] \gets \bot\cr \cr &\underline{\mathtt{Decap}((\hat{c}_A, \hat{c}_B)):}\cr &\ \texttt{assert } (\hat{c}_A, \hat{c}_B) \neq (c_A, c_B)\cr &\ \hat{k}_B \gets \text{B.Decap}(\text{sk}_B, \hat{c}_B)\cr &\ \texttt{if } \hat{k}_B = \bot\cr &\ \quad \texttt{return } \bot\cr &\ \texttt{if } \hat{c}_A = c_A\cr &\ \quad\texttt{if } (\hat{c}_A, \hat{c}_B) \notin d\cr &\ \quad\quad d[(\hat{c}_A, \hat{c}_B)] \gets \texttt{super.QueryF}(\hat{k}_B, (\hat{c}_A, \hat{c}_B))\cr &\ \quad\texttt{return } d[(\hat{c}_A, \hat{c}_B)] \cr &\ \texttt{else }\cr &\ \quad \hat{k}_A \gets \texttt{super.Decap}(\hat{c}_A)\cr &\ \quad \texttt{if } \hat{k}_A = \bot\cr &\ \quad\quad\texttt{return } \bot \cr &\ \quad \texttt{return } F(\hat{k}_A, \hat{k}_B, (\hat{c}_A, \hat{c}_B))\cr \end{aligned} } \circ \text{SPLIT-PRF}_0

One interesting thing is that we keep calls to the actual function FF, we just use the oracle queries for when the key kAk_A was used before.

Now, the next question is: what happens when our PRF is perfect? Well, we get the following situation:

Γb3SPLIT-PRF1=Γb4 (skB,pkB)RB.Gen() (,cB)A.Encap(pkA) (kB,cB)B.Encap(pkB) k0RK k1RK\Gamma^3_b \circ \text{SPLIT-PRF}_1 = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma^4_b$ }\cr \cr &(\text{sk}_B, \text{pk}_B) \xleftarrow{R} \text{B.Gen}()\cr &\ (\cdot, c_B) \gets \text{A.Encap}(\text{pk}_A) \cr &\ (k_B, c_B) \gets \text{B.Encap}(\text{pk}_B) \cr &\ k_0 \xleftarrow{R} \bold{K} \cr &\ k_1 \xleftarrow{R} \bold{K} \cr &\ldots\cr \end{aligned} }

The rest of the game doesn't matter. The important part is that now there's no difference between k0k_0 and k1k_1, and so we have Γ04=Γ14\Gamma^4_0 = \Gamma^4_1. Now, we can do the walk backwards, chaining everything together to get our result:

IND-CCA-10=Γ00=Γ01IND-CCA-10(A)ϵ1Γ01IND-CCA-11(A)=Γ02=Γ03SPLIT-PRF0ϵ2Γ03SPLIT-PRF1=Γ04=Γ14=Γ13SPLIT-PRF1ϵ2Γ13SPLIT-PRF0=Γ12=Γ11IND-CCA-11(A)ϵ1Γ11IND-CCA-10(A)=Γ10=IND-CCA-11\begin{aligned} \text{IND-CCA-1}_0 &= \Gamma^0_0\cr &= \Gamma^1_0 \circ \text{IND-CCA-1}_0(A)\cr &\stackrel{\epsilon_1}{\approx} \Gamma^1_0 \circ \text{IND-CCA-1}_1(A)\cr &= \Gamma^2_0\cr &= \Gamma^3_0 \circ \text{SPLIT-PRF}_0\cr &\stackrel{\epsilon_2}{\approx} \Gamma^3_0 \circ \text{SPLIT-PRF}_1\cr &= \Gamma^4_0\cr &= \Gamma^4_1\cr &= \Gamma^3_1 \circ \text{SPLIT-PRF}_1\cr &\stackrel{\epsilon_2}{\approx} \Gamma^3_1 \circ \text{SPLIT-PRF}_0\cr &= \Gamma^2_1\cr &= \Gamma^1_1 \circ \text{IND-CCA-1}_1(A)\cr &\stackrel{\epsilon_1}{\approx} \Gamma^1_1 \circ \text{IND-CCA-1}_0(A)\cr &= \Gamma^0_1\cr &= \text{IND-CCA-1}_1\cr \end{aligned}

\square

Constructing Split-Key PRFs

In the KEM combiner paper [^[GHP18]], they present many examples of split-key PRFs. As an illustration, I'll choose one of the simplest ones, which can be proven secure even without random oracles. I'd recommend checking out the papers for other more efficient constructions.

Xoring Two PRFs

One simple construction involves combining two PRFs F0F_0 and F1F_1 together:

F(k0,k1,x):=F0(k0,x)F1(k1,x)F(k_0, k_1, x) := F_0(k_0, x) \oplus F_1(k_1, x)

This PRF is split-key because even if you can influence one of the keys, the other side looks completely random, and thus so is the final result. Let's prove this a bit more formally.

Without loss of generality, we can simply prove that it's split key for the first key, k0k_0, since the function is symmetric.

SPLIT-PRF(0)PRF(F0)\text{SPLIT-PRF}(0) \leq \text{PRF}(F_0)

The high level proof idea is that what happens with F1F_1 can't affect the outcome, because F0F_0 being a PRF is enough to guarantee that the output is random. In our split-key game, we'll easily be able to

First, explicitly write the split-key game:

SPLIT-PRF0  k_0RK0 seenQueryF(k1,x): assert xseen yF(k0,x)F(k1,x) return y\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{SPLIT-PRF}_0$ }\cr \cr &\ k\_0 \xleftarrow{R} \bold{K}_0\cr &\ \text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(k_1, x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y \gets F(k_0, x) \oplus F(k_1, x)\cr &\ \texttt{return } y \end{aligned} }

Next, extract out F0F_0:

SPLIT-PRF0=Γ QueryF(k1,x): ysuper.QueryF(x)F(k1,x) return yPRF0  kRK0 seenQueryF(k1,x): assert xseen yF(k0,x) return y\text{SPLIT-PRF}_0 = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma$ }\cr \cr &\underline{\texttt{QueryF}(k_1, x):}\cr &\ y \gets \texttt{super.QueryF}(x) \oplus F(k_1, x)\cr &\ \texttt{return } y \end{aligned} } \circ \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{PRF}_0$ }\cr \cr &\ k \xleftarrow{R} \bold{K}_0\cr &\ \text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(k_1, x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y \gets F(k_0, x) \cr &\ \texttt{return } y \end{aligned} }

Next, notice that if we switch to PRF1\text{PRF}_1, then the output of Γ\Gamma becomes completely random:

Γ QueryF(k1,x): ysuper.QueryF(x)F(k1,x) return yPRF1  seenQueryF(k1,x): assert xseen yRY return y=SPLIT-PRF1 seenQueryF(k1,x): assert xseen yRY return y\boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\Gamma$ }\cr \cr &\underline{\texttt{QueryF}(k_1, x):}\cr &\ y \gets \texttt{super.QueryF}(x) \oplus F(k_1, x)\cr &\ \texttt{return } y \end{aligned} } \circ \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{PRF}_1$ }\cr \cr &\ \text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(k_1, x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y \xleftarrow{R} \bold{Y}\cr &\ \texttt{return } y \end{aligned} } = \boxed{ \begin{aligned} &\colorbox{#FBCFE8}{\large $\text{SPLIT-PRF}_1$ }\cr \cr &\text{seen} \gets \emptyset\cr \cr &\underline{\texttt{QueryF}(k_1, x):}\cr &\ \texttt{assert } x \notin \text{seen}\cr &\ y \xleftarrow{R} \bold{Y}\cr &\ \texttt{return } y \end{aligned} }

This is because xoring any value with a (uniformly) random value returns another random value. Thus, xoring F(k1,x)F(k_1, x) with a random yy gives us a random result.

To summarize, we have:

SPLIT-PRF0=ΓPRF0ϵ1ΓPRF1=SPLIT-PRF1\text{SPLIT-PRF}_0 = \Gamma \circ \text{PRF}_0 \stackrel{\epsilon_1}{\approx} \Gamma \circ \text{PRF}_1 = \text{SPLIT-PRF}_1

which concludes our proof.

\square

Other Methods

The kem combiner paper [^GHP18] also provides various other methods for creating split-key PRFs.

One method is to do F(kA,cA)F(kB,cB)F(k_A, c_A) \oplus F(k_B, c_B), including only one of the ciphertexts.

Another method, which requires the random oracle model, is to do: H(kAkB,cA,cB){H(k_A \oplus k_B, c_A, c_B)}.

And this is just a small sample; I really recommend checking out the paper if you want to know more; it's a very well written paper!

Conclusion

While there weren't that many topics covered in thist post, I felt that stopping it here made for a hefty post already, especially with the focus on proofs. I think state-separable proofs are quite fun to write, and hopefully they were enjoyable to read as well. A secret about this post is that KEMs are really just an excuse to write more state-separable proofs, since that's what motivated me to write about the topic.

There's still a lot more interesting things to say about KEMs though. One notion I didn't touch on at all here is that of authenticated KEMs, where you also want to authenticate the sender in some way. Neil Madden wrote a post on this topic as well, and you might want to check out [^ABHKLR20], which analyzes the HPKE standard for authenticated KEMs, outlining various notions of security for this construction.

Another thing that would be neat to go over is the notion of deniable authenticated KEMs, wherein it's possible to forge ciphertexts which claim to have been sent by another person to yourself. This makes it so that KEM ciphertexts can't be used as evidence of communication between two parties.

This also butts heads against the notion of insider security, which Yolan Romailler brought to my attention on twitter. See also his blog post touching on ephemeral keys and insider security in key exchanges.

References

[^[ABHKLR20]] [^[GHP18]]

[^[ABHKLR20]]: [ABHKLR20] Analysing the HPKE standard - Joël Alwen, Bruno Blanchet, Eduard Hauck, Eike Kiltz, Benjamin Lipp, Doreen Riepel [^[GHP18]]: [GHP18] KEM Combiners - Federico Giacon, Felix Heuer, Bertram Poettering