diff --git a/Cargo.toml b/Cargo.toml index 906aa894f..639afc192 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ members = [ "utils-aio", "clmul", "matrix-transpose", - "gf2-128" + "share-conversion-core", + "share-conversion-aio" ] exclude = ["tls-client"] diff --git a/gf2-128/src/lib.rs b/gf2-128/src/lib.rs deleted file mode 100644 index 12508f5cf..000000000 --- a/gf2-128/src/lib.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! This subcrate implements secure two-party (2PC) multiplication-to-addition (M2A) and -//! addition-to-multiplication (A2M) algorithms, both with semi-honest security for elements -//! of GF(2^128). -//! -//! ### M2A algorithm -//! Let `A` be an element of some finite field with `A = a * b`, where `a` is only known to Alice -//! and `b` is only known to Bob. A is unknown to both parties and it is their goal that each of -//! them ends up with an additive share of A. So both parties start with `a` and `b` and want to -//! end up with `x` and `y`, where `A = a * b = x + y`. -//! -//! This is an implementation for the extension field GF(2^128), which uses the oblivious transfer -//! method in chapter 4.1 of -//! -//! ### A2M algorithm -//! This is the other way round. -//! Let `A` be an element of some finite field with `A = x + y`, where `x` is only known to Alice -//! and `y` is only known to Bob. A is unknown to both parties and it is their goal that each of -//! them ends up with a multiplicative share of A. So both parties start with `x` and `y` and want to -//! end up with `a` and `b`, where `A = x + y = a * b`. -//! -//! This is an implementation for the extension field GF(2^128), which is a semi-honest adaptation -//! of the "A2M Protocol" in chapter 4 of - -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha12Rng; - -/// Masked values for an oblivious transfer -pub struct MaskedPartialValue(pub [u128; 128], pub [u128; 128]); - -/// A multiplicative share of `A = a * b` -pub struct MulShare(u128); - -impl MulShare { - /// Create a new `MulShare` holding a factor of `A` - pub fn new(share: u128) -> Self { - Self(share) - } - - /// Return inner share - pub fn inner(&self) -> u128 { - self.0 - } - - /// Turn into an additive share and masked partial values - /// - /// This function returns - /// * `AddShare` - The sender's additive share; this is `y` in the paper - /// * `MaskedPartialValue` - Used for oblivious transfer; t0 and t1 in the paper - pub fn to_additive(&self) -> (AddShare, MaskedPartialValue) { - let mut rng = ChaCha12Rng::from_entropy(); - - let t0: [u128; 128] = std::array::from_fn(|_| rng.gen()); - let t1: [u128; 128] = std::array::from_fn(|i| mul(self.inner(), 1 << i) ^ t0[i]); - - let add_share = AddShare::new(t0.into_iter().fold(0, |acc, i| acc ^ i)); - (add_share, MaskedPartialValue(t0, t1)) - } - - /// Create a multiplicative share from the output of an OT - /// - /// The `value` needs to be built by choices of an oblivious transfer - pub fn from_choice(value: [u128; 128]) -> Self { - Self::new(value.into_iter().fold(0, |acc, i| acc ^ i)) - } -} - -/// An additive share of `A = x + y` -pub struct AddShare(u128); - -impl AddShare { - /// Create a new `AddShare` holding a summand of `A` - pub fn new(share: u128) -> Self { - Self(share) - } - - /// Return inner share - pub fn inner(&self) -> u128 { - self.0 - } - - /// Turn into a multiplicative share and masked partial values - /// - /// This function returns - /// * `MulShare` - The sender's multiplicative share - /// * `MaskedPartialValue` - Used for oblivious transfer - pub fn to_multiplicative(&self) -> (MulShare, MaskedPartialValue) { - let mut rng = ChaCha12Rng::from_entropy(); - - let random: u128 = rng.gen(); - if random == 0 { - panic!("Random u128 is 0"); - } - - let mut masks: [u128; 128] = std::array::from_fn(|_| rng.gen()); - // set the last mask such that the sum of all 128 masks equals 0 - masks[127] = masks.into_iter().take(127).fold(0, |acc, i| acc ^ i); - - let mul_share = MulShare::new(inverse(random)); - - // `self.inner() & (1 << i)` extracts bit of `self.inner()` in position `i` (counting from - // the right) shifted left by `i` - let b0: [u128; 128] = - std::array::from_fn(|i| mul(self.inner() & (1 << i), random) ^ masks[i]); - let b1: [u128; 128] = - std::array::from_fn(|i| mul((self.inner() & (1 << i)) ^ (1 << i), random) ^ masks[i]); - - (mul_share, MaskedPartialValue(b0, b1)) - } - - /// Create an additive share from the output of an OT - /// - /// The `value` needs to be built by choices of an oblivious transfer - pub fn from_choice(value: [u128; 128]) -> Self { - Self::new(value.into_iter().fold(0, |acc, i| acc ^ i)) - } -} - -/// R is GCM polynomial in little-endian. In hex: "E1000000000000000000000000000000" -const R: u128 = 299076299051606071403356588563077529600; - -/// Galois field multiplication of two 128-bit blocks reduced by the GCM polynomial -pub fn mul(mut x: u128, y: u128) -> u128 { - let mut result: u128 = 0; - for i in (0..128).rev() { - result ^= x * ((y >> i) & 1); - x = (x >> 1) ^ ((x & 1) * R); - } - result -} - -/// Galois field inversion of 128-bit block -pub fn inverse(mut x: u128) -> u128 { - let one = 1 << 127; - let mut out = one; - - for _ in 0..127 { - x = mul(x, x); - out = mul(out, x); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - use ghash_rc::universal_hash::NewUniversalHash; - use ghash_rc::universal_hash::UniversalHash; - use ghash_rc::GHash; - use rand::{Rng, SeedableRng}; - use rand_chacha::ChaCha12Rng; - - fn ot_mock(envelopes: ([u128; 128], [u128; 128]), choices: u128) -> [u128; 128] { - let mut out = [0_u128; 128]; - for (k, number) in out.iter_mut().enumerate() { - let bit = (choices >> k) & 1; - *number = (bit * envelopes.1[k]) ^ ((bit ^ 1) * envelopes.0[k]); - } - out - } - - #[test] - fn test_m2a() { - let mut rng = ChaCha12Rng::from_entropy(); - let a: MulShare = MulShare::new(rng.gen()); - let b: MulShare = MulShare::new(rng.gen()); - - let (x, MaskedPartialValue(t0, t1)) = a.to_additive(); - - let choice = ot_mock((t0, t1), b.inner()); - let y = AddShare::from_choice(choice); - - assert_eq!(mul(a.inner(), b.inner()), x.inner() ^ y.inner()); - } - - #[test] - fn test_a2m() { - let mut rng = ChaCha12Rng::from_entropy(); - let x: AddShare = AddShare::new(rng.gen()); - let y: AddShare = AddShare::new(rng.gen()); - - let (a, MaskedPartialValue(t0, t1)) = x.to_multiplicative(); - - let choice = ot_mock((t0, t1), y.inner()); - let b = MulShare::from_choice(choice); - - assert_eq!(x.inner() ^ y.inner(), mul(a.inner(), b.inner())); - } - - #[test] - // Test multiplication against RustCrypto - fn test_mul() { - let mut rng = ChaCha12Rng::from_entropy(); - let a: u128 = rng.gen(); - let b: u128 = rng.gen(); - - let mut g = GHash::new(&a.to_be_bytes().into()); - g.update(&b.to_be_bytes().into()); - // Ghash will internally multiply a and b - let expected = g.finalize(); - - assert_eq!( - mul(a, b), - u128::from_be_bytes(expected.into_bytes().try_into().unwrap()) - ); - } - - #[test] - fn test_inverse() { - let mut rng = ChaCha12Rng::from_entropy(); - let a: u128 = rng.gen(); - let inverse_a = inverse(a); - - assert_eq!(mul(a, inverse_a), 1_u128 << 127); - assert_eq!(inverse(1_u128 << 127), 1_u128 << 127); - } -} diff --git a/gf2-128/tests/helper/mod.rs b/gf2-128/tests/helper/mod.rs deleted file mode 100644 index 57b537e5d..000000000 --- a/gf2-128/tests/helper/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use mpc_core::Block; - -pub fn u128_to_bool(a: u128) -> [bool; 128] { - let mut out = [false; 128]; - for (k, item) in out.iter_mut().enumerate() { - *item = (a >> k & 1) == 1 - } - out -} - -pub fn interleave_to_blocks(values: ([u128; 128], [u128; 128])) -> Vec<[Block; 2]> { - let mut out = vec![]; - for (first, second) in values.0.into_iter().zip(values.1.into_iter()) { - out.push([Block::new(first), Block::new(second)]); - } - out -} diff --git a/gf2-128/tests/ot_integration.rs b/gf2-128/tests/ot_integration.rs deleted file mode 100644 index e9abe35b7..000000000 --- a/gf2-128/tests/ot_integration.rs +++ /dev/null @@ -1,97 +0,0 @@ -use gf2_128::{mul, AddShare, MaskedPartialValue, MulShare}; -use mpc_aio::protocol::ot::kos::receiver::Kos15IOReceiver; -use mpc_aio::protocol::ot::kos::sender::Kos15IOSender; -use mpc_aio::protocol::ot::{ObliviousReceive, ObliviousSend}; -use mpc_core::msgs::ot::OTMessage; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha12Rng; -use utils_aio::duplex::DuplexChannel; - -pub mod helper; -use helper::{interleave_to_blocks, u128_to_bool}; - -#[tokio::test] -async fn test_m2a_ot() { - let mut rng = ChaCha12Rng::from_entropy(); - - // Prepare multiplicative shares and encoding - let a: MulShare = MulShare::new(rng.gen()); - let b: MulShare = MulShare::new(rng.gen()); - let (x, MaskedPartialValue(t0, t1)) = a.to_additive(); - - // Prepare inputs/outputs for OT - let choices = u128_to_bool(b.inner()); - let blocks = interleave_to_blocks((t0, t1)); - - //Send via KOS OT - let (channel, channel_2) = DuplexChannel::::new(); - let (sender, receiver) = ( - Kos15IOSender::new(Box::new(channel)), - Kos15IOReceiver::new(Box::new(channel_2)), - ); - let send = tokio::spawn(async { - let mut sender = sender.rand_setup(128).await.unwrap(); - sender.send(blocks).await.unwrap(); - }); - let receive = tokio::spawn(async move { - let mut receiver = receiver.rand_setup(128).await.unwrap(); - receiver.receive(&choices).await.unwrap() - }); - - let (_, output) = tokio::join!(send, receive); - - // Turn output into additive share for receiver - let output: [u128; 128] = output - .unwrap() - .iter() - .map(|block| block.inner()) - .collect::>() - .try_into() - .unwrap(); - let y = AddShare::from_choice(output); - - assert_eq!(mul(a.inner(), b.inner()), x.inner() ^ y.inner()); -} - -#[tokio::test] -async fn test_a2m_ot() { - let mut rng = ChaCha12Rng::from_entropy(); - - // Prepare additive shares and encoding - let x: AddShare = AddShare::new(rng.gen()); - let y: AddShare = AddShare::new(rng.gen()); - let (a, MaskedPartialValue(t0, t1)) = x.to_multiplicative(); - - // Prepare inputs/outputs for OT - let choices = u128_to_bool(y.inner()); - let blocks = interleave_to_blocks((t0, t1)); - - //Send via KOS OT - let (channel, channel_2) = DuplexChannel::::new(); - let (sender, receiver) = ( - Kos15IOSender::new(Box::new(channel)), - Kos15IOReceiver::new(Box::new(channel_2)), - ); - let send = tokio::spawn(async { - let mut sender = sender.rand_setup(128).await.unwrap(); - sender.send(blocks).await.unwrap(); - }); - let receive = tokio::spawn(async move { - let mut receiver = receiver.rand_setup(128).await.unwrap(); - receiver.receive(&choices).await.unwrap() - }); - - let (_, output) = tokio::join!(send, receive); - - // Turn output into multiplicative share for receiver - let output: [u128; 128] = output - .unwrap() - .iter() - .map(|block| block.inner()) - .collect::>() - .try_into() - .unwrap(); - let b = MulShare::from_choice(output); - - assert_eq!(x.inner() ^ y.inner(), mul(a.inner(), b.inner())); -} diff --git a/mpc-aio/src/protocol/ot/mock.rs b/mpc-aio/src/protocol/ot/mock.rs index 8fab4f5b7..d44a0cef8 100644 --- a/mpc-aio/src/protocol/ot/mock.rs +++ b/mpc-aio/src/protocol/ot/mock.rs @@ -1,7 +1,57 @@ -use super::{OTError, ObliviousReceive, ObliviousSend}; +use std::sync::{Arc, Mutex}; + +use super::{ + OTError, OTFactoryError, OTReceiverFactory, OTSenderFactory, ObliviousReceive, ObliviousSend, +}; use async_trait::async_trait; use futures::{channel::mpsc, StreamExt}; +#[derive(Default)] +pub struct MockOTFactory { + waiting_sender: Option>, + waiting_receiver: Option>, +} + +#[async_trait] +impl OTSenderFactory for Arc>> { + type Protocol = MockOTSender; + + async fn new_sender( + &mut self, + _id: String, + _count: usize, + ) -> Result { + let mut inner = self.lock().unwrap(); + if inner.waiting_sender.is_some() { + Ok(inner.waiting_sender.take().unwrap()) + } else { + let (sender, receiver) = mock_ot_pair::(); + inner.waiting_receiver = Some(receiver); + Ok(sender) + } + } +} + +#[async_trait] +impl OTReceiverFactory for Arc>> { + type Protocol = MockOTReceiver; + + async fn new_receiver( + &mut self, + _id: String, + _count: usize, + ) -> Result { + let mut inner = self.lock().unwrap(); + if inner.waiting_receiver.is_some() { + Ok(inner.waiting_receiver.take().unwrap()) + } else { + let (sender, receiver) = mock_ot_pair::(); + inner.waiting_sender = Some(sender); + Ok(receiver) + } + } +} + pub struct MockOTSender { sender: mpsc::Sender>, } diff --git a/mpc-aio/src/protocol/ot/mod.rs b/mpc-aio/src/protocol/ot/mod.rs index 7693fa9f6..4a80996f1 100644 --- a/mpc-aio/src/protocol/ot/mod.rs +++ b/mpc-aio/src/protocol/ot/mod.rs @@ -32,6 +32,22 @@ pub enum OTError { Unexpected(OTMessage), } +#[derive(Debug, thiserror::Error)] +pub enum OTFactoryError { + // #[error("muxer error")] + // MuxerError(#[from] MuxerError), + #[error("ot error")] + OTError(#[from] OTError), + #[error("io error")] + IOError(#[from] std::io::Error), + // #[error("unexpected message")] + // UnexpectedMessage(OTFactoryMessage), + #[error("{0} Sender expects {1} OTs, Receiver expects {2}")] + SplitMismatch(String, usize, usize), + #[error("other: {0}")] + Other(String), +} + #[async_trait] pub trait ObliviousSend { type Inputs; @@ -73,6 +89,36 @@ pub trait ObliviousVerify { async fn verify(self, input: Vec) -> Result<(), OTError>; } +#[async_trait] +pub trait OTSenderFactory { + type Protocol: ObliviousSend + Send; + + /// Constructs a new Sender + /// + /// * `id` - Instance id + /// * `count` - Number of OTs to provision + async fn new_sender( + &mut self, + id: String, + count: usize, + ) -> Result; +} + +#[async_trait] +pub trait OTReceiverFactory { + type Protocol: ObliviousReceive + Send; + + /// Constructs a new Receiver + /// + /// * `id` - Instance id + /// * `count` - Number of OTs to provision + async fn new_receiver( + &mut self, + id: String, + count: usize, + ) -> Result; +} + #[cfg(test)] mockall::mock! { pub ObliviousSender {} diff --git a/share-conversion-aio/Cargo.toml b/share-conversion-aio/Cargo.toml new file mode 100644 index 000000000..cf51c2d83 --- /dev/null +++ b/share-conversion-aio/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "share-conversion-aio" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror.workspace = true +tlsn-mpc-aio = { path = "../mpc-aio" } +tlsn-mpc-core = { path = "../mpc-core" } +async-trait.workspace = true +rand.workspace = true +rand_chacha.workspace = true +share-conversion-core = { path = "../share-conversion-core" } + +[dev-dependencies] +tokio.workspace = true diff --git a/share-conversion-aio/src/gf2_128/mod.rs b/share-conversion-aio/src/gf2_128/mod.rs new file mode 100644 index 000000000..6afab04cf --- /dev/null +++ b/share-conversion-aio/src/gf2_128/mod.rs @@ -0,0 +1,105 @@ +//! This module implements the IO layer of share-conversion for field elements of +//! GF(2^128), using oblivious transfer. + +use share_conversion_core::gf2_128::{AddShare, Gf2_128ShareConvert, MulShare, OTEnvelope}; + +mod receiver; +mod sender; + +pub use receiver::Receiver; +pub use sender::Sender; + +#[cfg(test)] +mod tests { + use std::sync::{Arc, Mutex}; + + use super::*; + use mpc_aio::protocol::ot::mock::MockOTFactory; + use mpc_core::Block; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha12Rng; + use share_conversion_core::gf2_128::mul; + + #[tokio::test] + async fn test_aio_a2m() { + let (mut sender, mut receiver) = mock_converter_pair(); + + // Create some random numbers + let random_numbers_1: Vec = get_random_gf2_128_vec(128); + let random_numbers_2: Vec = get_random_gf2_128_vec(128); + let random_numbers: Vec = + std::iter::zip(random_numbers_1.iter(), random_numbers_2.iter()) + .map(|(a, b)| a ^ b) + .collect(); + + // Spawn tokio tasks and wait for them to finish + let sender_task = tokio::spawn(async move { + sender + .convert_from::(&random_numbers_1) + .await + .unwrap() + }); + let receiver_task = tokio::spawn(async move { + receiver + .convert_from::(&random_numbers_2) + .await + .unwrap() + }); + let (sender_output, receiver_output) = tokio::join!(sender_task, receiver_task); + let (sender_output, receiver_output) = (sender_output.unwrap(), receiver_output.unwrap()); + + // Check result + for (k, (a, b)) in std::iter::zip(sender_output, receiver_output).enumerate() { + assert_eq!(mul(a, b), random_numbers[k]); + } + } + + #[tokio::test] + async fn test_aio_m2a() { + let (mut sender, mut receiver) = mock_converter_pair(); + + // Create some random numbers + let random_numbers_1: Vec = get_random_gf2_128_vec(128); + let random_numbers_2: Vec = get_random_gf2_128_vec(128); + let random_numbers: Vec = + std::iter::zip(random_numbers_1.iter(), random_numbers_2.iter()) + .map(|(a, b)| mul(*a, *b)) + .collect(); + + // Spawn tokio tasks and wait for them to finish + let sender_task = tokio::spawn(async move { + sender + .convert_from::(&random_numbers_1) + .await + .unwrap() + }); + let receiver_task = tokio::spawn(async move { + receiver + .convert_from::(&random_numbers_2) + .await + .unwrap() + }); + let (sender_output, receiver_output) = tokio::join!(sender_task, receiver_task); + let (sender_output, receiver_output) = (sender_output.unwrap(), receiver_output.unwrap()); + + // Check result + for (k, (a, b)) in std::iter::zip(sender_output, receiver_output).enumerate() { + assert_eq!(a ^ b, random_numbers[k]); + } + } + + fn mock_converter_pair() -> ( + Sender>>>, + Receiver>>>, + ) { + let ot_factory = Arc::new(Mutex::new(MockOTFactory::::default())); + let sender = Sender::new(Arc::clone(&ot_factory), String::from("")); + let receiver = Receiver::new(Arc::clone(&ot_factory), String::from("")); + (sender, receiver) + } + + fn get_random_gf2_128_vec(len: usize) -> Vec { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + (0..len).map(|_| rng.gen::()).collect() + } +} diff --git a/share-conversion-aio/src/gf2_128/receiver.rs b/share-conversion-aio/src/gf2_128/receiver.rs new file mode 100644 index 000000000..cfa005f21 --- /dev/null +++ b/share-conversion-aio/src/gf2_128/receiver.rs @@ -0,0 +1,90 @@ +//! This module implements the async IO receiver + +use super::{AddShare, Gf2_128ShareConvert, MulShare}; +use crate::{AdditiveToMultiplicative, MultiplicativeToAdditive, ShareConversionError}; +use async_trait::async_trait; +use mpc_aio::protocol::ot::{OTReceiverFactory, ObliviousReceive}; +use mpc_core::Block; + +/// The receiver for the conversion +/// +/// Will be the OT receiver +pub struct Receiver { + receiver_factory: T, + id: String, +} + +impl< + T: OTReceiverFactory + Send, + U: ObliviousReceive>, + > Receiver +{ + /// Creates a new receiver + pub fn new(receiver_factory: T, id: String) -> Self { + Self { + receiver_factory, + id, + } + } + + /// Convert the shares using oblivious transfer + pub(crate) async fn convert_from( + &mut self, + shares: &[u128], + ) -> Result, ShareConversionError> { + if shares.is_empty() { + return Ok(vec![]); + } + + let mut choices: Vec = vec![]; + shares.iter().for_each(|x| { + let share = V::new(*x).choices(); + choices.extend_from_slice(&share); + }); + let mut ot_receiver = self + .receiver_factory + .new_receiver(self.id.clone(), choices.len() * 128) + .await?; + let ot_output = ot_receiver.receive(&choices).await?; + + let converted_shares = ot_output + .chunks(128) + .map(|chunk| { + V::from_choice(&chunk.iter().map(|x| x.inner()).collect::>()).inner() + }) + .collect(); + Ok(converted_shares) + } +} + +#[async_trait] +impl< + T: OTReceiverFactory + Send, + U: ObliviousReceive> + Send, + > AdditiveToMultiplicative for Receiver +{ + type FieldElement = u128; + + async fn a_to_m( + &mut self, + input: &[Self::FieldElement], + ) -> Result, ShareConversionError> { + self.convert_from::(input).await + } +} + +#[async_trait] +impl< + T: OTReceiverFactory + Send, + U: ObliviousReceive> + Send, + > MultiplicativeToAdditive for Receiver +{ + type FieldElement = u128; + + async fn m_to_a( + &mut self, + input: &[Self::FieldElement], + ) -> Result, ShareConversionError> { + self.convert_from::(input).await + } +} diff --git a/share-conversion-aio/src/gf2_128/sender.rs b/share-conversion-aio/src/gf2_128/sender.rs new file mode 100644 index 000000000..225a2afe2 --- /dev/null +++ b/share-conversion-aio/src/gf2_128/sender.rs @@ -0,0 +1,84 @@ +//! This module implements the async IO sender + +use super::{AddShare, Gf2_128ShareConvert, MulShare, OTEnvelope}; +use crate::{AdditiveToMultiplicative, MultiplicativeToAdditive, ShareConversionError}; +use async_trait::async_trait; +use mpc_aio::protocol::ot::{OTSenderFactory, ObliviousSend}; +use rand::SeedableRng; +use rand_chacha::ChaCha12Rng; + +/// The sender for the conversion +/// +/// Will be the OT sender +pub struct Sender { + sender_factory: T, + id: String, +} + +impl Sender +where + T: Send, + <::Protocol as ObliviousSend>::Inputs: From, +{ + /// Creates a new sender + pub fn new(sender_factory: T, id: String) -> Self { + Self { sender_factory, id } + } + + /// Convert the shares using oblivious transfer + pub(crate) async fn convert_from( + &mut self, + shares: &[u128], + ) -> Result, ShareConversionError> { + let mut rng = ChaCha12Rng::from_entropy(); + let mut local_shares = vec![]; + + if shares.is_empty() { + return Ok(local_shares); + } + + let mut ot_shares = OTEnvelope::default(); + shares.iter().for_each(|share| { + let share = U::new(*share); + let (local, ot) = share.convert(&mut rng); + local_shares.push(local.inner()); + ot_shares.extend(ot); + }); + let mut ot_sender = self + .sender_factory + .new_sender(self.id.clone(), ot_shares.len()) + .await?; + ot_sender.send(ot_shares.into()).await?; + Ok(local_shares) + } +} + +#[async_trait] +impl AdditiveToMultiplicative for Sender +where + <::Protocol as ObliviousSend>::Inputs: From + Send, +{ + type FieldElement = u128; + + async fn a_to_m( + &mut self, + input: &[Self::FieldElement], + ) -> Result, ShareConversionError> { + self.convert_from::(input).await + } +} + +#[async_trait] +impl MultiplicativeToAdditive for Sender +where + <::Protocol as ObliviousSend>::Inputs: From + Send, +{ + type FieldElement = u128; + + async fn m_to_a( + &mut self, + input: &[Self::FieldElement], + ) -> Result, ShareConversionError> { + self.convert_from::(input).await + } +} diff --git a/share-conversion-aio/src/lib.rs b/share-conversion-aio/src/lib.rs new file mode 100644 index 000000000..f5e7177f2 --- /dev/null +++ b/share-conversion-aio/src/lib.rs @@ -0,0 +1,36 @@ +//! This subcrate implements the async IO layer for share-conversion + +use async_trait::async_trait; +use mpc_aio::protocol::ot::{OTError, OTFactoryError}; +use thiserror::Error; + +pub mod gf2_128; + +/// Allows to convert additive shares of type `FieldElement` into multiplicative ones +#[async_trait] +pub trait AdditiveToMultiplicative { + type FieldElement: Copy + std::fmt::Debug; + async fn a_to_m( + &mut self, + input: &[Self::FieldElement], + ) -> Result, ShareConversionError>; +} + +/// Allows to convert multiplicative shares of type `FieldElement` into additive ones +#[async_trait] +pub trait MultiplicativeToAdditive { + type FieldElement: Copy + std::fmt::Debug; + async fn m_to_a( + &mut self, + input: &[Self::FieldElement], + ) -> Result, ShareConversionError>; +} + +/// An error for what can go wrong during conversion +#[derive(Debug, Error)] +pub enum ShareConversionError { + #[error("OTFactoryError: {0}")] + OTFactoryError(#[from] OTFactoryError), + #[error("OTError: {0}")] + OTError(#[from] OTError), +} diff --git a/share-conversion-aio/tests/ot_integration.rs b/share-conversion-aio/tests/ot_integration.rs new file mode 100644 index 000000000..677319c17 --- /dev/null +++ b/share-conversion-aio/tests/ot_integration.rs @@ -0,0 +1 @@ +// TODO: Add integration test with OTFactory diff --git a/gf2-128/Cargo.toml b/share-conversion-core/Cargo.toml similarity index 68% rename from gf2-128/Cargo.toml rename to share-conversion-core/Cargo.toml index f53c2239a..48c7ea473 100644 --- a/gf2-128/Cargo.toml +++ b/share-conversion-core/Cargo.toml @@ -1,22 +1,20 @@ [package] -name = "gf2-128" +name = "share-conversion-core" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +tlsn-mpc-core = { path = "../mpc-core" } +thiserror.workspace = true rand.workspace = true -rand_chacha.workspace = true [dev-dependencies] +rand_chacha.workspace = true ghash_rc.workspace = true -tokio.workspace = true -tlsn-mpc-aio = { path = "../mpc-aio" } -tlsn-utils-aio = { path = "../utils-aio", features = ["duplex"] } -tlsn-mpc-core = { path = "../mpc-core" } criterion.workspace = true [[bench]] -name = "inverse" +name = "inverse_gf2_128" harness = false diff --git a/gf2-128/benches/inverse.rs b/share-conversion-core/benches/inverse_gf2_128.rs similarity index 72% rename from gf2-128/benches/inverse.rs rename to share-conversion-core/benches/inverse_gf2_128.rs index a3905374f..e7e89fac2 100644 --- a/gf2-128/benches/inverse.rs +++ b/share-conversion-core/benches/inverse_gf2_128.rs @@ -1,10 +1,10 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use gf2_128::inverse; use rand::Rng; use rand::SeedableRng; use rand_chacha::ChaCha12Rng; +use share_conversion_core::gf2_128::inverse; -fn criterion_benchmark(c: &mut Criterion) { +fn bench_gf2_128_inverse(c: &mut Criterion) { let mut rng = ChaCha12Rng::from_entropy(); let a: u128 = rng.gen(); @@ -15,5 +15,5 @@ fn criterion_benchmark(c: &mut Criterion) { }); } -criterion_group!(benches, criterion_benchmark); +criterion_group!(benches, bench_gf2_128_inverse); criterion_main!(benches); diff --git a/share-conversion-core/src/gf2_128/conversion/a2m.rs b/share-conversion-core/src/gf2_128/conversion/a2m.rs new file mode 100644 index 000000000..20507b2c3 --- /dev/null +++ b/share-conversion-core/src/gf2_128/conversion/a2m.rs @@ -0,0 +1,68 @@ +//! This module implements the A2M algorithm. + +use std::num::NonZeroU128; + +use super::MulShare; +use super::{Gf2_128ShareConvert, OTEnvelope}; +use crate::gf2_128::{inverse, mul}; +use rand::{CryptoRng, Rng}; + +/// An additive share of `A = x + y` +#[derive(Clone, Copy, Debug)] +pub struct AddShare(u128); + +impl AddShare { + /// Turn into a multiplicative share and get values for OT + /// + /// This function returns + /// * `MulShare` - The sender's multiplicative share + /// * `OTEnvelope` - Used for oblivious transfer + pub fn convert_to_multiplicative( + &self, + rng: &mut R, + ) -> (MulShare, OTEnvelope) { + // We need to exclude 0 here, because it does not have an inverse + // which is needed later + let random: NonZeroU128 = rng.gen(); + let random = random.get(); + + let mut masks: [u128; 128] = std::array::from_fn(|_| rng.gen()); + // set the last mask such that the sum of all 128 masks equals 0 + masks[127] = masks.into_iter().take(127).fold(0, |acc, i| acc ^ i); + + let mul_share = MulShare::new(inverse(random)); + + // decompose the share into component sums, e.g. if the share is 10110, we decompose it into + // 0 + 10 + 100 + 0000 + 10000 + let components: Vec = (0..128) + .map(|i| { + // `self.inner() & (1 << i)` first extracts a bit of `self.inner()` in position `i` (counting from + // the right) and then left-shifts that bit by `i` + self.inner() & (1 << i) + }) + .collect(); + + let b0: [u128; 128] = std::array::from_fn(|i| mul(components[i], random) ^ masks[i]); + let b1: [u128; 128] = + std::array::from_fn(|i| mul(components[i] ^ (1 << i), random) ^ masks[i]); + + (mul_share, OTEnvelope(b0.to_vec(), b1.to_vec())) + } +} + +impl Gf2_128ShareConvert for AddShare { + type Output = MulShare; + + fn new(share: u128) -> Self { + Self(share) + } + + #[inline] + fn inner(&self) -> u128 { + self.0 + } + + fn convert(&self, rng: &mut R) -> (Self::Output, OTEnvelope) { + self.convert_to_multiplicative(rng) + } +} diff --git a/share-conversion-core/src/gf2_128/conversion/m2a.rs b/share-conversion-core/src/gf2_128/conversion/m2a.rs new file mode 100644 index 000000000..c499cb6a6 --- /dev/null +++ b/share-conversion-core/src/gf2_128/conversion/m2a.rs @@ -0,0 +1,44 @@ +//! This module implements the M2A algorithm. + +use super::a2m::AddShare; +use super::{Gf2_128ShareConvert, OTEnvelope}; +use crate::gf2_128::mul; +use rand::{CryptoRng, Rng}; + +/// A multiplicative share of `A = a * b` +#[derive(Clone, Copy, Debug)] +pub struct MulShare(u128); + +impl MulShare { + /// Turn into an additive share and get values for OT + /// + /// This function returns + /// * `AddShare` - The sender's additive share + /// * `OTEnvelope` - Used for oblivious transfer + pub fn convert_to_additive(&self, rng: &mut R) -> (AddShare, OTEnvelope) { + let masks: [u128; 128] = std::array::from_fn(|_| rng.gen()); + + let t0: [u128; 128] = std::array::from_fn(|i| masks[i]); + let t1: [u128; 128] = std::array::from_fn(|i| mul(self.inner(), 1 << i) ^ masks[i]); + + let add_share = AddShare::new(t0.into_iter().fold(0, |acc, i| acc ^ i)); + (add_share, OTEnvelope(t0.to_vec(), t1.to_vec())) + } +} + +impl Gf2_128ShareConvert for MulShare { + type Output = AddShare; + + fn new(share: u128) -> Self { + Self(share) + } + + #[inline] + fn inner(&self) -> u128 { + self.0 + } + + fn convert(&self, rng: &mut R) -> (Self::Output, OTEnvelope) { + self.convert_to_additive(rng) + } +} diff --git a/share-conversion-core/src/gf2_128/conversion/mod.rs b/share-conversion-core/src/gf2_128/conversion/mod.rs new file mode 100644 index 000000000..526168d5d --- /dev/null +++ b/share-conversion-core/src/gf2_128/conversion/mod.rs @@ -0,0 +1,133 @@ +//! This module implements the M2A and A2M conversion algorithms for field elements of GF(2^128), using +//! oblivious transfer. +//! +//! * M2A: Implementation of chapter 4.1 in +//! * A2M: Adaptation of chapter 4 in + +mod a2m; +mod m2a; + +pub use a2m::AddShare; +pub use m2a::MulShare; +use mpc_core::Block; +use rand::{CryptoRng, Rng}; + +/// A trait for converting field elements +/// +/// Allows two parties to switch between additively and multiplicatively +/// shared representations of a field element. +pub trait Gf2_128ShareConvert: Copy +where + Self: Sized, +{ + type Output: Gf2_128ShareConvert; + + /// Create a new instance + fn new(share: u128) -> Self; + + /// Converts '&self' into choices needed for the receiver input to an oblivious transfer. + /// The choices are in the "least-bit-first" order. + fn choices(&self) -> Vec { + let mut out: Vec = Vec::with_capacity(128); + for k in 0..128 { + out.push(((self.inner() >> k) & 1) == 1); + } + out + } + + /// Return the inner value + fn inner(&self) -> u128; + + /// Create a share of type `Self::Output` from the result of an oblivious transfer (OT) + /// + /// The `value` needs to be built from the output of an OT + fn from_choice(value: &[u128]) -> Self::Output { + Self::Output::new(value.iter().fold(0, |acc, i| acc ^ i)) + } + + /// Prepares a share for conversion in an OT + /// + /// Converts the share to a new share and returns what is needed for sending in an OT. + fn convert(&self, rng: &mut R) -> (Self::Output, OTEnvelope); +} + +/// Batched values for several oblivious transfers +/// +/// The inner vectors `.0` and `.1` belong to the corresponding receiver's choice bit +#[derive(Clone, Debug, Default)] +pub struct OTEnvelope(pub(crate) Vec, pub(crate) Vec); + +impl OTEnvelope { + /// Allows to aggregate envelopes + pub fn extend(&mut self, other: OTEnvelope) { + self.0.extend_from_slice(&other.0); + self.1.extend_from_slice(&other.1); + } + + /// Get the number of OTs in this envelope + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if envelope is empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From for Vec<[Block; 2]> { + fn from(value: OTEnvelope) -> Self { + let mut out = Vec::with_capacity(value.0.len()); + for (zero, one) in value.0.iter().zip(value.1.iter()) { + out.push([Block::new(*zero), Block::new(*one)]) + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::gf2_128::mul; + use a2m::AddShare; + use m2a::MulShare; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha12Rng; + + #[test] + fn test_m2a() { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + let a: MulShare = MulShare::new(rng.gen()); + let b: MulShare = MulShare::new(rng.gen()); + + let (x, sharings) = a.convert_to_additive(&mut rng); + + let choice = mock_ot(sharings, b.inner()); + let y = AddShare::from_choice(&choice); + + assert_eq!(mul(a.inner(), b.inner()), x.inner() ^ y.inner()); + } + + #[test] + fn test_a2m() { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + let x: AddShare = AddShare::new(rng.gen()); + let y: AddShare = AddShare::new(rng.gen()); + + let (a, sharings) = x.convert_to_multiplicative(&mut rng); + + let choice = mock_ot(sharings, y.inner()); + let b = MulShare::from_choice(&choice); + + assert_eq!(x.inner() ^ y.inner(), mul(a.inner(), b.inner())); + } + + fn mock_ot(envelopes: OTEnvelope, choices: u128) -> Vec { + let mut out: Vec = vec![0; 128]; + for (k, number) in out.iter_mut().enumerate() { + let bit = (choices >> k) & 1; + *number = (bit * envelopes.1[k]) ^ ((bit ^ 1) * envelopes.0[k]); + } + out + } +} diff --git a/share-conversion-core/src/gf2_128/mod.rs b/share-conversion-core/src/gf2_128/mod.rs new file mode 100644 index 000000000..d11deb40b --- /dev/null +++ b/share-conversion-core/src/gf2_128/mod.rs @@ -0,0 +1,102 @@ +//! This module provides the share-conversion algorithms inside the module `conversion` for +//! elements of GF(2^128), as well as some arithmetic functions for these field elements. + +mod conversion; + +pub use conversion::{AddShare, Gf2_128ShareConvert, MulShare, OTEnvelope}; + +/// R is the GCM polynomial in little-endian. In hex: "E1000000000000000000000000000000" +const R: u128 = 299076299051606071403356588563077529600; + +/// Galois field multiplication of two 128-bit blocks reduced by the GCM polynomial +pub fn mul(mut x: u128, y: u128) -> u128 { + let mut result: u128 = 0; + for i in (0..128).rev() { + result ^= x * ((y >> i) & 1); + x = (x >> 1) ^ ((x & 1) * R); + } + result +} + +/// Galois field inversion of 128-bit block +pub fn inverse(mut x: u128) -> u128 { + let one = 1 << 127; + let mut out = one; + + for _ in 0..127 { + x = mul(x, x); + out = mul(out, x); + } + out +} + +/// Iteratively multiplies some field element with another field element +/// +/// This function multiplies the last element in `powers` with some other field element `factor` +/// and appends the result to `powers`. This process is repeated `count` times. +/// +/// * `powers` - The vector to which the new higher powers get pushed +/// * `factor` - The field element with which the last element of the vector is multiplied +/// * `count` - How many products are computed +pub fn compute_product_repeated(powers: &mut Vec, factor: u128, count: usize) { + for _ in 0..count { + let last_power = *powers + .last() + .expect("Vector is empty. Cannot compute higher powers"); + powers.push(mul(factor, last_power)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ghash_rc::{ + universal_hash::{NewUniversalHash, UniversalHash}, + GHash, + }; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha12Rng; + + #[test] + // Test multiplication against RustCrypto + fn test_mul() { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + let a: u128 = rng.gen(); + let b: u128 = rng.gen(); + + let mut g = GHash::new(&a.to_be_bytes().into()); + g.update(&b.to_be_bytes().into()); + // Ghash will internally multiply a and b + let expected = g.finalize(); + + assert_eq!( + mul(a, b), + u128::from_be_bytes(expected.into_bytes().try_into().unwrap()) + ); + } + + #[test] + fn test_inverse() { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + let a: u128 = rng.gen(); + let inverse_a = inverse(a); + + assert_eq!(mul(a, inverse_a), 1_u128 << 127); + assert_eq!(inverse(1_u128 << 127), 1_u128 << 127); + } + + #[test] + fn test_compute_product_repeated() { + let mut rng = ChaCha12Rng::from_seed([0; 32]); + let a: u128 = rng.gen(); + + let mut powers = vec![a]; + let factor = mul(a, a); + + compute_product_repeated(&mut powers, factor, 2); + + assert_eq!(powers[0], a); + assert_eq!(powers[1], mul(powers[0], factor)); + assert_eq!(powers[2], mul(powers[1], factor)); + } +} diff --git a/share-conversion-core/src/lib.rs b/share-conversion-core/src/lib.rs new file mode 100644 index 000000000..95cfb2d9a --- /dev/null +++ b/share-conversion-core/src/lib.rs @@ -0,0 +1,17 @@ +//! This subcrate implements secure two-party (2PC) multiplication-to-addition (M2A) and +//! addition-to-multiplication (A2M) algorithms, both with semi-honest security. +//! +//! ### M2A algorithm +//! Let `A` be an element of some finite field with `A = a * b`, where `a` is only known to Alice +//! and `b` is only known to Bob. A is unknown to both parties and it is their goal that each of +//! them ends up with an additive share of A. So both parties start with `a` and `b` and want to +//! end up with `x` and `y`, where `A = a * b = x + y`. +//! +//! ### A2M algorithm +//! This is the other way round. +//! Let `A` be an element of some finite field with `A = x + y`, where `x` is only known to Alice +//! and `y` is only known to Bob. A is unknown to both parties and it is their goal that each of +//! them ends up with a multiplicative share of A. So both parties start with `x` and `y` and want to +//! end up with `a` and `b`, where `A = x + y = a * b`. + +pub mod gf2_128;