Skip to content

Commit dc4561f

Browse files
committed
simplify and clear up mlkem-ot-protocol.md
1 parent f35510f commit dc4561f

File tree

1 file changed

+56
-29
lines changed

1 file changed

+56
-29
lines changed

cryprot-ot/docs/mlkem-ot-protocol.md

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ This is identical to `H` except the seed is random rather than derived from a ha
9595

9696
## Protocol
9797

98+
**Convention:** All arithmetic (`+`, `-`) in the protocol steps operates on the `t_hat`
99+
component only. The `rho` component is always the same across all keys in a single OT
100+
(taken from the real key generated in step 1) and is carried along unchanged.
101+
98102
### Receiver (choice bit `b`)
99103

100104
1. **Generate real keypair:**
@@ -111,8 +115,7 @@ This is identical to `H` except the seed is random rather than derived from a ha
111115

112116
3. **Compute the correlated key for position `b`:**
113117
```
114-
r_b.t_hat = ek.t_hat - H(r_{1-b}).t_hat
115-
r_b.rho = ek.rho
118+
r_b = ek - H(r_{1-b})
116119
```
117120

118121
4. **Send to sender:**
@@ -123,56 +126,80 @@ This is identical to `H` except the seed is random rather than derived from a ha
123126

124127
### Sender
125128

126-
1. **Receive `(r_0, r_1)` from the receiver.**
129+
5. **Receive `(r_0, r_1)` from the receiver**
127130

128-
2. **For each `j in {0, 1}`, reconstruct the encapsulation key:**
131+
6. **For each `j in {0, 1}`, reconstruct the encapsulation key:**
129132
```
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
133+
ek_j = r_j + H(r_{1-j})
132134
```
133135

134-
3. **Encapsulate to both reconstructed keys:**
136+
7. **Encapsulate to both reconstructed keys:**
135137
```
136-
(ct_0, k_0) = ML-KEM.Encaps(pk_0)
137-
(ct_1, k_1) = ML-KEM.Encaps(pk_1)
138+
(ct_0, ss_0) = ML-KEM.Encaps(ek_0)
139+
(ct_1, ss_1) = ML-KEM.Encaps(ek_1)
138140
```
141+
Each `ss_j` is a 32-byte ML-KEM shared secret. Each `ct_j` is an ML-KEM ciphertext.
142+
143+
8. **Derive OT output keys**
139144

140-
4. **Derive OT keys:**
145+
Hashing each shared secret down to a 128-bit `Block`:
141146
```
142-
key_j = RO(domain_sep || k_j || j)
147+
key_j = RO(domain_sep || ss_j || i)
143148
```
149+
`RO` is a random oracle (instantiated as a hash function), `domain_sep` is a fixed
150+
byte string for domain separation, and `i` is a tweak (the OT batch index) ensuring
151+
that different OTs in a batch produce independent keys.
144152

145-
5. **Send to receiver:**
153+
The sender stores `ots[i] = [key_0, key_1]` — both OT output keys.
154+
155+
9. **Send to receiver:**
146156
```
147157
Sender -> Receiver: (ct_0, ct_1)
148158
```
149159

150160
### Receiver (continued)
151161

152-
6. **Decapsulate the chosen ciphertext:**
153-
```
154-
k_b = ML-KEM.Decaps(dk, ct_b)
155-
```
162+
10. **Decapsulate the chosen ciphertext:**
163+
```
164+
ss_b = ML-KEM.Decaps(dk, ct_b)
165+
```
156166
157-
7. **Derive OT key:**
158-
```
159-
key_b = RO(domain_sep || k_b || b)
160-
```
167+
11. **Derive OT key:**
168+
```
169+
key_b = RO(domain_sep || ss_b || i)
170+
```
171+
The receiver stores `ots[i] = key_b` — one OT output key for OT `i` batch in the batch.
161172
162173
## Why This Works
163174
164-
**Correctness:** For the chosen side `b`, the sender reconstructs:
175+
**Correctness:**
176+
177+
For the chosen side `b`, the sender reconstructs in step 6:
178+
```
179+
ek_b = r_b + H(r_{1-b})
180+
= (ek - H(r_{1-b})) + H(r_{1-b})
181+
= ek
182+
```
183+
So `ek_b = ek`, the real public key. In step 10, the receiver calls `ML-KEM.Decaps(dk, ct_b)` and recovers the same shared secret `ss_b` that the sender computed via `ML-KEM.Encaps(ek_b)` in step 7.
184+
185+
**Security:**
186+
187+
For the other side `1-b`, the sender reconstructs in step 6:
165188
```
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
189+
ek_{1-b} = r_{1-b} + H(r_b)
169190
```
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)`.
171191
172-
**Security:** For the other side `1-b`, the sender reconstructs:
192+
Expanding `r_b` (from step 3):
173193
```
174-
pk_{1-b}.t_hat = r_{1-b}.t_hat + H(r_b).t_hat
194+
ek_{1-b} = r_{1-b} + H(ek - H(r_{1-b}))
175195
```
176-
The receiver does not have the secret key for `pk_{1-b}`, so they cannot decapsulate `ct_{1-b}`.
177196
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.
197+
This does NOT simplify — `H` is a hash function, so `H(ek - H(r_{1-b}))` does not
198+
cancel with `H(r_{1-b})`. The result `ek_{1-b}` is an unrelated key for which the
199+
receiver does not have a decapsulation key `dk`, so they cannot decapsulate `ct_{1-b}`.
200+
201+
The choice bit `b` is hidden because `r_b = ek - H(r_{1-b})`. Since `ek` is
202+
indistinguishable from uniform under MLWE, and `H(r_{1-b})` is determined by the
203+
already-public `r_{1-b}`, subtracting it from a uniform value still yields a uniform
204+
value. So both `r_0` and `r_1` appear uniform to the sender — neither reveals which
205+
is the real key.

0 commit comments

Comments
 (0)