Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion crates/core/src/config/prove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ use rangeset::{
};
use serde::{Deserialize, Serialize};

use crate::transcript::{Direction, Transcript, TranscriptCommitConfig, TranscriptCommitRequest};
use crate::{
hash::{Blinder, HashAlgId},
transcript::{Direction, Transcript, TranscriptCommitConfig, TranscriptCommitRequest},
};

/// Configuration to prove information to the verifier.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProveConfig {
server_identity: bool,
reveal: Option<(RangeSet<usize>, RangeSet<usize>)>,
transcript_commit: Option<TranscriptCommitConfig>,
/// Prover-local blinders for hash commitments. Never serialized: the
/// verifier learns commitment values from the proving computation, never
/// from the prover's configuration.
#[serde(skip)]
hash_blinders: Vec<(Direction, RangeSet<usize>, HashAlgId, Blinder)>,
}

impl ProveConfig {
Expand All @@ -38,7 +46,22 @@ impl ProveConfig {
self.transcript_commit.as_ref()
}

/// Returns the prover-selected blinder for one hash commitment, if any.
pub fn hash_blinder(
&self,
direction: Direction,
idx: &RangeSet<usize>,
alg: HashAlgId,
) -> Option<&Blinder> {
self.hash_blinders
.iter()
.find(|(d, i, a, _)| *d == direction && i == idx && *a == alg)
.map(|(_, _, _, blinder)| blinder)
}

/// Returns a request.
///
/// Hash blinders stay prover-local and never appear in the request.
pub fn to_request(&self) -> ProveRequest {
ProveRequest {
server_identity: self.server_identity,
Expand All @@ -58,6 +81,7 @@ pub struct ProveConfigBuilder<'a> {
server_identity: bool,
reveal: Option<(RangeSet<usize>, RangeSet<usize>)>,
transcript_commit: Option<TranscriptCommitConfig>,
hash_blinders: Vec<(Direction, RangeSet<usize>, HashAlgId, Blinder)>,
}

impl<'a> ProveConfigBuilder<'a> {
Expand All @@ -68,6 +92,7 @@ impl<'a> ProveConfigBuilder<'a> {
server_identity: false,
reveal: None,
transcript_commit: None,
hash_blinders: Vec::new(),
}
}

Expand All @@ -83,6 +108,26 @@ impl<'a> ProveConfigBuilder<'a> {
self
}

/// Uses a caller-supplied blinder for one hash commitment instead of
/// sampling one during proving.
///
/// The prover can then derive the commitment value locally and start
/// dependent work before the proving phase. The blinder must come from a
/// cryptographically secure source and be used for exactly one
/// commitment. It never leaves the prover: the verifier learns the
/// commitment from the proving computation alone.
pub fn hash_blinder(
&mut self,
direction: Direction,
ranges: impl IntoRangeIterator<usize>,
alg: HashAlgId,
blinder: Blinder,
) -> &mut Self {
self.hash_blinders
.push((direction, RangeSet::from_range_iter(ranges), alg, blinder));
self
}

/// Reveals the given ranges of the transcript.
pub fn reveal(
&mut self,
Expand Down Expand Up @@ -152,6 +197,7 @@ impl<'a> ProveConfigBuilder<'a> {
server_identity: self.server_identity,
reveal: self.reveal,
transcript_commit: self.transcript_commit,
hash_blinders: self.hash_blinders,
})
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/core/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ pub struct Blinder([u8; 16]);
opaque_debug::implement!(Blinder);

impl Blinder {
/// Creates a blinder from caller-supplied randomness.
///
/// A prover that chooses its own blinder can derive the commitment value
/// locally and start dependent work before the proving phase. The blinder
/// only hides the prover's own pre-image from the verifier, so choosing
/// it early adds no verifier-side assumption; it must still come from a
/// cryptographically secure source and be used for exactly one commitment.
pub fn new(blinder: [u8; 16]) -> Self {
Self(blinder)
}

/// Returns the blinder as a byte slice.
pub fn as_bytes(&self) -> &[u8] {
&self.0
Expand Down
6 changes: 5 additions & 1 deletion crates/mpc-tls/src/record_layer/aead/aes_gcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ impl MpcAesGcm {
vm.assign(zero_block, [0u8; 16])?;
vm.commit(zero_block)?;

ghash.alloc()?;
// Size GHASH preprocessing to the largest single record this
// allocation admits instead of always paying for a maximum-size TLS
// record. `powers_for_len` clamps to the old bound, so a caller that
// provisions more than one full record is unchanged.
ghash.alloc(super::ghash::powers_for_len(len))?;
let ghash_key = self.aes.alloc_block(vm, zero_block)?;
let ghash_key_share = OneTimePadShared::<[u8; 16]>::new(self.role, ghash_key, vm)?;

Expand Down
150 changes: 134 additions & 16 deletions crates/mpc-tls/src/record_layer/aead/ghash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,25 @@ use serde::{Deserialize, Serialize};
use crate::record_layer::aead::AeadError;

/// Maximum exponent used in GHASH.
const MAX_POWER: usize = 1026;
///
/// A maximum-size TLS record needs 1024 ciphertext blocks plus one AAD block
/// and one length block, so no single record can need more powers than this.
pub(crate) const MAX_POWER: usize = 1026;

/// Highest exponent a GHASH input of `len` bytes can need.
///
/// One AAD block and one length block accompany the padded ciphertext. The
/// result is clamped to [`MAX_POWER`], so a caller that provisions more than
/// one maximum-size record still allocates exactly the old amount, and is
/// rounded up to an even value because odd powers are converted in pairs.
pub(crate) fn powers_for_len(len: usize) -> usize {
(len.div_ceil(16) + 2).min(MAX_POWER).next_multiple_of(2)
}

#[async_trait]
pub(crate) trait Ghash {
/// Allocates resources needed for GHASH.
fn alloc(&mut self) -> Result<(), GhashError>;
/// Allocates resources needed for GHASH up to `max_power` exponents.
fn alloc(&mut self, max_power: usize) -> Result<(), GhashError>;

/// Preprocesses GHASH.
async fn preprocess(&mut self, ctx: &mut Context) -> Result<(), GhashError>;
Expand All @@ -41,6 +54,10 @@ pub(crate) struct MpcGhash<C> {
state: State,
converter: C,
alloc: bool,
/// Exponent bound fixed by [`Ghash::alloc`]; `setup` and `compute` must
/// use this same value or the share vector and the block offset in
/// `compute` disagree and produce a wrong tag share without an error.
max_power: usize,
}

#[derive(Debug)]
Expand Down Expand Up @@ -68,6 +85,7 @@ impl<C> MpcGhash<C> {
state: State::Init,
converter,
alloc: false,
max_power: MAX_POWER,
}
}
}
Expand All @@ -78,16 +96,23 @@ where
C: AdditiveToMultiplicative<Gf2_128> + Flush + Send,
C: MultiplicativeToAdditive<Gf2_128> + Flush + Send,
{
fn alloc(&mut self) -> Result<(), GhashError> {
fn alloc(&mut self, max_power: usize) -> Result<(), GhashError> {
if !self.alloc {
if max_power < 2 || max_power > MAX_POWER || max_power % 2 != 0 {
return Err(GhashError::state(format!(
"max_power must be even and in 2..={MAX_POWER}, got {max_power}"
)));
}
self.max_power = max_power;

// Odd powers are computed using M2A, even powers are computed
// locally. We need one extra A2M conversion in the beginning.
// Both M2A and A2M, each require a single OLE.
AdditiveToMultiplicative::<Gf2_128>::alloc(&mut self.converter, 1)
.map_err(GhashError::conversion)?;

// -1 because the odd power H^1 is already known at this point.
MultiplicativeToAdditive::<Gf2_128>::alloc(&mut self.converter, (MAX_POWER / 2) - 1)
MultiplicativeToAdditive::<Gf2_128>::alloc(&mut self.converter, (max_power / 2) - 1)
.map_err(GhashError::conversion)?;

self.alloc = true;
Expand Down Expand Up @@ -150,8 +175,9 @@ where

// Compute the odd powers of the multiplicative key share.
//
// Resulting vector contains odd powers of H from H^3 to H^1025.
let odd_shares: Vec<_> = (0..MAX_POWER)
// Resulting vector contains odd powers of H from H^3 to
// H^(max_power - 1).
let odd_shares: Vec<_> = (0..self.max_power)
.scan(mult_key, |acc, _| {
let power_n = *acc;
*acc = power_n * mult_key;
Expand Down Expand Up @@ -180,7 +206,7 @@ where
.expect("share should be computed")
.shares;

let shares = compute_shares(add_key, &add_shares_odd);
let shares = compute_shares(add_key, &add_shares_odd, self.max_power);

self.state = State::Ready { shares };

Expand All @@ -195,10 +221,10 @@ where
// Divide by block length and round up.
let block_count = input.len() / 16 + !input.len().is_multiple_of(16) as usize;

if block_count > MAX_POWER {
if block_count > self.max_power {
return Err(ErrorRepr::InputLength {
len: block_count,
max: MAX_POWER * 16,
max: self.max_power * 16,
}
.into());
}
Expand Down Expand Up @@ -237,14 +263,17 @@ where
///
/// * `key` - Additive share of H.
/// * `odd_powers` - Additive shares of odd powers of H starting at H^3.
fn compute_shares(key: Gf2_128, odd_powers: &[Gf2_128]) -> Vec<Gf2_128> {
let mut shares = Vec::with_capacity(MAX_POWER);
/// * `max_power` - Highest power to compute; must equal the value passed to
/// [`Ghash::alloc`], because `compute` derives its block offset from the
/// length of the returned vector.
fn compute_shares(key: Gf2_128, odd_powers: &[Gf2_128], max_power: usize) -> Vec<Gf2_128> {
let mut shares = Vec::with_capacity(max_power);

// H^1
shares.push(key);

let mut odd_idx = 0;
for i in 2..=MAX_POWER {
for i in 2..=max_power {
if i % 2 == 0 {
// Even power, compute by squaring the square root power.
let base = shares[i / 2 - 1];
Expand Down Expand Up @@ -351,12 +380,21 @@ mod tests {
fn create_pair() -> (
MpcGhash<IdealShareConvertSender<Gf2_128>>,
MpcGhash<IdealShareConvertReceiver<Gf2_128>>,
) {
create_pair_with(MAX_POWER)
}

fn create_pair_with(
max_power: usize,
) -> (
MpcGhash<IdealShareConvertSender<Gf2_128>>,
MpcGhash<IdealShareConvertReceiver<Gf2_128>>,
) {
let (convert_a, convert_b) = ideal_share_convert(Block::ZERO);

let (mut sender, mut receiver) = (MpcGhash::new(convert_a), MpcGhash::new(convert_b));
sender.alloc().unwrap();
receiver.alloc().unwrap();
sender.alloc(max_power).unwrap();
receiver.alloc(max_power).unwrap();

(sender, receiver)
}
Expand All @@ -381,11 +419,91 @@ mod tests {
.cloned()
.collect::<Vec<_>>();

let powers = compute_shares(key, &odd_powers);
let powers = compute_shares(key, &odd_powers, MAX_POWER);

assert_eq!(powers, expected_powers);
}

#[test]
fn bounded_shares_match_the_unbounded_prefix() {
let mut rng = StdRng::seed_from_u64(0);
let key = Gf2_128::rand(&mut rng);
let all: Vec<_> = (0..MAX_POWER)
.scan(key, |acc, _| {
let power_n = *acc;
*acc = power_n * key;
Some(power_n)
})
.collect();
let odd: Vec<_> = all.iter().skip(2).step_by(2).cloned().collect();

for max_power in [2, 4, 56, 1026] {
let bounded = compute_shares(key, &odd[..(max_power / 2) - 1], max_power);
assert_eq!(bounded.len(), max_power);
assert_eq!(bounded, all[..max_power]);
}
}

#[test]
fn powers_for_len_covers_the_input_and_clamps() {
// One AAD block and one length block accompany the padded ciphertext.
assert_eq!(powers_for_len(0), 2);
assert_eq!(powers_for_len(16), 4);
assert_eq!(powers_for_len(17), 4);
assert_eq!(powers_for_len(33), 6);
assert_eq!(powers_for_len(855), 56);
assert_eq!(powers_for_len(16 * 1024), 1026);
assert_eq!(powers_for_len(usize::MAX / 2), MAX_POWER);
for len in [0, 1, 15, 16, 17, 64, 855, 4096] {
assert!(powers_for_len(len) >= len.div_ceil(16) + 2);
assert_eq!(powers_for_len(len) % 2, 0);
}
}

#[test]
fn alloc_rejects_bounds_it_cannot_honor() {
let (convert_a, _) = ideal_share_convert(Block::ZERO);
let mut ghash = MpcGhash::new(convert_a);
assert!(ghash.alloc(0).is_err());
assert!(ghash.alloc(3).is_err());
assert!(ghash.alloc(MAX_POWER + 2).is_err());
ghash.alloc(56).unwrap();
}

#[tokio::test]
async fn bounded_ghash_accepts_its_limit_and_rejects_one_block_more() {
let (mut ctx_a, mut ctx_b) = test_st_context(8);
let mut rng = StdRng::seed_from_u64(7);
let h: u128 = rng.random();
let sender_key: u128 = rng.random();
let receiver_key: u128 = h ^ sender_key;

// Four powers cover an input of two 16-byte blocks plus the AAD and
// length blocks that a real record adds.
let max_power = 4;
let (mut sender, mut receiver) = create_pair_with(max_power);
sender.set_key(sender_key.to_be_bytes().to_vec()).unwrap();
receiver
.set_key(receiver_key.to_be_bytes().to_vec())
.unwrap();
tokio::try_join!(sender.setup(&mut ctx_a), receiver.setup(&mut ctx_b)).unwrap();

let at_limit: Vec<u8> = (0..16 * max_power).map(|_| rng.random()).collect();
let sender_share = sender.compute(&at_limit).unwrap();
let receiver_share = receiver.compute(&at_limit).unwrap();
let combined: Vec<u8> = sender_share
.iter()
.zip(receiver_share.iter())
.map(|(a, b)| a ^ b)
.collect();
assert_eq!(combined, ghash_reference_impl(h, &at_limit));

let mut over_limit = at_limit;
over_limit.push(0);
assert!(sender.compute(&over_limit).is_err());
assert!(receiver.compute(&over_limit).is_err());
}

#[tokio::test]
async fn test_ghash_output() {
let (mut ctx_a, mut ctx_b) = test_st_context(8);
Expand Down
4 changes: 2 additions & 2 deletions crates/tls/client/examples/internal/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
// etc. because it's unstable at the time of writing.

use rustls::{
ClientConfig, ClientConnection, ConnectionCommon, RootCertStore, ServerConfig,
ServerConnection, SideData, Ticketer,
client::{ClientSessionMemoryCache, NoClientSessionStorage},
server::{
AllowAnyAuthenticatedClient, NoClientAuth, NoServerSessionStorage, ServerSessionMemoryCache,
},
ClientConfig, ClientConnection, ConnectionCommon, RootCertStore, ServerConfig,
ServerConnection, SideData, Ticketer,
};
use std::{
convert::TryInto,
Expand Down
Loading