Refactor of gf2_128 crate (#123)

* WIP: Refactoring a2m/m2a...

* WIP: Refactor homomorphic subcrate...

* Implemented aio homomorph sender

* Removed integration test for now and renamed things

* Implemented homomorph aio receiver

* Improved API

* Rename crate homomorph -> share_conversion

* Rename errors and traits

* Migrated changes from branch `ghash-refactor-again`

* Improvements to share-conversion

- Renamed share_conversion to share-conversion
- Got rid of PhantomData for aio layer

* Improved variable naming and some doc

* Added aio unit tests and various improvements

- documentation
- variable naming

* Added TODO comment for OT integration test

* Added part of feedback

* Separated share-conversion into {share-converison-core, share-conversion-aio}

* Sample from NonZeroU128 for `random` in a2m

* misc comments and code simplifications

Co-authored-by: themighty1 <you@example.com>
This commit is contained in:
th4s
2022-12-13 11:23:50 +00:00
committed by GitHub
parent fb604e5973
commit b6d362ca69
19 changed files with 805 additions and 342 deletions

View File

@@ -11,7 +11,8 @@ members = [
"utils-aio",
"clmul",
"matrix-transpose",
"gf2-128"
"share-conversion-core",
"share-conversion-aio"
]
exclude = ["tls-client"]

View File

@@ -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 <https://link.springer.com/content/pdf/10.1007/3-540-48405-1_8.pdf>
//!
//! ### 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 <https://www.cs.umd.edu/~fenghao/paper/modexp.pdf>
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);
}
}

View File

@@ -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
}

View File

@@ -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::<OTMessage>::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::<Vec<u128>>()
.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::<OTMessage>::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::<Vec<u128>>()
.try_into()
.unwrap();
let b = MulShare::from_choice(output);
assert_eq!(x.inner() ^ y.inner(), mul(a.inner(), b.inner()));
}

View File

@@ -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<T> {
waiting_sender: Option<MockOTSender<T>>,
waiting_receiver: Option<MockOTReceiver<T>>,
}
#[async_trait]
impl<T: Send + 'static> OTSenderFactory for Arc<Mutex<MockOTFactory<T>>> {
type Protocol = MockOTSender<T>;
async fn new_sender(
&mut self,
_id: String,
_count: usize,
) -> Result<Self::Protocol, OTFactoryError> {
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::<T>();
inner.waiting_receiver = Some(receiver);
Ok(sender)
}
}
}
#[async_trait]
impl<T: Send + 'static> OTReceiverFactory for Arc<Mutex<MockOTFactory<T>>> {
type Protocol = MockOTReceiver<T>;
async fn new_receiver(
&mut self,
_id: String,
_count: usize,
) -> Result<Self::Protocol, OTFactoryError> {
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::<T>();
inner.waiting_sender = Some(sender);
Ok(receiver)
}
}
}
pub struct MockOTSender<T> {
sender: mpsc::Sender<Vec<[T; 2]>>,
}

View File

