diff --git a/Cargo.lock b/Cargo.lock index 60ec358..23667aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "cpufeatures" version = "0.3.0" @@ -17,6 +23,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "hex" version = "0.4.3" @@ -121,12 +136,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.117" @@ -148,12 +157,12 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" name = "wharrgarbl" version = "0.1.0" dependencies = [ + "ctutils", "hex", "keccak", "serde", "serde-big-array", "serde_json", - "subtle", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 127e0ba..2ea9412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2024" [dependencies] keccak = "0.2" -subtle = { version = "2.6", default-features = false } +ctutils = { version = "0.4.2", default-features = false } [dev-dependencies] serde_json = "1" diff --git a/src/opflags.rs b/src/opflags.rs index 34277c5..6a3f367 100644 --- a/src/opflags.rs +++ b/src/opflags.rs @@ -1,6 +1,6 @@ use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; -use subtle::{Choice, ConstantTimeEq}; +use ctutils::{Choice, CtAssign, CtEq}; use crate::strobe::Role; @@ -43,18 +43,30 @@ impl OpFlags { } } +impl Default for OpFlags { + fn default() -> Self { + Self::EMPTY + } +} + impl core::fmt::Debug for OpFlags { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "OpFlags({:#08b})", self.0) } } -impl ConstantTimeEq for OpFlags { - fn ct_eq(&self, other: &Self) -> subtle::Choice { +impl CtEq for OpFlags { + fn ct_eq(&self, other: &Self) -> Choice { self.0.ct_eq(&other.0) } } +impl CtAssign for OpFlags { + fn ct_assign(&mut self, src: &Self, choice: Choice) { + self.0.ct_assign(&src.0, choice); + } +} + impl BitAnd for OpFlags { type Output = OpFlags; @@ -106,7 +118,7 @@ impl BitXorAssign for OpFlags { // This is for a specific case to toggle INBOUND impl BitXorAssign for OpFlags { fn bitxor_assign(&mut self, rhs: Choice) { - self.0 ^= rhs.unwrap_u8(); + self.0 ^= rhs.to_u8(); } } diff --git a/src/strobe.rs b/src/strobe.rs index 5c3a81b..a13d9f9 100644 --- a/src/strobe.rs +++ b/src/strobe.rs @@ -1,4 +1,4 @@ -use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeLess}; +use ctutils::{Choice, CtAssign, CtEq, CtLt, CtSelect}; use crate::{ GarbledError, STROBE_VERSION, @@ -14,7 +14,7 @@ pub enum Role { Receiver, } -impl ConstantTimeEq for Role { +impl CtEq for Role { fn ct_eq(&self, other: &Self) -> Choice { (*self as u8).ct_eq(&(*other as u8)) } @@ -162,7 +162,7 @@ impl StrobeState { fn increment_position(&mut self, increment: usize) { self.position += increment; - if self.position.ct_eq(&self.rate).into() { + if self.position.ct_eq(&self.rate).to_bool() { self.permutation_f(); } } @@ -246,15 +246,11 @@ impl StrobeState { ); // Do the zero-writing in chunks - while bytes_to_zero.ct_ne(&0).into() { + while bytes_to_zero > 0 { let min_slice = (self.rate - self.position) as u32; let to_zero = bytes_to_zero as u32; - let slice_len = ConditionallySelectable::conditional_select( - &min_slice, - &to_zero, - to_zero.ct_lt(&min_slice), - ) as usize; + let slice_len = min_slice.ct_select(&to_zero, to_zero.ct_lt(&min_slice)) as usize; self.state.0[self.position..(self.position + slice_len)].fill(0); @@ -267,8 +263,8 @@ impl StrobeState { /// Mixes the current state index and flags into the state, accounting for whether we are /// sending or receiving fn begin_op(&mut self, mut flags: OpFlags) { - if flags.contains(OpFlags::TRANSPORT).into() { - let op_role = if flags.contains(OpFlags::INBOUND).into() { + if flags.contains(OpFlags::TRANSPORT).to_bool() { + let op_role = if flags.contains(OpFlags::INBOUND).to_bool() { Role::Receiver } else { Role::Sender @@ -287,7 +283,7 @@ impl StrobeState { let mut force_permutation = flags.intersects(OpFlags::CIPHER | OpFlags::KEYTREE); force_permutation &= self.position.ct_ne(&0); - if force_permutation.into() { + if force_permutation.to_bool() { self.permutation_f(); } } @@ -299,7 +295,7 @@ impl StrobeState { self.prev_flags = flags; // If `more` isn't set, this is a new operation. Do the begin_op sequence - if !bool::from(more) { + if !more.to_bool() { self.begin_op(flags); } @@ -311,12 +307,24 @@ impl StrobeState { // RATCHET is special-cased to never call operate directly debug_assert!(flags != ops::KEY && bool::from(flags.contains(OpFlags::CIPHER))); - match flags { - ops::PRF => self.squeeze(data), - ops::SEND_MAC => self.copy_state(data), - ops::SEND_ENC => self.absorb_and_set(data), - _ => self.exchange(data), - } + const SPECIAL_CASES: [OpFlags; 3] = [ops::PRF, ops::SEND_MAC, ops::SEND_ENC]; + const OPS: [fn(&mut StrobeState, data: &mut [u8]); 4] = [ + StrobeState::squeeze, + StrobeState::copy_state, + StrobeState::absorb_and_set, + StrobeState::exchange, + ]; + + // Constant time resolution of op index + let index = SPECIAL_CASES + .iter() + .enumerate() + .fold(3usize, |mut res, (index, op)| { + res.ct_assign(&index, flags.ct_eq(op)); + res + }); + + OPS[index](self, data); } /// Performs the state transformation that corresponds to the given flags. If `more` is given, @@ -326,7 +334,7 @@ impl StrobeState { self.prev_flags = flags; // If `more` isn't set, this is a new operation. Do the begin_op sequence - if !bool::from(more) { + if !more.to_bool() { self.begin_op(flags); } @@ -337,27 +345,28 @@ impl StrobeState { // RATCHET is special cased to never call operate/operate_no_mutate directly debug_assert!(flags == ops::KEY || !bool::from(flags.contains(OpFlags::CIPHER))); - match flags { - // This is equivalent to a non-mutating form of the `duplex` operation in the Python - // implementation, with `cbefore = True` - ops::KEY => self.overwrite(data), - // This is equivalent to the `duplex` operation in the Python implementation, with - // `cbefore = cafter = False` - _ => self.absorb(data), - } + const OPS: [fn(&mut StrobeState, data: &[u8]); 2] = + [StrobeState::absorb, StrobeState::overwrite]; + + let index = (flags.ct_eq(&ops::KEY).to_u8() & 1) as usize; + + OPS[index](self, data); } fn recv_mac_inner(&mut self, flags: OpFlags, mac_copy: &mut [u8]) -> Result<(), GarbledError> { // recv_mac can never be streamed - self.operate(flags, mac_copy, Choice::from(0u8)); + self.operate(flags, mac_copy, Choice::FALSE); // Constant-time MAC check. This accumulates the truth values of byte == 0 - let all_zero: bool = mac_copy + let all_zero = mac_copy .iter() - .fold(Choice::from(1u8), |all_zero, b| all_zero & 0u8.ct_eq(b)) - .into(); + .fold(Choice::TRUE, |all_zero, b| all_zero & 0u8.ct_eq(b)); - if all_zero { Ok(()) } else { Err(GarbledError) } + if all_zero.to_bool() { + Ok(()) + } else { + Err(GarbledError) + } } pub fn recv_mac(&mut self, mac: &[u8; N]) -> Result<(), GarbledError> { @@ -379,7 +388,7 @@ impl StrobeState { // to make the `begin_op` call manually. self.prev_flags = flags; - if !bool::from(more) { + if !more.to_bool() { self.begin_op(flags); }