From 0e0f81117619fc705f387c10522c078a312b0507 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:27:31 -0500 Subject: [PATCH] Implement constant-time hash ordering --- Cargo.toml | 3 ++- src/lib.rs | 23 +++++++++++++++++++++++ src/test.rs | 22 +++++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c5f8c99db..7421602dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ neon = [] # --no-default-features, the only way to use the SIMD implementations in this # crate is to enable the corresponding instruction sets statically for the # entire build, with e.g. RUSTFLAGS="-C target-cpu=native". -std = [] +std = ["subtle/std"] # The `rayon` feature (disabled by default, but enabled for docs.rs) adds the # `update_rayon` and (in combination with `mmap` below) `update_mmap_rayon` @@ -103,6 +103,7 @@ digest = { version = "0.10.1", features = [ "mac" ], optional = true } memmap2 = { version = "0.9", optional = true } rayon-core = { version = "1.12.1", optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } +subtle = { version = "2.6", default-features = false } zeroize = { version = "1", default-features = false, optional = true } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 37c2c0b31..f63ecca7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,8 +136,10 @@ mod join; use arrayref::{array_mut_ref, array_ref}; use arrayvec::{ArrayString, ArrayVec}; use core::cmp; +use core::cmp::Ordering; use core::fmt; use platform::{Platform, MAX_SIMD_DEGREE, MAX_SIMD_DEGREE_OR_2}; +use subtle::{ConditionallySelectable, ConstantTimeEq, ConstantTimeGreater, ConstantTimeLess}; #[cfg(feature = "zeroize")] use zeroize::Zeroize; @@ -341,6 +343,27 @@ impl PartialEq<[u8]> for Hash { impl Eq for Hash {} +impl PartialOrd for Hash { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Hash { + /// This ordering is [lexigraphical](https://doc.rust-lang.org/std/cmp/trait.Ord.html#lexicographical-comparison), and is done in constant time. + fn cmp(&self, other: &Self) -> cmp::Ordering { + let mut order = Ordering::Equal; + + // Iterate over all corresponding bytes, but only set a non-equal ordering on the first mismatch + for (l, r) in self.0.iter().zip(other.0.iter()) { + order.conditional_assign(&Ordering::Less, l.ct_lt(r) & order.ct_eq(&Ordering::Equal)); + order.conditional_assign(&Ordering::Greater, l.ct_gt(r) & order.ct_eq(&Ordering::Equal)); + } + + order + } +} + impl fmt::Display for Hash { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Formatting field as `&str` to reduce code size since the `Debug` diff --git a/src/test.rs b/src/test.rs index a7ac4fcf6..9e2be78a7 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,7 +1,7 @@ use crate::{CVBytes, CVWords, IncrementCounter, BLOCK_LEN, CHUNK_LEN, OUT_LEN}; use arrayref::array_ref; use arrayvec::ArrayVec; -use core::usize; +use core::{cmp::Ordering, usize}; use rand::prelude::*; // Interesting input lengths to run tests on. @@ -488,6 +488,26 @@ fn reference_hash(input: &[u8]) -> crate::Hash { bytes.into() } +#[test] +fn test_ordering() { + // Test equality behavior + let hash = reference_hash(&[0]); + assert_eq!(hash.0.cmp(&hash.0), Ordering::Equal); + assert_eq!(hash.cmp(&hash), Ordering::Equal); + + // Test less-than behavior + let l = reference_hash(&[0]); + let r = reference_hash(&[1]); + assert_eq!(l.0.cmp(&r.0), Ordering::Less); + assert_eq!(l.cmp(&r), Ordering::Less); + + // Test greater-than behavior + let l = reference_hash(&[3]); + let r = reference_hash(&[4]); + assert_eq!(l.0.cmp(&r.0), Ordering::Greater); + assert_eq!(l.cmp(&r), Ordering::Greater); +} + #[test] fn test_compare_update_multiple() { // Don't use all the long test cases here, since that's unnecessarily slow