Skip to main content

polyval/backend/intrinsics/
x86.rs

1//! VPCLMULQDQ optimized POLYVAL implementation using R/F Algorithm
2//! Adapted from the implementation in the Apache 2.0 + MIT-licensed HPCrypt library
3//! Copyright (c) 2024 HPCrypt Contributors
4//!
5//! Uses the R/F algorithm from "Efficient GHASH Implementation Using CLMUL":
6//! - 4 CLMULs per block for multiplication (R and F terms)
7//! - 1 CLMUL for reduction (Lemma 3)
8//! - 4-block aggregated processing with single reduction
9//!
10//! Key equations:
11//! - D = swap(H) ⊕ (H0 × P1)
12//! - R = M0×D1 ⊕ M1×H1
13//! - F = M0×D0 ⊕ M1×H0
14//! - Result = R ⊕ F1 ⊕ (x^64×F0) ⊕ (P1×F0)
15//!
16//! POLYVAL operates in GF(2^128) with polynomial x^128 + x^127 + x^126 + x^121 + 1
17//! Unlike GHASH, POLYVAL uses little-endian byte ordering (no byte swap needed).
18//!
19//! <https://eprint.iacr.org/2025/2171.pdf>
20
21#![allow(unsafe_op_in_unsafe_fn)]
22
23use super::ExpandedKey;
24use crate::{Block, ParBlocks, field_element::FieldElement};
25
26#[cfg(target_arch = "x86")]
27use core::arch::x86::*;
28#[cfg(target_arch = "x86_64")]
29use core::arch::x86_64::*;
30
31/// P1 polynomial: x^63 + x^62 + x^57 = 0xC200000000000000
32const P1: u64 = 0xC200000000000000;
33
34cpufeatures::new!(clmul, "avx", "pclmulqdq");
35pub(crate) use clmul::InitToken;
36
37/// Byte array which is the inner type of `FieldElement`
38type ByteArray = [u8; 16];
39
40impl FieldElement {
41    #[target_feature(enable = "sse2")]
42    #[inline]
43    unsafe fn from_m128i(reg: __m128i) -> Self {
44        let mut out = ByteArray::default();
45        _mm_storeu_si128(out.as_mut_ptr().cast(), reg);
46        out.into()
47    }
48
49    #[target_feature(enable = "sse2")]
50    #[inline]
51    unsafe fn to_m128i(self) -> __m128i {
52        load_bytes(&self.into())
53    }
54}
55
56/// Convert 16 bytes into `__m128i`.
57///
58/// # Safety
59/// Requires SSE2 support
60#[target_feature(enable = "sse2")]
61#[inline]
62unsafe fn load_bytes(bytes: &ByteArray) -> __m128i {
63    _mm_loadu_si128(bytes.as_ptr().cast())
64}
65
66/// Update with a single block (5 CLMULs)
67///
68/// # Safety
69/// Requires PCLMULQDQ support with VEX operands.
70#[target_feature(enable = "avx", enable = "pclmulqdq")]
71#[inline]
72pub(super) unsafe fn proc_block(
73    key: &ExpandedKey,
74    acc: FieldElement,
75    block: &Block,
76) -> FieldElement {
77    let data = load_bytes(&block.0);
78
79    // XOR with accumulator
80    let y = _mm_xor_si128(acc.to_m128i(), data);
81
82    // Multiply by H using R/F algorithm
83    FieldElement::from_m128i(gf128_mul_rf(y, key.h1.to_m128i(), key.d1.to_m128i()))
84}
85
86/// Process 4 blocks with R/F algorithm and aggregated reduction
87///
88/// Uses 16 CLMULs for multiplication (4 per block) + 1 CLMUL for reduction = 17 CLMULs total
89#[target_feature(enable = "avx", enable = "pclmulqdq")]
90#[inline]
91pub(super) unsafe fn proc_par_blocks(
92    key: &ExpandedKey,
93    acc: FieldElement,
94    par_blocks: &ParBlocks,
95) -> FieldElement {
96    // Load all 4 blocks (no byte swap for POLYVAL)
97    let m0 = load_bytes(&par_blocks[0].0);
98    let m1 = load_bytes(&par_blocks[1].0);
99    let m2 = load_bytes(&par_blocks[2].0);
100    let m3 = load_bytes(&par_blocks[3].0);
101
102    // XOR first block with accumulator
103    let y0 = _mm_xor_si128(acc.to_m128i(), m0);
104
105    // R/F multiply all 4 blocks (16 CLMULs)
106    let (r0, f0) = rf_mul_unreduced(y0, key.h4.to_m128i(), key.d4.to_m128i());
107    let (r1, f1) = rf_mul_unreduced(m1, key.h3.to_m128i(), key.d3.to_m128i());
108    let (r2, f2) = rf_mul_unreduced(m2, key.h2.to_m128i(), key.d2.to_m128i());
109    let (r3, f3) = rf_mul_unreduced(m3, key.h1.to_m128i(), key.d1.to_m128i());
110
111    // Aggregate R and F values
112    let r = _mm_xor_si128(_mm_xor_si128(r0, r1), _mm_xor_si128(r2, r3));
113    let f = _mm_xor_si128(_mm_xor_si128(f0, f1), _mm_xor_si128(f2, f3));
114
115    // Single reduction (1 CLMUL)
116    FieldElement::from_m128i(reduce_rf(r, f))
117}
118
119/// Create a new POLYVAL key with R/F algorithm
120///
121/// # Safety
122/// Requires PCLMULQDQ support with VEX operands.
123#[target_feature(enable = "avx", enable = "pclmulqdq")]
124pub(super) unsafe fn expand_key(h: &[u8; 16]) -> ExpandedKey {
125    let h1 = load_bytes(h);
126    let d1 = compute_d(h1);
127
128    // Compute powers using R/F multiplication
129    let h2 = gf128_mul_rf(h1, h1, d1);
130    let d2 = compute_d(h2);
131
132    let h3 = gf128_mul_rf(h2, h1, d1);
133    let d3 = compute_d(h3);
134
135    let h4 = gf128_mul_rf(h2, h2, d2);
136    let d4 = compute_d(h4);
137
138    ExpandedKey {
139        h1: FieldElement::from_m128i(h1),
140        d1: FieldElement::from_m128i(d1),
141        h2: FieldElement::from_m128i(h2),
142        d2: FieldElement::from_m128i(d2),
143        h3: FieldElement::from_m128i(h3),
144        d3: FieldElement::from_m128i(d3),
145        h4: FieldElement::from_m128i(h4),
146        d4: FieldElement::from_m128i(d4),
147    }
148}
149
150/// Compute D from H using the R/F algorithm
151///
152/// D = swap(H) ⊕ (H0 × P1)
153#[target_feature(enable = "avx", enable = "pclmulqdq")]
154#[inline]
155unsafe fn compute_d(h: __m128i) -> __m128i {
156    // TODO(tarcieri): P1.cast_signed() when MSRV 1.87+
157    #[allow(clippy::cast_possible_wrap)]
158    let p = _mm_set_epi64x(P1 as i64, 0);
159
160    // Swap halves: [H1 : H0] -> [H0 : H1]
161    let h_swap = _mm_shuffle_epi32(h, 0x4e);
162
163    // T = H0 × P1
164    let t = _mm_clmulepi64_si128(h, p, 0x10);
165
166    // D = swap(H) ⊕ T
167    _mm_xor_si128(h_swap, t)
168}
169
170/// R/F multiplication using 4 CLMULs per block
171///
172/// Given M = [M1 : M0] and precomputed H = [H1 : H0], D = [D1 : D0]:
173/// - R = M0×D1 ⊕ M1×H1 (2 CLMULs)
174/// - F = M0×D0 ⊕ M1×H0 (2 CLMULs)
175///
176/// Returns (R, F) for later reduction
177#[target_feature(enable = "avx", enable = "pclmulqdq")]
178#[inline]
179unsafe fn rf_mul_unreduced(m: __m128i, h: __m128i, d: __m128i) -> (__m128i, __m128i) {
180    // R = M0×D1 ⊕ M1×H1
181    let r0 = _mm_clmulepi64_si128(m, d, 0x10); // M0 × D1
182    let r1 = _mm_clmulepi64_si128(m, h, 0x11); // M1 × H1
183    let r = _mm_xor_si128(r0, r1);
184
185    // F = M0×D0 ⊕ M1×H0
186    let f0 = _mm_clmulepi64_si128(m, d, 0x00); // M0 × D0
187    let f1 = _mm_clmulepi64_si128(m, h, 0x01); // M1 × H0
188    let f = _mm_xor_si128(f0, f1);
189
190    (r, f)
191}
192
193/// Reduction using Lemma 3: Result = R ⊕ F1 ⊕ (x^64×F0) ⊕ (P1×F0)
194///
195/// Uses 1 CLMUL for reduction
196#[target_feature(enable = "avx", enable = "pclmulqdq")]
197#[inline]
198unsafe fn reduce_rf(r: __m128i, f: __m128i) -> __m128i {
199    // TODO(tarcieri): P1.cast_signed() when MSRV 1.87+
200    #[allow(clippy::cast_possible_wrap)]
201    let p1 = _mm_set_epi64x(0, P1 as i64);
202
203    // F1 in low position
204    let f1 = _mm_srli_si128(f, 8);
205
206    // x^64×F0 (shift F0 to high position)
207    let f0_shifted = _mm_slli_si128(f, 8);
208
209    // P1×F0
210    let p1_f0 = _mm_clmulepi64_si128(f, p1, 0x00);
211
212    // Result = R ⊕ F1 ⊕ (x^64×F0) ⊕ (P1×F0)
213    let result = _mm_xor_si128(r, f1);
214    let result = _mm_xor_si128(result, f0_shifted);
215    _mm_xor_si128(result, p1_f0)
216}
217
218/// Complete R/F multiplication with reduction (5 CLMULs total)
219#[target_feature(enable = "avx", enable = "pclmulqdq")]
220#[inline]
221unsafe fn gf128_mul_rf(m: __m128i, h: __m128i, d: __m128i) -> __m128i {
222    let (r, f) = rf_mul_unreduced(m, h, d);
223    reduce_rf(r, f)
224}