mpc_engine/primitives/ot.rs
1//! This module implements "The Simplest Protocol for Oblivious Transfer" due to
2//! Orlandi and Chou.
3//! (cf. https://eprint.iacr.org/archive/2015/267/1527602042.pdf)
4//!
5//! The protocol works as follows in an elliptic curve group G with base point `B` and scalars `Scalars`
6//!
7//! ```text
8//! Sender(l, r) Receiver(c)
9//! y <-$ Scalars
10//! S := yB
11//! T := yS -- S --> x <-$ Scalars
12//! R := cS + xB
13//! <-- R --
14//! k_l k = H(S, R, xS)
15//! = H(S, R, yR)
16//! k_r
17//! = H(S, R, yR - T)
18//!
19//! c_l = E(k_l, l)
20//! c_r = E(k_r, r)
21//!
22//! -- c_l -->
23//! -- c_r --> output = D(k, c_l) if decryption successful
24//! otherwise output = D(k, c_r)
25//! ```
26//! We instantiate the primitives as follows:
27//! - H: HKDF(SHA-256)
28//! - group G: P256
29//! - Encryption scheme: Chacha20Poly1305
30
31use hacspec_chacha20poly1305::{ChaChaPolyIV, ChaChaPolyKey};
32use hacspec_lib::Randomness;
33use p256::{p256_point_mul, p256_point_mul_base, P256Point};
34
35use crate::Error;
36
37/// The state of the sender
38pub struct OTSender {
39 y: p256::P256Scalar,
40 s: p256::P256Point,
41 t: p256::P256Point,
42 dst: Vec<u8>,
43}
44
45/// The state of the receiver
46pub struct OTReceiver {
47 x: p256::P256Scalar,
48 r: p256::P256Point,
49 s: P256Point,
50 dst: Vec<u8>,
51}
52
53/// The OT sender's first message.
54#[derive(Debug)]
55pub struct OTSenderInit(p256::P256Point);
56
57/// The OT receiver's first message.
58#[derive(Debug)]
59pub struct OTReceiverSelect(p256::P256Point);
60
61/// The encryption of an OT input.
62#[derive(Debug)]
63pub struct OTCiphertext {
64 iv: ChaChaPolyIV,
65 ciphertext: Vec<u8>,
66 tag: [u8; 16],
67}
68/// The OT sender's second message.
69#[derive(Debug)]
70pub struct OTSenderSend {
71 left: OTCiphertext,
72 right: OTCiphertext,
73}
74
75impl OTSender {
76 /// Generate the first sender message.
77 ///
78 /// Initiates an OT sender by picking a random P256 scalar `y` and deriving
79 /// `S = yB` and `T = yS`, where `B` is the P256 base point. These values
80 /// will later be used to derive encryption keys in the send stage of the
81 /// protocol. In addition, the domain separation tag `dst`, which will be
82 /// used in key generation is stored in the receiver and `S` is prepared for
83 /// sending to the receiver by wrapping it in an `OTSenderInit` message.
84 pub fn init(entropy: &mut Randomness, dst: &[u8]) -> Result<(Self, OTSenderInit), Error> {
85 let y = p256::random_scalar(entropy, dst)?;
86
87 let s = p256::p256_point_mul_base(y)?;
88 let t = p256_point_mul(y, s)?;
89
90 Ok((
91 OTSender {
92 y,
93 s,
94 t,
95 dst: dst.to_vec(),
96 },
97 OTSenderInit(s),
98 ))
99 }
100
101 /// Generate the second sender message based on the receiver's selection.
102 ///
103 /// Given the `OTReceiverSelect` message and the two sender inputs, the
104 /// sender can generate the transfer messages. It does so by deriving two
105 /// domain separated encryption keys, based on the values `S` and `T`
106 /// generated during initiation and on the masked choice bit sent by the
107 /// receiver. It then encrypts the left and right inputs under their
108 /// respective keys and prepares an `OTSenderSend` message with both
109 /// ciphertexts. This finishes the OT session for the sender. By the
110 /// security of the protocol, the receiver will only be able to generate one
111 /// of the decryption keys, namely that one corresponding to its choice bit.
112 pub fn send(
113 &self,
114 left_input: &[u8],
115 right_input: &[u8],
116 selection: &OTReceiverSelect,
117 entropy: &mut Randomness,
118 ) -> Result<OTSenderSend, Error> {
119 debug_assert_eq!(
120 left_input.len(),
121 right_input.len(),
122 "Left and right inputs to the OT must be of the same length."
123 );
124 let OTReceiverSelect(r) = selection;
125
126 let (left_key, right_key) = self.derive_keys(r)?;
127
128 let (left, right) = encrypt_inputs(entropy, left_key, left_input, right_key, right_input);
129
130 Ok(OTSenderSend { left, right })
131 }
132
133 /// Derive a pair of encryption keys for creating the `OTSenderSend`
134 /// message.
135 ///
136 /// Given the points `S`,`T` from the sender initialization, as well as the
137 /// masked receiver choice `R`, a pre-key is generated based on `S` and `R`.
138 /// Then the keys are generated by first extracting
139 /// `HKDF-SHA256-extract(pre-key||yR)` for the right input and
140 /// `HKDF-SHA256-extract(pre-key||yR - T)` for the left input where the
141 /// elliptic curve points are serialized and concatenated to the pre-key and
142 /// a fixed salt value is used to extract. From this, two 32-byte keys are
143 /// expanded for use in ChaCha20Poly1305.
144 fn derive_keys(
145 &self,
146 receiver_selection: &p256::P256Point,
147 ) -> Result<(ChaChaPolyKey, ChaChaPolyKey), Error> {
148 let (salt, ikm) = derive_prk(&self.s, receiver_selection);
149
150 let input_right = p256_point_mul(self.y, *receiver_selection)?;
151
152 let input_left = p256::point_add(input_right, -self.t)?;
153
154 let input_left_serialized = p256::serialize_point(&input_left);
155 let input_right_serialized = p256::serialize_point(&input_right);
156
157 let mut ikm_left = ikm.clone();
158 let mut ikm_right = ikm;
159
160 ikm_left.extend_from_slice(&input_left_serialized);
161 ikm_right.extend_from_slice(&input_right_serialized);
162
163 let prk_left = hmac::hkdf_extract(&salt, &ikm_left);
164 let prk_right = hmac::hkdf_extract(&salt, &ikm_right);
165 Ok((
166 hmac::hkdf_expand(&prk_left, &self.dst, 32)
167 .try_into()
168 .expect(
169 "should have received the right number of bytes, because we requested them",
170 ),
171 hmac::hkdf_expand(&prk_right, &self.dst, 32)
172 .try_into()
173 .expect(
174 "should have received the right number of bytes, because we requested them",
175 ),
176 ))
177 }
178}
179
180/// Create the pre-key for encrypting and decrypting `OTSenderSend` ciphertexts.
181///
182/// Based on points `S` and `R`, use `HKDF-SHA256-extract(S||R)` to generate a
183/// pre-key with a fixed salt value by serializing and concatenating the points.
184fn derive_prk(
185 sender_commitment: &p256::P256Point,
186 receiver_selection: &p256::P256Point,
187) -> (Vec<u8>, Vec<u8>) {
188 let serialized_s = p256::serialize_point(sender_commitment);
189 let serialized_r = p256::serialize_point(receiver_selection);
190 let salt = b"no-salt";
191 let mut ikm = Vec::from(serialized_s);
192 ikm.extend_from_slice(&serialized_r);
193 (salt.to_vec(), ikm)
194}
195
196/// Encrypt the OT sender inputs.
197///
198/// Using the keys generated by `derive_keys()` encrypt the sender's inputs with
199/// Chacha20Poly1305, without any additional authenticated data.
200fn encrypt_inputs(
201 entropy: &mut Randomness,
202 left_key: [u8; 32],
203 left_input: &[u8],
204 right_key: [u8; 32],
205 right_input: &[u8],
206) -> (OTCiphertext, OTCiphertext) {
207 let left_iv = entropy
208 .bytes(12)
209 .expect("sufficient randomness should have been provided externally")
210 .try_into()
211 .expect("should have received the right number of bytes, because we requested them");
212
213 let right_iv = entropy
214 .bytes(12)
215 .expect("sufficient randomness should have been provided externally")
216 .try_into()
217 .expect("should have received the right number of bytes, because we requested them");
218
219 let (left_enc, left_tag) =
220 hacspec_chacha20poly1305::chacha20_poly1305_encrypt(left_key, left_iv, &[], left_input);
221 let (right_enc, right_tag) =
222 hacspec_chacha20poly1305::chacha20_poly1305_encrypt(right_key, right_iv, &[], right_input);
223 (
224 OTCiphertext {
225 iv: left_iv,
226 ciphertext: left_enc,
227 tag: left_tag,
228 },
229 OTCiphertext {
230 iv: right_iv,
231 ciphertext: right_enc,
232 tag: right_tag,
233 },
234 )
235}
236
237impl OTReceiver {
238 /// Generate the first receiver message.
239 ///
240 /// Initiates the OT receiver by generating a random P256 Scalar `x` which
241 /// is used to mask the receivers choice bit `c` of output in the
242 /// computation of the masked receiver selection `R = cS + xB`, where `S` is
243 /// obtained from the initial sender message. The values required in the
244 /// decryption of the sender messages are stored in the `OTReceiver` struct
245 /// and the masked choice is wrapped in an `OTReceiverSelect` message for
246 /// sending to the sender.
247 pub fn select(
248 entropy: &mut Randomness,
249 dst: &[u8],
250 sender_message: OTSenderInit,
251 choose_left: bool,
252 ) -> Result<(Self, OTReceiverSelect), Error> {
253 let x = p256::random_scalar(entropy, dst)?;
254 let OTSenderInit(s) = sender_message;
255
256 let mut res = p256_point_mul_base(x)?;
257 let r = if choose_left {
258 res = p256::point_add(res, s)?;
259 res
260 } else {
261 res
262 };
263
264 Ok((
265 OTReceiver {
266 x,
267 r,
268 s,
269 dst: dst.to_vec(),
270 },
271 OTReceiverSelect(r),
272 ))
273 }
274
275 /// Receive the selected input from the sender.
276 ///
277 /// A decryption key is generated based on the initialization message
278 /// received from the sender and the masked choice generated during receiver
279 /// initialization. Then trial-decryption of the sender messages are
280 /// attempted. Correctness of the protocol guarantees that one decryption
281 /// will be successful (exactly one, by security of the protocol) and will
282 /// yield the receiver's chosen output ending the OT session for the
283 /// receiver.
284 pub fn receive(&self, sender_message: OTSenderSend) -> Result<Vec<u8>, Error> {
285 let key = self.derive_key()?;
286
287 let dec = hacspec_chacha20poly1305::chacha20_poly1305_decrypt(
288 key,
289 sender_message.left.iv,
290 &[],
291 &sender_message.left.ciphertext,
292 sender_message.left.tag,
293 )
294 .or_else(|_| {
295 hacspec_chacha20poly1305::chacha20_poly1305_decrypt(
296 key,
297 sender_message.right.iv,
298 &[],
299 &sender_message.right.ciphertext,
300 sender_message.right.tag,
301 )
302 });
303
304 dec.map_err(|e| e.into())
305 }
306
307 /// Derive a decryption key for trial decrypting the `OTSenderSend`
308 /// messages.
309 ///
310 /// Given the point `S` from the initial sender message, as well as the
311 /// scalar `x` generated during receiver initialization and the masked
312 /// receiver choice `R`, a pre-key is generated based on `S` and `R`. Then
313 /// the key is generated by first extracting
314 /// `HKDF-SHA256-extract(pre-key||xS)` where the elliptic curve points are
315 /// serialized and concatenated to the pre-key and a fixed salt value is
316 /// used to extract. From this, a 32-byte key is expanded for use in
317 /// ChaCha20Poly1305.
318 fn derive_key(&self) -> Result<ChaChaPolyKey, Error> {
319 let (salt, mut ikm) = derive_prk(&self.s, &self.r);
320
321 let input = p256_point_mul(self.x, self.s)?;
322 let input_serialized = p256::serialize_point(&input);
323
324 ikm.extend_from_slice(&input_serialized);
325
326 let prk = hmac::hkdf_extract(&salt, &ikm);
327
328 Ok(hmac::hkdf_expand(&prk, &self.dst, 32)
329 .try_into()
330 .expect("should have received the required number of bytes because we requested them"))
331 }
332}
333
334#[test]
335fn simple() {
336 use rand::RngCore;
337 let mut rng = rand::thread_rng();
338 let mut entropy = [0u8; 88];
339 rng.fill_bytes(&mut entropy);
340 let mut entropy = Randomness::new(entropy.to_vec());
341
342 let dst = b"test-context";
343 let left_input = b"lefto";
344 let right_input = b"right";
345 let (sender, commitment) = OTSender::init(&mut entropy, dst).unwrap();
346 let (receiver, selection) = OTReceiver::select(&mut entropy, dst, commitment, false).unwrap();
347
348 let send_message = sender
349 .send(left_input, right_input, &selection, &mut entropy)
350 .unwrap();
351
352 let receiver_output = receiver.receive(send_message).unwrap();
353 debug_assert_eq!(right_input.to_vec(), receiver_output);
354}