Skip to content

Commit f35510f

Browse files
committed
add an overview of approach (still work in progress)
1 parent a7dc9c7 commit f35510f

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# ML-KEM OT Protocol
2+
3+
Based on MR19 (Masny-Rindal, ePrint 2019/706), Figure 8, instantiated with ML-KEM per Section D.3.
4+
5+
Reference implementation: [libOTe KyberOT]([](https://github.com/osu-crypto/libOTe/blob/d0e499206d1d4d16c6b4ca6c0e712490e0632f80/thirdparty/KyberOT/KyberOT.c#L40-L41)).
6+
7+
ML-KEM implementation: [ML-KEM]([](https://github.com/RustCrypto/KEMs/blob/5a7f3ab7af5420cacca9befc9212532e4c7f6ca1/ml-kem/src/)).
8+
9+
## Notation
10+
11+
### Field and Ring
12+
13+
- `q = 3329` (ML-KEM prime)
14+
- `Z_q = {0, 1, ..., 3328}` (integers mod q)
15+
- `R_q = Z_q[X] / (X^256 + 1)` (polynomial ring; each element has 256 coefficients in Z_q)
16+
- `T_q`: the NTT domain representation of `R_q` (256 elements of Z_q, stored as `NttPolynomial`)
17+
18+
### Vectors and Keys
19+
20+
- `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`
22+
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)
31+
- `rho` is a 32-byte seed used to derive the public matrix `A_hat`
32+
33+
Note that the ML-KEM encapsulation key is the same as the K-PKE encryption key in our simplified outline above.
34+
35+
The serialized form is:
36+
37+
```
38+
ek_bytes = ByteEncode_12(t_hat) || rho
39+
```
40+
41+
where `ByteEncode_12` encodes each of the `256*k` coefficients using 12 bits (FIPS 203, Algorithm 5 ByteEncode_d).
42+
43+
We write `ek.t_hat` and `ek.rho` to refer to the two components of an encapsulation key.
44+
45+
A decapsulation key `dk` contains the secret vector `s` and some additional data (FIPS 203, Algorithm 16 KeyGen_internal).
46+
47+
### Operations
48+
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`
51+
52+
### Helper Functions
53+
54+
**`SampleNTTVector(seed, rho) -> (t_hat, rho)`**
55+
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>`
59+
is indistinguishable from a real `t_hat`, since a real `t_hat = A_hat * s + e` is computationally
60+
indistinguishable from a pseudorandom vector in `T_q^k`.
61+
62+
```
63+
for j in 0..k:
64+
t_hat[j] = SampleNTT(seed || j || 0) // 34 bytes: 32-byte seed + 2 index bytes
65+
```
66+
67+
Each call uses different index bytes `(j, 0)` in FIPS 203 Algorithm 7 for domain separation.
68+
In libOTe, this corresponds to `randomPK`, where it instead generates `A_hat` and takes a single
69+
row or column from it.
70+
71+
Output: `(t_hat, rho)`. The `rho` is passed through unchanged.
72+
73+
**`H(ek) -> (h, ek.rho)`**
74+
75+
Hash-to-key (corresponds to libOTe's `pkHash`). Maps an encapsulation key to another
76+
encapsulation key. Takes an element of `T_q^k`, hashes it
77+
to a 32-byte seed, and uses that seed to sample a new element of `T_q^k`.
78+
79+
Given an encapsulation key `ek = (t_hat, rho)`:
80+
81+
```
82+
seed = SHA3-256(ByteEncode_12(ek.t_hat)) // hash only the t_hat bytes, not rho
83+
h = SampleNTTVector(seed, ek.rho) // sample a new NttVector<k> from the seed
84+
```
85+
86+
Output: `(h, ek.rho)` where `h` is an `NttVector<k>` in `T_q^k`.
87+
88+
**`RandomEK(seed, rho) -> (r_hat, rho)`**
89+
90+
Generate a random encapsulation key from the given random 32 byte `seed` and `rho`:
91+
92+
Output: `SampleNTTVector(seed, rho)`
93+
94+
This is identical to `H` except the seed is random rather than derived from a hash.
95+
96+
## Protocol
97+
98+
### Receiver (choice bit `b`)
99+
100+
1. **Generate real keypair:**
101+
```
102+
(dk, ek) = ML-KEM.KeyGen()
103+
```
104+
where `ek = (t_hat, rho)`.
105+
106+
2. **Sample random key for position `1-b`:**
107+
```
108+
seed = 32 random bytes
109+
r_{1-b} = RandomEK(seed, ek.rho)
110+
```
111+
112+
3. **Compute the correlated key for position `b`:**
113+
```
114+
r_b.t_hat = ek.t_hat - H(r_{1-b}).t_hat
115+
r_b.rho = ek.rho
116+
```
117+
118+
4. **Send to sender:**
119+
```
120+
Receiver -> Sender: (r_0, r_1)
121+
```
122+
Each serialized as `ByteEncode_12(r_j.t_hat) || rho`.
123+
124+
### Sender
125+
126+
1. **Receive `(r_0, r_1)` from the receiver.**
127+
128+
2. **For each `j in {0, 1}`, reconstruct the encapsulation key:**
129+
```
130+
pk_j.t_hat = r_j.t_hat + H(r_{1-j}).t_hat
131+
pk_j.rho = rho // same rho from both r_0 and r_1
132+
```
133+
134+
3. **Encapsulate to both reconstructed keys:**
135+
```
136+
(ct_0, k_0) = ML-KEM.Encaps(pk_0)
137+
(ct_1, k_1) = ML-KEM.Encaps(pk_1)
138+
```
139+
140+
4. **Derive OT keys:**
141+
```
142+
key_j = RO(domain_sep || k_j || j)
143+
```
144+
145+
5. **Send to receiver:**
146+
```
147+
Sender -> Receiver: (ct_0, ct_1)
148+
```
149+
150+
### Receiver (continued)
151+
152+
6. **Decapsulate the chosen ciphertext:**
153+
```
154+
k_b = ML-KEM.Decaps(dk, ct_b)
155+
```
156+
157+
7. **Derive OT key:**
158+
```
159+
key_b = RO(domain_sep || k_b || b)
160+
```
161+
162+
## Why This Works
163+
164+
**Correctness:** For the chosen side `b`, the sender reconstructs:
165+
```
166+
pk_b.t_hat = r_b.t_hat + H(r_{1-b}).t_hat
167+
= (ek.t_hat - H(r_{1-b}).t_hat) + H(r_{1-b}).t_hat
168+
= ek.t_hat
169+
```
170+
So `pk_b = ek`, the real public key. `ML-KEM.Decaps(dk, ct_b)` recovers the same shared key `k_b` that the sender computed via `ML-KEM.Encaps(pk_b)`.
171+
172+
**Security:** For the other side `1-b`, the sender reconstructs:
173+
```
174+
pk_{1-b}.t_hat = r_{1-b}.t_hat + H(r_b).t_hat
175+
```
176+
The receiver does not have the secret key for `pk_{1-b}`, so they cannot decapsulate `ct_{1-b}`.
177+
178+
The choice bit `b` is hidden because `r_b.t_hat = ek.t_hat - H(r_{1-b}).t_hat`. Since `ek.t_hat` is indistinguishable from uniform under MLWE, `r_b` looks like a random key regardless of `b`. Both `r_0` and `r_1` appear uniform to the sender.

0 commit comments

Comments
 (0)