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
302 changes: 298 additions & 4 deletions crates/core/src/aes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Fixed-key AES cipher

use std::sync::OnceLock;

use aes::Aes128Enc;
use cipher::{BlockCipherEncrypt, KeyInit};
use once_cell::sync::Lazy;
Expand All @@ -12,20 +14,136 @@ pub const FIXED_KEY: [u8; 16] = [
];

/// Fixed-key AES cipher
pub static FIXED_KEY_AES: Lazy<FixedKeyAes> = Lazy::new(|| FixedKeyAes {
aes: Aes128Enc::new_from_slice(&FIXED_KEY).unwrap(),
});
pub static FIXED_KEY_AES: Lazy<FixedKeyAes> = Lazy::new(|| FixedKeyAes::new(FIXED_KEY));

/// Fixed-key AES cipher
/// Fixed-key AES cipher.
///
/// Provides correlation-robust hash functions (CR, CCR, TCCR) and the
/// RTCCR hash from "Three Halves Make a Whole" (Rosulek & Roy, 2021).
///
/// The RTCCR universal hash coefficient is lazily initialized on first use.
pub struct FixedKeyAes {
aes: Aes128Enc,
/// Lazily computed universal hash coefficient for RTCCR.
/// Derived by encrypting zero: u = AES_k(0).
u: OnceLock<Block>,
}

