You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: cryprot-ot/docs/mlkem-ot-protocol.md
+56-29Lines changed: 56 additions & 29 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -95,6 +95,10 @@ This is identical to `H` except the seed is random rather than derived from a ha
95
95
96
96
## Protocol
97
97
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
+
98
102
### Receiver (choice bit `b`)
99
103
100
104
1.**Generate real keypair:**
@@ -111,8 +115,7 @@ This is identical to `H` except the seed is random rather than derived from a ha
111
115
112
116
3.**Compute the correlated key for position `b`:**
113
117
```
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})
116
119
```
117
120
118
121
4.**Send to sender:**
@@ -123,56 +126,80 @@ This is identical to `H` except the seed is random rather than derived from a ha
123
126
124
127
### Sender
125
128
126
-
1.**Receive `(r_0, r_1)` from the receiver.**
129
+
5.**Receive `(r_0, r_1)` from the receiver**
127
130
128
-
2.**For each `j in {0, 1}`, reconstruct the encapsulation key:**
131
+
6.**For each `j in {0, 1}`, reconstruct the encapsulation key:**
129
132
```
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})
132
134
```
133
135
134
-
3.**Encapsulate to both reconstructed keys:**
136
+
7.**Encapsulate to both reconstructed keys:**
135
137
```
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)
138
140
```
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**
139
144
140
-
4.**Derive OT keys:**
145
+
Hashing each shared secret down to a 128-bit `Block`:
141
146
```
142
-
key_j = RO(domain_sep || k_j || j)
147
+
key_j = RO(domain_sep || ss_j || i)
143
148
```
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.
144
152
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:**
146
156
```
147
157
Sender -> Receiver: (ct_0, ct_1)
148
158
```
149
159
150
160
### Receiver (continued)
151
161
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
+
```
156
166
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.
161
172
162
173
## Why This Works
163
174
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:
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
191
172
-
**Security:** For the other side `1-b`, the sender reconstructs:
192
+
Expanding `r_b` (from step 3):
173
193
```
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}))
175
195
```
176
-
The receiver does not have the secret key for `pk_{1-b}`, so they cannot decapsulate `ct_{1-b}`.
177
196
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
0 commit comments