Skip to content

Commit da327c2

Browse files
committed
Small speedups for AES-GCM:
* Eliminate a copy in the GHASH(length) computation at the end. * In the tag computation, instead of copying the 128-bit GHASH result into a u8 array and doing a bytewise XOR loop, load it into a u128 and do a vector XOR.
1 parent 0bbcb9e commit da327c2

7 files changed

Lines changed: 91 additions & 41 deletions

File tree

graviola/src/low/aarch64/cpu.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Written for Graviola by Joe Birr-Pixton, 2024.
22
// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0
33

4+
use core::arch::aarch64::{uint8x16_t, vgetq_lane_u64, vreinterpretq_u64_u8};
5+
use core::mem;
6+
47
pub(crate) fn enter_cpu_state() -> u32 {
58
dit::maybe_enable()
69
}
@@ -155,6 +158,20 @@ pub(in crate::low) unsafe fn ct_compare_bytes(a: *const u8, b: *const u8, len: u
155158
}
156159
}
157160

161+
/// Compare `a` and `b` in constant time, returning 0 if they are equal
162+
/// and some nonzero value if they are not equal.
163+
#[target_feature(enable = "neon")]
164+
pub(in crate::low) fn ct_compare_u128(a: u128, b: u128) -> u64 {
165+
// SAFETY: u128 and uint8x16_t have the compatible memory layouts.
166+
let a: uint8x16_t = unsafe { mem::transmute(a) };
167+
let a = vreinterpretq_u64_u8(a);
168+
// SAFETY: u128 and uint8x16_t have the compatible memory layouts.
169+
let b: uint8x16_t = unsafe { mem::transmute(b) };
170+
let b = vreinterpretq_u64_u8(b);
171+
(vgetq_lane_u64::<0>(a) ^ vgetq_lane_u64::<0>(b)) |
172+
(vgetq_lane_u64::<1>(a) ^ vgetq_lane_u64::<1>(b))
173+
}
174+
158175
/// This macro interdicts is_aarch64_feature_detected to allow testability.
159176
macro_rules! have_cpu_feature {
160177
("neon") => {

graviola/src/low/aarch64/ghash.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,18 @@ impl<'a> Ghash<'a> {
102102
}
103103
}
104104

105-
pub(crate) fn into_bytes(self) -> [u8; 16] {
106-
to_u128(self.current).to_be_bytes()
105+
#[cfg(test)]
106+
fn into_bytes(self) -> [u8; 16] {
107+
self.into_u128().to_be_bytes()
107108
}
108109

109-
fn one_block(&mut self, block: u128) {
110+
pub(crate) fn into_u128(self) -> u128 {
111+
// SAFETY: u128 and uint64x2_t have the same size and meaning of bits
112+
unsafe { mem::transmute(self.current) }
113+
}
114+
115+
// Input the 16 bytes of `block` to the computation.
116+
pub(crate) fn one_block(&mut self, block: u128) {
110117
// SAFETY: this crate requires the `neon` cpu feature
111118
self.current = unsafe { veorq_u64(self.current, from_u128(block)) };
112119
self.current = mul(self.current, self.table.powers[0]);
@@ -326,12 +333,6 @@ fn from_u128(u: u128) -> uint64x2_t {
326333
unsafe { mem::transmute(u) }
327334
}
328335

329-
#[inline]
330-
fn to_u128(u: uint64x2_t) -> u128 {
331-
// SAFETY: u128 and uint64x2_t have the same size and meaning of bits
332-
unsafe { mem::transmute(u) }
333-
}
334-
335336
#[inline]
336337
#[target_feature(enable = "neon")]
337338
// Make a copy of `u` with the bytes reversed, and cast to `uint64x2_t`.

graviola/src/low/generic/ct_equal.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Written for Graviola by Joe Birr-Pixton, 2024.
22
// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0
33

4-
use crate::low::ct_compare_bytes;
4+
use crate::low::{ct_compare_bytes, ct_compare_u128};
55

66
pub(crate) fn ct_equal(a: &[u8], b: &[u8]) -> bool {
77
if a.len() != b.len() {
@@ -12,3 +12,7 @@ pub(crate) fn ct_equal(a: &[u8], b: &[u8]) -> bool {
1212
let diff = unsafe { ct_compare_bytes(a.as_ptr(), b.as_ptr(), a.len()) };
1313
diff == 0
1414
}
15+
16+
pub(crate) fn ct_equal_u128(a: u128, b: u128) -> bool {
17+
(unsafe { ct_compare_u128(a, b) }) == 0
18+
}

graviola/src/low/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ mod posint;
3232
pub(crate) use entry::Entry;
3333
pub(crate) use generic::blockwise::Blockwise;
3434
pub(crate) use generic::ct_copy::{ct_copy, ct_select_i16};
35-
pub(crate) use generic::ct_equal::ct_equal;
35+
pub(crate) use generic::ct_equal::{ct_equal, ct_equal_u128};
3636
pub(crate) use generic::poly1305;
3737
pub(crate) use generic::zeroise::{zeroise, zeroise_value};
3838
pub(crate) use posint::{PosInt, SecretPosInt};
@@ -45,7 +45,7 @@ cfg_if::cfg_if! {
4545
if #[cfg(target_arch = "x86_64")] {
4646
mod x86_64;
4747

48-
pub(in crate::low) use x86_64::cpu::{enter_cpu_state, zero_bytes, ct_compare_bytes, leave_cpu_state, verify_cpu_features};
48+
pub(in crate::low) use x86_64::cpu::{enter_cpu_state, zero_bytes, ct_compare_bytes, ct_compare_u128, leave_cpu_state, verify_cpu_features};
4949
pub(crate) use x86_64::chacha20;
5050
pub(crate) use x86_64::aes::AesKey;
5151
pub(crate) use x86_64::aes_gcm;
@@ -127,7 +127,7 @@ cfg_if::cfg_if! {
127127
} else if #[cfg(target_arch = "aarch64")] {
128128
mod aarch64;
129129

130-
pub(in crate::low) use aarch64::cpu::{enter_cpu_state, zero_bytes, ct_compare_bytes, leave_cpu_state, verify_cpu_features};
130+
pub(in crate::low) use aarch64::cpu::{enter_cpu_state, zero_bytes, ct_compare_bytes, ct_compare_u128, leave_cpu_state, verify_cpu_features};
131131
pub(crate) use aarch64::aes::AesKey;
132132
pub(crate) use aarch64::aes_gcm;
133133
pub(crate) use aarch64::bignum_add::bignum_add;

graviola/src/low/x86_64/cpu.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0
33

44
use core::arch::x86_64::*;
5+
use core::mem;
56

67
pub(crate) fn enter_cpu_state() -> u32 {
78
// DOIT: "Data Operand Independent Timing" -- turning this on
@@ -115,6 +116,17 @@ pub(in crate::low) unsafe fn ct_compare_bytes(a: *const u8, b: *const u8, len: u
115116
acc
116117
}
117118

119+
/// Compare `a` and `b` in constant time, returning 0 if they are equal
120+
/// and some nonzero value if they are not equal.
121+
#[target_feature(enable = "sse2")]
122+
pub(in crate::low) fn ct_compare_u128(a: u128, b: u128) -> u64 {
123+
let a: __m128i = unsafe { mem::transmute(a) };
124+
let b: __m128i = unsafe { mem::transmute(b) };
125+
let cmp = _mm_cmpeq_epi8(a, b);
126+
let mask = _mm_movemask_epi8(cmp) as u16;
127+
!mask as _
128+
}
129+
118130
/// This macro interdicts is_x86_feature_detected to
119131
/// allow testability.
120132
macro_rules! have_cpu_feature {

graviola/src/low/x86_64/ghash.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ impl<'a> Ghash<'a> {
206206

207207
for chunk in whole_blocks.by_ref() {
208208
let u = u128::from_be_bytes(chunk.try_into().unwrap());
209-
self.one_block(u128_to_m128i(u));
209+
self.one_block(u);
210210
}
211211

212212
let bytes = whole_blocks.remainder();
@@ -215,15 +215,22 @@ impl<'a> Ghash<'a> {
215215
block[..bytes.len()].copy_from_slice(bytes);
216216

217217
let u = u128::from_be_bytes(block);
218-
self.one_block(u128_to_m128i(u));
218+
self.one_block(u);
219219
}
220220
}
221221

222+
#[cfg(test)]
222223
pub(crate) fn into_bytes(self) -> [u8; 16] {
223224
// SAFETY: this crate requires the `sse2` and `ssse3` cpu features
224225
unsafe { self._into_bytes() }
225226
}
226227

228+
pub(crate) fn into_u128(self) -> u128 {
229+
// SAFETY: sizeof(u128) == sizeof(__m128i), all bits have same meaning
230+
unsafe { mem::transmute(self.current) }
231+
}
232+
233+
#[cfg(test)]
227234
#[target_feature(enable = "sse2,ssse3")]
228235
fn _into_bytes(self) -> [u8; 16] {
229236
let mut out: i128 = 0;
@@ -233,7 +240,9 @@ impl<'a> Ghash<'a> {
233240
out.to_le_bytes()
234241
}
235242

236-
fn one_block(&mut self, block: __m128i) {
243+
// Input the 16 bytes of `block` to the computation.
244+
pub(crate) fn one_block(&mut self, block: u128) {
245+
let block = u128_to_m128i(block);
237246
// SAFETY: this crate requires the `avx` and `pclmulqdq` cpu features
238247
unsafe {
239248
self.current = _mm_xor_si128(self.current, block);

graviola/src/mid/aes_gcm.rs

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use crate::Error;
55
use crate::low::ghash::{Ghash, GhashTable};
6-
use crate::low::{AesKey, Entry, aes_gcm, ct_equal};
6+
use crate::low::{AesKey, Entry, aes_gcm, ct_equal_u128};
77

88
/// An AES-GCM key.
99
///
@@ -66,16 +66,13 @@ impl AesGcm {
6666
// computations. see low::generic::aes_gcm for model version.
6767
aes_gcm::encrypt(&self.key, &mut ghash, &counter, aad, cipher_inout);
6868

69-
let mut lengths = [0u8; 16];
70-
lengths[..8].copy_from_slice(&((aad.len() * 8) as u64).to_be_bytes());
71-
lengths[8..].copy_from_slice(&((cipher_inout.len() * 8) as u64).to_be_bytes());
72-
ghash.add(&lengths);
69+
let lengths: u128 = Self::pack_bit_lengths(aad, cipher_inout);
70+
ghash.one_block(lengths);
7371

74-
let final_xi = ghash.into_bytes();
75-
76-
for ((out, x), e) in tag_out.iter_mut().zip(final_xi.iter()).zip(e_y0.iter()) {
77-
*out = *x ^ *e;
78-
}
72+
let final_xi = ghash.into_u128();
73+
let e = u128::from_be_bytes(e_y0);
74+
let tag = final_xi ^ e;
75+
tag_out.copy_from_slice(&tag.to_be_bytes());
7976
}
8077

8178
/// Decrypts and verifies the given message.
@@ -107,23 +104,33 @@ impl AesGcm {
107104

108105
aes_gcm::decrypt(&self.key, &mut ghash, &counter, aad, cipher_inout);
109106

110-
let mut lengths = [0u8; 16];
111-
lengths[..8].copy_from_slice(&((aad.len() * 8) as u64).to_be_bytes());
112-
lengths[8..].copy_from_slice(&((cipher_inout.len() * 8) as u64).to_be_bytes());
113-
ghash.add(&lengths);
114-
115-
let mut actual_tag = ghash.into_bytes();
116-
for (out, e) in actual_tag.iter_mut().zip(e_y0.iter()) {
117-
*out ^= *e;
107+
let lengths: u128 = Self::pack_bit_lengths(aad, cipher_inout);
108+
ghash.one_block(lengths);
109+
110+
let actual_tag = ghash.into_u128() ^ u128::from_be_bytes(e_y0);
111+
let tag: Result<[u8; 16], _> = tag.try_into();
112+
match tag {
113+
Ok(tag) => {
114+
// TODO add a constant-time u128 comparison function
115+
if ct_equal_u128(u128::from_be_bytes(tag), actual_tag) {
116+
Ok(())
117+
} else {
118+
// avoid unauthenticated plaintext leak
119+
cipher_inout.fill(0x00);
120+
Err(Error::DecryptFailed)
121+
}
122+
},
123+
Err(_) => {
124+
cipher_inout.fill(0x00);
125+
Err(Error::DecryptFailed)
126+
}
118127
}
128+
}
119129

120-
if ct_equal(&actual_tag, tag) {
121-
Ok(())
122-
} else {
123-
// avoid unauthenticated plaintext leak
124-
cipher_inout.fill(0x00);
125-
Err(Error::DecryptFailed)
126-
}
130+
fn pack_bit_lengths(high: &[u8], low: &[u8]) -> u128 {
131+
let lengths = (high.len() * 8) as u128;
132+
let lengths = lengths << 64;
133+
lengths | ((low.len() * 8) as u128)
127134
}
128135

129136
fn nonce_to_y0(&self, nonce: &[u8; 12]) -> [u8; 16] {

0 commit comments

Comments
 (0)