@@ -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<Self::Input>) -> 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<Self::Protocol, OTFactoryError>;
}
#[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<Self::Protocol, OTFactoryError>;
}
#[cfg(test)]
mockall::mock! {
pub ObliviousSender {}

View File

@@ -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

View File

@@ -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<u128> = get_random_gf2_128_vec(128);
let random_numbers_2: Vec<u128> = get_random_gf2_128_vec(128);
let random_numbers: Vec<u128> =
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::<AddShare>(&random_numbers_1)
.await
.unwrap()
});
let receiver_task = tokio::spawn(async move {
receiver
.convert_from::<AddShare>(&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<u128> = get_random_gf2_128_vec(128);
let random_numbers_2: Vec<u128> = get_random_gf2_128_vec(128);
let random_numbers: Vec<u128> =
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::<MulShare>(&random_numbers_1)
.await
.unwrap()
});
let receiver_task = tokio::spawn(async move {
receiver
.convert_from::<MulShare>(&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<Arc<Mutex<MockOTFactory<Block>>>>,
Receiver<Arc<Mutex<MockOTFactory<Block>>>>,
) {
let ot_factory = Arc::new(Mutex::new(MockOTFactory::<Block>::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<u128> {
let mut rng = ChaCha12Rng::from_seed([0; 32]);
(0..len).map(|_| rng.gen::<u128>()).collect()
}
}

View File

@@ -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<T: OTReceiverFactory> {
receiver_factory: T,
id: String,
}
impl<
T: OTReceiverFactory<Protocol = U> + Send,
U: ObliviousReceive<Choice = bool, Outputs = Vec<Block>>,
> Receiver<T>
{
/// 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<V: Gf2_128ShareConvert>(
&mut self,
shares: &[u128],
) -> Result<Vec<u128>, ShareConversionError> {
if shares.is_empty() {
return Ok(vec![]);
}
let mut choices: Vec<bool> = 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::<Vec<u128>>()).inner()
})
.collect();
Ok(converted_shares)
}
}
#[async_trait]
impl<
T: OTReceiverFactory<Protocol = U> + Send,
U: ObliviousReceive<Choice = bool, Outputs = Vec<Block>> + Send,
> AdditiveToMultiplicative for Receiver<T>
{
type FieldElement = u128;
async fn a_to_m(
&mut self,
input: &[Self::FieldElement],
) -> Result<Vec<Self::FieldElement>, ShareConversionError> {
self.convert_from::<AddShare>(input).await
}
}
#[async_trait]
impl<
T: OTReceiverFactory<Protocol = U> + Send,
U: ObliviousReceive<Choice = bool, Outputs = Vec<Block>> + Send,
> MultiplicativeToAdditive for Receiver<T>
{
type FieldElement = u128;
async fn m_to_a(
&mut self,
input: &[Self::FieldElement],
) -> Result<Vec<Self::FieldElement>, ShareConversionError> {
self.convert_from::<MulShare>(input).await
}
}

View File

@@ -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<T: OTSenderFactory> {
sender_factory: T,
id: String,
}
impl<T: OTSenderFactory> Sender<T>
where
T: Send,
<<T as OTSenderFactory>::Protocol as ObliviousSend>::Inputs: From<OTEnvelope>,
{
/// 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<U: Gf2_128ShareConvert>(
&mut self,
shares: &[u128],
) -> Result<Vec<u128>, 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<T: OTSenderFactory + Send> AdditiveToMultiplicative for Sender<T>
where
<<T as OTSenderFactory>::Protocol as ObliviousSend>::Inputs: From<OTEnvelope> + Send,
{
type FieldElement = u128;
async fn a_to_m(
&mut self,
input: &[Self::FieldElement],
) -> Result<Vec<Self::FieldElement>, ShareConversionError> {
self.convert_from::<AddShare>(input).await
}
}
#[async_trait]
impl<T: OTSenderFactory + Send> MultiplicativeToAdditive for Sender<T>
where
<<T as OTSenderFactory>::Protocol as ObliviousSend>::Inputs: From<OTEnvelope> + Send,
{
type FieldElement = u128;
async fn m_to_a(
&mut self,
input: &[Self::FieldElement],
) -> Result<Vec<Self::FieldElement>, ShareConversionError> {
self.convert_from::<MulShare>(input).await
}
}

View File

@@ -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<Vec<Self::FieldElement>, 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<Vec<Self::FieldElement>, 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),
}

View File

@@ -0,0 +1 @@
// TODO: Add integration test with OTFactory

View File

@@ -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

View File

@@ -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);

View File

@@ -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<R: Rng + CryptoRng>(
&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<u128> = (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<R: Rng + CryptoRng>(&self, rng: &mut R) -> (Self::Output, OTEnvelope) {
self.convert_to_multiplicative(rng)
}
}

View File

@@ -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<R: Rng + CryptoRng>(&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<R: Rng + CryptoRng>(&self, rng: &mut R) -> (Self::Output, OTEnvelope) {
self.convert_to_additive(rng)
}
}

View File

@@ -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 <https://link.springer.com/content/pdf/10.1007/3-540-48405-1_8.pdf>
//! * A2M: Adaptation of chapter 4 in <https://www.cs.umd.edu/~fenghao/paper/modexp.pdf>
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<bool> {
let mut out: Vec<bool> = 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<R: Rng + CryptoRng>(&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<u128>, pub(crate) Vec<u128>);
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<OTEnvelope> 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<u128> {
let mut out: Vec<u128> = 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
}
}

View File

@@ -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<u128>, 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));
}
}

View File

@@ -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;