Skip to main content

binius_field/arch/portable/arithmetic/
ghash.rs

1// Copyright 2023-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! Portable (software) implementation of GHASH field multiplication.
5
6use std::{
7	iter::Sum,
8	ops::{Add, AddAssign, Sub, SubAssign},
9};
10
11use bytemuck::TransparentWrapper;
12
13use super::super::univariate_mul_utils_128::{Underlier64bLanes, Underlier128bLanes, bmul64};
14use crate::{
15	BinaryField, Divisible, Ghash128b, WideMul,
16	arithmetic_traits::{MulX, Square},
17	packed_fields::primitive::PackedPrimitiveType,
18	underlier::Underlier,
19};
20
21/// The reduction polynomial `X^128 + X^7 + X^2 + X + 1`, with its `X^128` term left implicit.
22pub const POLY: u128 = 0x87;
23
24/// Scales one GHASH element by `X`, over `u128`. The scalar reference.
25#[inline]
26pub const fn ghash_mul_x(x: u128) -> u128 {
27	// Why: shifting up one degree can push a term to X^128, which the modulus rewrites. Negating
28	// bit 127, read as 0 or 1, gives a mask of all zeros or all ones, so one exclusive or covers
29	// both cases.
30	let mask = (x >> 127).wrapping_neg();
31
32	(x << 1) ^ (POLY & mask)
33}
34
35/// Multiply two GHASH field elements using software implementation.
36///
37/// Method described at:
38/// * <https://www.bearssl.org/constanttime.html#ghash-for-gcm>
39/// * <https://crypto.stackexchange.com/questions/66448/how-does-bearssls-gcm-modular-reduction-work/66462#66462>
40///
41/// This code does not conform to the bit-endianness requirements of the GCM specification, but is
42/// a valid GHASH field multiplication with the modified representation.
43#[inline]
44pub fn ghash_mul<U: Underlier128bLanes>(x: U, y: U) -> U {
45	ghash_wide_mul(x, y).reduce()
46}
47
48/// Widening multiply: the schoolbook polynomial product of two GHASH field elements, without the
49/// modular reduction. The unreduced result can be accumulated by XOR and reduced once at the end
50/// via [`WideGhashProduct::reduce`].
51#[inline]
52pub fn ghash_wide_mul<U: Underlier128bLanes>(x: U, y: U) -> WideGhashProduct<U> {
53	// Convert to U64x2 representation
54	let (x1, x0) = U::split_hi_lo_64(x);
55	let (y1, y0) = U::split_hi_lo_64(y);
56
57	// Perform multiplication
58	let x0r = x0.reverse_bits_64();
59	let x1r = x1.reverse_bits_64();
60	let x2 = x0 ^ x1;
61	let x2r = x0r ^ x1r;
62
63	let y0r = y0.reverse_bits_64();
64	let y1r = y1.reverse_bits_64();
65	let y2 = y0 ^ y1;
66	let y2r = y0r ^ y1r;
67
68	let z0 = bmul64(y0, x0);
69	let z1 = bmul64(y1, x1);
70	let mut z2 = bmul64(y2, x2);
71
72	let mut z0h = bmul64(y0r, x0r);
73	let mut z1h = bmul64(y1r, x1r);
74	let mut z2h = bmul64(y2r, x2r);
75
76	z2 ^= z0 ^ z1;
77	z2h ^= z0h ^ z1h;
78	z0h = z0h.reverse_bits_64().shr_64(1);
79	z1h = z1h.reverse_bits_64().shr_64(1);
80	z2h = z2h.reverse_bits_64().shr_64(1);
81
82	WideGhashProduct {
83		v0: z0,
84		v1: z0h ^ z2,
85		v2: z1 ^ z2h,
86		v3: z1h,
87	}
88}
89
90#[inline]
91pub fn ghash_square<U: Underlier128bLanes>(x: U) -> U {
92	// Squared value in the polynomial basis is just a value with bits interleaved with zeroes.
93	let (hi, lo) = x.spread_bits_128();
94
95	let (v3, v2) = hi.split_hi_lo_64();
96	let (v1, v0) = lo.split_hi_lo_64();
97
98	reduce_64(v0, v1, v2, v3)
99}
100
101/// Reduce a 256-bit value represented as four 64-bit values by the GHASH polynomial.
102#[inline]
103fn reduce_64<U: Underlier128bLanes>(
104	mut v0: U::U64,
105	mut v1: U::U64,
106	mut v2: U::U64,
107	v3: U::U64,
108) -> U {
109	// Reduce modulo X^64 + X^7 + X^2 + X + 1.
110	v1 ^= v3 ^ v3.shl_64(1) ^ v3.shl_64(2) ^ v3.shl_64(7);
111	v2 ^= v3.shr_64(63) ^ v3.shr_64(62) ^ v3.shr_64(57);
112	v0 ^= v2 ^ v2.shl_64(1) ^ v2.shl_64(2) ^ v2.shl_64(7);
113	v1 ^= v2.shr_64(63) ^ v2.shr_64(62) ^ v2.shr_64(57);
114
115	// Convert back to 128-bit lanes
116	U::join_u64s(v1, v0)
117}
118
119/// An unreduced GHASH product, stored as the four 64-bit limbs `(v0, v1, v2, v3)` of the 256-bit
120/// schoolbook product. Values of this type can be summed by XOR and reduced once at the end via
121/// [`reduce`](WideGhashProduct::reduce).
122#[derive(Clone, Copy, Default, Debug)]
123pub struct WideGhashProduct<U: Underlier128bLanes> {
124	v0: U::U64,
125	v1: U::U64,
126	v2: U::U64,
127	v3: U::U64,
128}
129
130impl<U: Underlier128bLanes> WideGhashProduct<U> {
131	/// Reduce the accumulated wide product to a single GF(2^128) element.
132	#[inline]
133	pub fn reduce(self) -> U {
134		reduce_64(self.v0, self.v1, self.v2, self.v3)
135	}
136}
137
138impl<U: Underlier128bLanes> MulX for WideGhashProduct<U> {
139	/// Shifts the 256-bit schoolbook product left by one bit, carrying between the four 64-bit
140	/// limbs.
141	///
142	/// The product of two 128-bit polynomials has degree at most 254, and XOR-accumulating such
143	/// products preserves that, so the top bit of `v3` is always clear and nothing is shifted out.
144	#[inline]
145	fn mul_x(self) -> Self {
146		Self {
147			v0: self.v0.shl_64(1),
148			v1: self.v1.shl_64(1) ^ self.v0.shr_64(63),
149			v2: self.v2.shl_64(1) ^ self.v1.shr_64(63),
150			v3: self.v3.shl_64(1) ^ self.v2.shr_64(63),
151		}
152	}
153}
154
155impl<U: Underlier128bLanes> Add for WideGhashProduct<U> {
156	type Output = Self;
157
158	#[inline]
159	fn add(self, rhs: Self) -> Self {
160		Self {
161			v0: self.v0 ^ rhs.v0,
162			v1: self.v1 ^ rhs.v1,
163			v2: self.v2 ^ rhs.v2,
164			v3: self.v3 ^ rhs.v3,
165		}
166	}
167}
168
169impl<U: Underlier128bLanes> AddAssign for WideGhashProduct<U> {
170	#[inline]
171	fn add_assign(&mut self, rhs: Self) {
172		self.v0 ^= rhs.v0;
173		self.v1 ^= rhs.v1;
174		self.v2 ^= rhs.v2;
175		self.v3 ^= rhs.v3;
176	}
177}
178
179impl<U: Underlier128bLanes> Sum for WideGhashProduct<U> {
180	#[inline]
181	fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
182		iter.fold(Self::default(), |acc, x| acc + x)
183	}
184}
185
186// In characteristic 2, subtraction is identical to addition (XOR).
187impl<U: Underlier128bLanes> Sub for WideGhashProduct<U> {
188	type Output = Self;
189
190	#[inline]
191	fn sub(self, rhs: Self) -> Self {
192		Self {
193			v0: self.v0 ^ rhs.v0,
194			v1: self.v1 ^ rhs.v1,
195			v2: self.v2 ^ rhs.v2,
196			v3: self.v3 ^ rhs.v3,
197		}
198	}
199}
200
201impl<U: Underlier128bLanes> SubAssign for WideGhashProduct<U> {
202	#[inline]
203	fn sub_assign(&mut self, rhs: Self) {
204		self.v0 ^= rhs.v0;
205		self.v1 ^= rhs.v1;
206		self.v2 ^= rhs.v2;
207		self.v3 ^= rhs.v3;
208	}
209}
210
211/// Widening-multiply wrapper for the portable GHASH packing.
212///
213/// [`wide_mul`](WideMul::wide_mul) computes the unreduced schoolbook product via
214/// [`ghash_wide_mul`], and [`reduce`](WideMul::reduce) performs the GHASH modular reduction. This
215/// defers the reduction so a sum of products is reduced only once.
216#[repr(transparent)]
217#[derive(bytemuck::TransparentWrapper)]
218pub struct GhashWideMul<T>(T);
219
220impl<U: Underlier128bLanes> WideMul for GhashWideMul<PackedPrimitiveType<U, Ghash128b>> {
221	type Output = WideGhashProduct<U>;
222
223	#[inline]
224	fn wide_mul(a: Self, b: Self) -> Self::Output {
225		let a = PackedPrimitiveType::peel(Self::peel(a));
226		let b = PackedPrimitiveType::peel(Self::peel(b));
227		ghash_wide_mul(a, b)
228	}
229
230	#[inline]
231	fn reduce(wide: Self::Output) -> Self {
232		Self::wrap(PackedPrimitiveType::wrap(wide.reduce()))
233	}
234}
235
236/// Square strategy wrapper for the software GHASH implementation.
237///
238/// Shared by the portable and wasm32 packings and used by the x86_64 packing when CLMUL is
239/// unavailable. Squares via the bit-spread [`ghash_square`], which interleaves the input bits with
240/// zeroes and reduces — no carryless multiply required.
241/// Scaling wrapper for the GHASH packings with no vector shift for their width.
242///
243/// Walks the 128-bit lanes and applies the scalar shift to each, so it serves every width.
244#[repr(transparent)]
245#[derive(TransparentWrapper)]
246pub struct GhashMulX<T>(T);
247
248impl<U: Underlier + Divisible<u128>, F: BinaryField> MulX for GhashMulX<PackedPrimitiveType<U, F>> {
249	#[inline]
250	fn mul_x(self) -> Self {
251		let lanes = Divisible::<u128>::value_iter(PackedPrimitiveType::peel(Self::peel(self)))
252			.map(ghash_mul_x);
253
254		Self::wrap(PackedPrimitiveType::wrap(Divisible::<u128>::from_iter(lanes)))
255	}
256}
257
258#[repr(transparent)]
259#[derive(TransparentWrapper)]
260pub struct GhashSoftMul<T>(T);
261
262impl<U: Underlier128bLanes> Square for GhashSoftMul<PackedPrimitiveType<U, Ghash128b>> {
263	#[inline]
264	fn square(self) -> Self {
265		Self::wrap(PackedPrimitiveType::wrap(ghash_square(PackedPrimitiveType::peel(Self::peel(
266			self,
267		)))))
268	}
269}
270
271#[cfg(test)]
272mod tests {
273	use proptest::{prelude::any, proptest};
274
275	use super::{super::super::m128::M128, MulX, ghash_mul, ghash_wide_mul};
276
277	// Exercises the deferred wide-mul building blocks (`ghash_wide_mul` + `WideGhashProduct`) that
278	// `GhashWideMul` wraps, directly on the portable `M128`. This runs on every host, whereas the
279	// portable `PackedGhash1x128b` is only a usable `PackedField` on targets where it is the
280	// re-exported b128 type (covered there by the proptests in `packed_fields::ghash`).
281	proptest! {
282		// The split must agree with the fused multiply: wide-multiply then reduce == ghash_mul.
283		#[test]
284		fn wide_mul_then_reduce_matches_ghash_mul(a in any::<u128>(), b in any::<u128>()) {
285			let (a, b) = (M128::from(a), M128::from(b));
286			assert_eq!(ghash_wide_mul(a, b).reduce(), ghash_mul(a, b));
287		}
288
289		// Accumulate two unreduced products and reduce once.
290		#[test]
291		fn wide_mul_deferred_accumulation(
292			a1 in any::<u128>(), b1 in any::<u128>(),
293			a2 in any::<u128>(), b2 in any::<u128>(),
294		) {
295			let (a1, b1) = (M128::from(a1), M128::from(b1));
296			let (a2, b2) = (M128::from(a2), M128::from(b2));
297			let acc = ghash_wide_mul(a1, b1) + ghash_wide_mul(a2, b2);
298			assert_eq!(acc.reduce(), ghash_mul(a1, b1) ^ ghash_mul(a2, b2));
299		}
300
301		// Scaling by X commutes with the reduction: scaling the unreduced product matches
302		// multiplying the reduced product by X (the field element 2).
303		#[test]
304		fn mul_x_wide_commutes_with_reduce(a in any::<u128>(), b in any::<u128>()) {
305			let (a, b) = (M128::from(a), M128::from(b));
306			let wide = ghash_wide_mul(a, b);
307			assert_eq!(wide.mul_x().reduce(), ghash_mul(wide.reduce(), M128::from(2u128)));
308		}
309	}
310}