impl FixedKeyAes {
/// Create a fixed-key AES cipher with a given key.
pub fn new(key: [u8; 16]) -> Self {
Self {
aes: Aes128Enc::new(&key.into()),
u: OnceLock::new(),
}
}

/// Get or compute the universal hash coefficient for RTCCR.
///
/// Lazily derives u = AES_k(0) on first call.
///
/// # Universal Hash Implementation Note
///
/// The paper (Section 5, Page 9) specifies U(τ) = (u₁·τ_L) ‖ (u₂·τ_R) using
/// two independent GF(2⁶⁴) multiplications. We instead use a single
/// GF(2¹²⁸) multiplication: U(τ) = u · τ in GF(2¹²⁸).
///
/// Rationale:
/// - GF(2¹²⁸) multiplication uses hardware CLMUL instructions (~10-20x
/// faster)
/// - A single field multiplication is still a valid universal hash function
/// - GF(2¹²⁸) provides better mixing than two independent GF(2⁶⁴)
/// operations
/// - The security proof only requires U to be universal, not the specific
/// construction
#[inline]
fn u(&self) -> Block {
*self.u.get_or_init(|| {
let mut u = Block::new([0u8; 16]);
self.aes.encrypt_block(u.as_array_mut());
u
})
}

/// Compute universal hash U(τ) = u · τ in GF(2¹²⁸)
#[inline]
fn universal_hash(&self, tweak: Block) -> Block {
self.u().gfmul(tweak)
}

/// Randomized tweakable circular correlation-robust hash function (RTCCR).
///
/// From "Three Halves Make a Whole" (Rosulek & Roy, 2021):
/// <https://eprint.iacr.org/2021/749>
///
/// `H(X, τ) = AES_k(X ⊕ U(τ)) ⊕ σ(X ⊕ U(τ))`
///
/// Where U(τ) is a universal hash function.
#[inline]
pub fn rtccr(&self, tweak: Block, block: Block) -> Block {
// σ(X) = α·X in GF(2^128), where α = 0x87.
// See rtccr_many for detailed documentation on the choice of α.
const ALPHA: Block = Block::new([0x87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
#[inline]
fn sigma(x: Block) -> Block {
x.gfmul(ALPHA)
}

let u_tweak = self.universal_hash(tweak);
let tweaked = block ^ u_tweak;
let mut encrypted = tweaked;
self.aes.encrypt_block(encrypted.as_array_mut());
encrypted ^ sigma(tweaked)
}

/// Randomized tweakable circular correlation-robust hash function (RTCCR) -
/// batch version.
///
/// From "Three Halves Make a Whole" (Rosulek & Roy, 2021):
/// <https://eprint.iacr.org/2021/749>
///
/// `H(X, τ) = AES_k(X ⊕ U(τ)) ⊕ σ(X ⊕ U(τ))`
///
/// Where U(τ) = (u₁·τ_L) ‖ (u₂·τ_R) is a universal hash function.
///
/// # Arguments
///
/// * `tweaks` - The tweaks to use for each block.
/// * `blocks` - The blocks to hash in-place.
#[inline]
pub fn rtccr_many<const N: usize>(&self, tweaks: &[Block; N], blocks: &mut [Block; N]) {
// The paper (Section 5) requires α ∈ GF(2^64) \ GF(2²), meaning α must
// not be in the subfield GF(4) = {0, 1, β, β+1} where β² + β + 1 = 0.
// Elements in GF(4) have multiplicative order dividing 3, which would
// make σ³ = identity and break circular correlation robustness.
//
// We use α = 0x87 (the GCM polynomial constant) in GF(2^128):
// - Much higher multiplicative order than minimal choices
// - Better security margin against attacks not covered by the proof
// - Uses hardware-accelerated CLMUL instructions
// - 0x87 is well-studied from GCM/GHASH
const ALPHA: Block = Block::new([0x87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);

#[inline]
fn sigma(x: Block) -> Block {
// RTCCR sigma function: σ(X) = α·X in GF(2^128)
x.gfmul(ALPHA)
}

// Compute X ⊕ U(τ) for all blocks
for (block, tweak) in blocks.iter_mut().zip(tweaks.iter()) {
*block ^= self.universal_hash(*tweak);
}

// Store σ(X ⊕ U(τ)) in buf before encryption overwrites blocks
let sigma_buf: [Block; N] = std::array::from_fn(|i| sigma(blocks[i]));

// Encrypt all tweaked blocks: AES_k(X ⊕ U(τ))
self.aes.encrypt_blocks(Block::as_array_mut_slice(blocks));

// XOR with sigma: AES_k(X ⊕ U(τ)) ⊕ σ(X ⊕ U(τ))
for (block, sigma) in blocks.iter_mut().zip(sigma_buf.iter()) {
*block ^= *sigma;
}
}

Expand Down Expand Up @@ -220,3 +338,179 @@ fn aes_test() {
]
);
}

#[cfg(test)]
mod rtccr_tests {
use super::*;

/// Test that rtccr and rtccr_many produce identical results
#[test]
fn rtccr_single_vs_batch() {
let key = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let aes = FixedKeyAes::new(key);

let tweak = Block::new([0xAB; 16]);
let block = Block::new([0xCD; 16]);

// Single call
let single_result = aes.rtccr(tweak, block);

// Batch call with 1 element
let mut blocks = [block];
aes.rtccr_many(&[tweak], &mut blocks);

assert_eq!(
single_result, blocks[0],
"Single and batch RTCCR should match"
);
}

/// Test that rtccr_many processes multiple blocks correctly
#[test]
fn rtccr_many_multiple_blocks() {
let key = [42u8; 16];
let aes = FixedKeyAes::new(key);

let tweaks = [
Block::new([1u8; 16]),
Block::new([2u8; 16]),
Block::new([3u8; 16]),
Block::new([4u8; 16]),
];
let blocks_original = [
Block::new([0x10; 16]),
Block::new([0x20; 16]),
Block::new([0x30; 16]),
Block::new([0x40; 16]),
];

// Compute individually
let expected: [Block; 4] =
std::array::from_fn(|i| aes.rtccr(tweaks[i], blocks_original[i]));

// Compute in batch
let mut blocks = blocks_original;
aes.rtccr_many(&tweaks, &mut blocks);

assert_eq!(blocks, expected, "Batch should match individual calls");
}

/// Test that universal hash produces different outputs for different tweaks
#[test]
fn universal_hash_different_tweaks() {
let key = [0x55u8; 16];
let aes = FixedKeyAes::new(key);

let block = Block::new([0xAA; 16]);
let tweak1 = Block::new([1u8; 16]);
let tweak2 = Block::new([2u8; 16]);

let result1 = aes.rtccr(tweak1, block);
let result2 = aes.rtccr(tweak2, block);

assert_ne!(
result1, result2,
"Different tweaks should produce different outputs"
);
}

/// Test that RTCCR is deterministic
#[test]
fn rtccr_deterministic() {
let key = [0x77u8; 16];
let aes = FixedKeyAes::new(key);

let tweak = Block::new([0x11; 16]);
let block = Block::new([0x22; 16]);

let result1 = aes.rtccr(tweak, block);
let result2 = aes.rtccr(tweak, block);

assert_eq!(result1, result2, "RTCCR should be deterministic");
}

/// Test that u1, u2 are derived consistently from the same key
#[test]
fn universal_hash_key_derivation() {
let key = [0x99u8; 16];

let aes1 = FixedKeyAes::new(key);
let aes2 = FixedKeyAes::new(key);

// Both should produce same results
let tweak = Block::new([0xBB; 16]);
let block = Block::new([0xCC; 16]);

assert_eq!(
aes1.rtccr(tweak, block),
aes2.rtccr(tweak, block),
"Same key should produce identical u1, u2"
);
}

/// Test that different keys produce different u1, u2
#[test]
fn universal_hash_different_keys() {
let key1 = [0x11u8; 16];
let key2 = [0x22u8; 16];

let aes1 = FixedKeyAes::new(key1);
let aes2 = FixedKeyAes::new(key2);

let tweak = Block::new([0xDD; 16]);
let block = Block::new([0xEE; 16]);

assert_ne!(
aes1.rtccr(tweak, block),
aes2.rtccr(tweak, block),
"Different keys should produce different RTCCR outputs"
);
}

/// Test that sigma uses α = 0x87 correctly
#[test]
fn sigma_alpha_properties() {
// Local sigma for testing (matches the one inside rtccr/rtccr_many)
const ALPHA: Block = Block::new([0x87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
fn sigma(x: Block) -> Block {
x.gfmul(ALPHA)
}

// σ(0) = 0
assert_eq!(sigma(Block::ZERO), Block::ZERO);

// σ(1) = 0x87
let one = Block::new([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
let expected = Block::new([0x87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(sigma(one), expected);

// Verify α = 0x87 is not in GF(4) by checking σ³(1) ≠ σ(1)
// (elements in GF(4) satisfy x^3 = x)
let sigma_one = sigma(one);
let sigma_cubed = sigma(sigma(sigma_one));
assert_ne!(sigma_cubed, sigma_one, "α = 0x87 should not be in GF(4)");
}

/// Test that sigma is linear: σ(A ⊕ B) = σ(A) ⊕ σ(B)
#[test]
fn sigma_linearity() {
const ALPHA: Block = Block::new([0x87, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
fn sigma(x: Block) -> Block {
x.gfmul(ALPHA)
}

let a = Block::new([
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88,
]);
let b = Block::new([
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x00, 0x11,
]);

let sigma_xor = sigma(a ^ b);
let xor_sigma = sigma(a) ^ sigma(b);

assert_eq!(sigma_xor, xor_sigma, "σ should be linear");
}
}
13 changes: 11 additions & 2 deletions crates/garble-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mpz-vm-core = { workspace = true }
aes = { workspace = true, features = [] }
bitvec = { workspace = true, features = ["serde"] }
blake3 = { workspace = true, features = ["serde"] }
bytemuck = { workspace = true }
cfg-if = { workspace = true }
cipher = { workspace = true }
derive_builder = { workspace = true }
Expand All @@ -56,9 +57,17 @@ criterion = { workspace = true }
pretty_assertions = { workspace = true }

[[bench]]
name = "garble"
name = "garbler_hg"
harness = false

[[bench]]
name = "evaluate"
name = "garbler_th"
harness = false

[[bench]]
name = "evaluator_hg"
harness = false

[[bench]]
name = "evaluator_th"
harness = false
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Benchmarks for half-gates evaluation.
//!
//! Run with: `cargo bench -p mpz-garble-core --bench evaluate`
//! Run with: `cargo bench -p mpz-garble-core --bench evaluator_hg`

use std::sync::Arc;

Expand Down
Loading
Loading