@@ -18,18 +18,14 @@ ML-KEM implementation: [ML-KEM](https://github.com/RustCrypto/KEMs/blob/5a7f3ab7
1818### Vectors and Keys
1919
2020- ` k ` : determines the module dimension (k=2 for ML-KEM-512, k=3 for ML-KEM-768, k=4 for ML-KEM-1024)
21- - ` NttVector<k> ` : a vector of ` k ` ` NttPolynomial ` s in ` T_q^k `
21+ - ` NttVector ` : a vector of ` k ` ` NttPolynomial ` s, i.e ` T_q^k `
2222
23- An ML-KEM encapsulation key (public key) consists of two parts:
24-
25- ```
26- ek = (t_hat, rho)
27- ```
28-
29- where:
30- - ` t_hat ` is an ` NttVector<k> ` : the public key vector in NTT domain (` t_hat = A_hat * s + e ` in NTT form)
23+ An ML-KEM encapsulation key is represented as ` EncapsulationKey(t_hat, rho) ` where:
24+ - ` t_hat ` is in ` T_q^k ` , i.e. an ` NttVector ` — the public key vector in NTT domain (` t_hat = A_hat * s + e ` in NTT form)
3125- ` rho ` is a 32-byte seed used to derive the public matrix ` A_hat `
3226
27+ We write ` ek.t_hat ` and ` ek.rho ` to refer to the two components.
28+
3329Note that the ML-KEM encapsulation key is the same as the K-PKE encryption key (FIPS 203, Section 5).
3430
3531The serialized form is:
@@ -40,80 +36,76 @@ ek_bytes = ByteEncode_12(t_hat) || rho
4036
4137where ` ByteEncode_12 ` encodes each of the ` 256*k ` coefficients using 12 bits (FIPS 203, Algorithm 5 ByteEncode_d).
4238
43- We write ` ek.t_hat ` and ` ek.rho ` to refer to the two components of an encapsulation key.
44-
4539A decapsulation key ` dk ` contains the secret vector ` s ` and some additional data (FIPS 203, Algorithm 16 KeyGen_internal).
4640
4741### Operations
4842
49- - ` + ` and ` - ` on ` NttVector<k> ` : component-wise addition and subtraction in ` T_q^k `
50- - ` SampleNTT(B) ` : Algorithm 7 from FIPS 203. Reads from a byte stream ` B ` and produces a pseudorandom element in ` T_q ` , i.e. an ` NttPolynomial `
43+ - ` + ` and ` - ` on ` NttVector ` : component-wise addition and subtraction in ` T_q^k `
44+ - ` EncapsulationKey +/- NttVector -> EncapsulationKey ` : operates on the ` t_hat ` component only, ` rho ` is preserved from the ` EncapsulationKey `
5145
5246### Helper Functions
5347
54- ** ` SampleNTTVector(seed, rho) -> (t_hat, rho) ` **
48+ ** ` sample_ntt_poly(xof) -> NttPolynomial ` **
49+
50+ Algorithm 7 from FIPS 203. Reads bytes from a XOF and produces a pseudorandom element in ` T_q ` .
51+
52+ ** ` sample_ntt_vector(seed) -> NttVector ` **
5553
56- Our helper (not from FIPS 203 or MR19). Produces a pseudorandom ` NttVector<k> ` from a 32-byte
57- ` seed ` by calling ` SampleNTT ` (FIPS 203, Algorithm 7) ` k ` times, once per polynomial. Each
58- ` SampleNTT ` call produces a pseudorandom element of ` T_q ` . The resulting ` NttVector<k> `
54+ Our helper (not from FIPS 203 or MR19). Produces a pseudorandom ` NttVector ` from a 32-byte
55+ ` seed ` by calling ` sample_ntt_poly ` (FIPS 203, Algorithm 7) ` k ` times, once per polynomial. Each
56+ ` sample_ntt_poly ` call produces a pseudorandom element of ` T_q ` . The resulting ` NttVector `
5957is indistinguishable from a real ` t_hat ` , since a real ` t_hat = A_hat * s + e ` is computationally
6058indistinguishable from a pseudorandom vector in ` T_q^k ` .
6159
6260```
6361for j in 0..k:
64- t_hat[j] = SampleNTT(seed || j || 0) // 34 bytes: 32-byte seed + 2 index bytes
62+ x = xof(seed || 0 || j) // 34 bytes: 32-byte seed + 2 index bytes
63+ t_hat[j] = sample_ntt_poly(x)
6564```
6665
67- Each call uses different index bytes ` (j, 0 ) ` in FIPS 203 Algorithm 7 for domain separation .
66+ Each call uses different index bytes ` (0, j ) ` for different XOF stream .
6867In libOTe, this corresponds to ` randomPK ` , where it instead generates ` A_hat ` and takes the first row from it.
6968
70- Output: ` (t_hat, rho) ` . The ` rho ` is passed through unchanged.
69+ ** ` hash_ek(ek) -> NttVector ` **
7170
72- ** ` HashEK(ek) -> (h, ek.rho) ` **
71+ Corresponds to libOTe's ` pkHash ` . Hashes ` ek.t_hat ` to a 32-byte seed and then samples a new ` NttVector ` from it.
7372
74- HashEK corresponds to libOTe's ` pkHash ` . Maps an encapsulation key to another
75- encapsulation key. Takes an element of ` T_q^k ` , hashes it to a 32-byte seed, and uses that seed to sample a new element of ` T_q^k ` .
76-
77- Given an encapsulation key ` ek = (t_hat, rho) ` :
73+ Given an ` EncapsulationKey ` ` ek ` :
7874
7975```
80- seed = SHA3-256 (ByteEncode_12(ek.t_hat)) // hash only the t_hat bytes, not rho
81- h = SampleNTTVector (seed, ek.rho ) // sample a new NttVector<k> from the seed
76+ seed = sha3_256 (ByteEncode_12(ek.t_hat)) // hash only the t_hat bytes, not rho
77+ h = sample_ntt_vector (seed) // sample a new NttVector from the seed
8278```
8379
84- Output: ` (h, ek.rho) ` where ` h ` is an ` NttVector<k> ` in ` T_q^k ` .
80+ Output: ` h ` .
8581
86- ** ` RandomEK (rng, rho) -> (t_hat, rho) ` **
82+ ** ` random_ek (rng, rho) -> EncapsulationKey ` **
8783
88- Generate a random encapsulation key. A 32-byte ` seed ` is sampled from ` rng ` :
84+ Generate a random encapsulation key. A 32-byte random ` seed ` is sampled from a cryptographically secure random number generator ` rng ` :
8985
90- Output: ` SampleNTTVector( seed, rho)`
86+ Output: ` EncapsulationKey(sample_ntt_vector( seed) , rho)`
9187
92- This is identical to ` HashEK ` except the seed is random rather than derived from a hash.
88+ Sampling is identical to the one on ` hash_ek ` , except that the seed is random rather than derived from a hash.
9389
9490## Protocol
9591
96- ** Convention:** All arithmetic (` + ` , ` - ` ) in the protocol steps operates on the ` t_hat `
97- component only. The ` rho ` component is always the same across all keys in a single OT
98- (taken from the real key generated in step 1) and is carried along unchanged.
99-
10092### Receiver (choice bit ` b ` )
10193
102941 . ** Generate real keypair:**
10395 ```
10496 (dk, ek) = ML-KEM.KeyGen()
10597 ```
106- where ` ek = (t_hat, rho) ` .
98+ where ` ek ` is an ` EncapsulationKey ` .
10799
1081002 . ** Sample random key for position ` 1-b ` :**
109101 ```
110- r_{1-b} = RandomEK (rng, ek.rho)
102+ r_{1-b} = random_ek (rng, ek.rho)
111103 ```
112104 where ` rng ` is a cryptographically secure random number generator.
113105
114- 3 . ** Compute the correlated key for position ` b ` :**
106+ 3 . ** Compute the real key for position ` b ` :**
115107 ```
116- r_b = ek - HashEK (r_{1-b})
108+ r_b = ek - hash_ek (r_{1-b})
117109 ```
118110
1191114 . ** Send to sender:**
@@ -128,7 +120,7 @@ component only. The `rho` component is always the same across all keys in a sing
128120
1291216 . ** For each ` j in {0, 1} ` , reconstruct the encapsulation key:**
130122 ```
131- ek_j = r_j + HashEK (r_{1-j})
123+ ek_j = r_j + hash_ek (r_{1-j})
132124 ```
133125
1341267 . ** Encapsulate to both reconstructed keys:**
@@ -172,33 +164,32 @@ component only. The `rho` component is always the same across all keys in a sing
172164
173165**Correctness:**
174166
175- For the chosen side `b`, the sender reconstructs in step 6:
167+ For the chosen side `b`, the sender reconstructs in step 6 by expanding `r_b` :
176168```
177- ek_b = r_b + HashEK (r_ {1-b})
178- = (ek - HashEK (r_ {1-b})) + HashEK (r_ {1-b})
169+ ek_b = r_b + hash_ek (r_ {1-b})
170+ = (ek - hash_ek (r_ {1-b})) + hash_ek (r_ {1-b})
179171 = ek
180172```
181- So `ek_b = ek`, the real public key. In step 10, the receiver calls `ML-KEM.Decaps(dk, ct_b)` and
173+ So `ek_b = ek`, the real encapsulation key. In step 10, the receiver calls `ML-KEM.Decaps(dk, ct_b)` and
182174recovers the same shared secret `ss_b` that the sender computed via `ML-KEM.Encaps(ek_b)` in step 7.
183175
184176**Security:**
185177
186178For the other side `1-b`, the sender reconstructs in step 6:
187179```
188- ek_ {1-b} = r_ {1-b} + HashEK (r_b)
180+ ek_ {1-b} = r_ {1-b} + hash_ek (r_b)
189181```
190182
191183Expanding `r_b` (from step 3):
192184```
193- ek_ {1-b} = r_ {1-b} + HashEK (ek - HashEK (r_ {1-b}))
185+ ek_ {1-b} = r_ {1-b} + hash_ek (ek - hash_ek (r_ {1-b}))
194186```
195187
196- This does NOT simplify — `HashEK ` is a hash function, so `HashEK (ek - HashEK (r_{1-b}))` does not
197- cancel with `HashEK(r_{1-b})` . The result `ek_{1-b}` is an unrelated key for which the
198- receiver does not have a decapsulation key `dk`, so they cannot decapsulate `ct_{1-b}`.
188+ This does not simplify — `hash_ek ` is a hash function, so the nested `hash_ek (ek - hash_ek (r_{1-b}))`
189+ cannot be reduced . The result `ek_{1-b}` is an unrelated key for which the receiver does not
190+ have a decapsulation key `dk`, so they cannot decapsulate `ct_{1-b}`.
199191
200- The choice bit `b` is hidden because `r_b = ek - HashEK(r_{1-b})`. Since `ek` is
201- indistinguishable from uniform under the MLWE assumption, and `HashEK(r_{1-b})` is determined by the
202- already-public `r_{1-b}`, subtracting it from a uniform value still yields a uniform
203- value. So both `r_0` and `r_1` appear uniform to the sender — neither reveals which
204- is the real key.
192+ The choice bit `b` is hidden from the sender. The sender reconstructs both `ek_0` and `ek_1` in
193+ step 6, but under the MLWE assumption, a real encapsulation key is indistinguishable from a random
194+ one. Since both reconstructed keys appear as valid encapsulation keys, the sender cannot determine
195+ which one has a corresponding decapsulation key `dk`.
0 commit comments