ic_cipher/polyval.rs
1//! POLYVAL, the universal hash underneath AES-GCM-SIV (RFC 8452 section 3).
2//!
3//! POLYVAL and GHASH are the same field with the bits written down in opposite
4//! orders. GHASH reads a 16-byte block as a polynomial with the most
5//! significant coefficient first; POLYVAL reads it least-significant first, and
6//! reduces modulo `x^128 + x^127 + x^126 + x^121 + 1` instead of GHASH's
7//! `x^128 + x^7 + x^2 + x + 1`. The two polynomials are each other's reverse,
8//! which is why the same hardware instruction serves both.
9//!
10//! # Two implementations, on purpose
11//!
12//! RFC 8452 Appendix A states the relationship exactly:
13//!
14//! ```text
15//! POLYVAL(H, X_1, ..., X_n) =
16//! ByteReverse(GHASH(mulX_GHASH(ByteReverse(H)),
17//! ByteReverse(X_1), ..., ByteReverse(X_n)))
18//! ```
19//!
20//! So POLYVAL can be computed two entirely different ways: directly in its own
21//! field, or by reversing bytes and borrowing GHASH. This module implements the
22//! first; the tests implement the second over [`crate::gcm`]'s GHASH, which is
23//! validated against published GCM vectors. Agreement between them is real
24//! evidence rather than a round trip, and it is the one part of AES-GCM-SIV
25//! that gets such evidence — see the `gcm_siv` module documentation for what
26//! does not.
27//!
28//! # Constant time
29//!
30//! The multiplication is bit-by-bit with a mask, never a table lookup and never
31//! a branch on data, for the same reason [`crate::gcm`]'s GHASH is: a
32//! table-driven implementation leaks the key through the cache.
33
34use ic_core::Zeroize;
35
36/// Block size, which is also the field element width.
37pub const BLOCK_LEN: usize = 16;
38
39/// Multiply two POLYVAL field elements: `a * b * x^-128`.
40///
41/// Both operands are little-endian: bit `i` of the 128-bit value is the
42/// coefficient of `x^i`, and byte 0 holds bits 0 through 7.
43///
44/// The `x^-128` factor is POLYVAL's convention, and it is what makes this field
45/// line up with GHASH's under byte reversal. It is applied here as 128 explicit
46/// divisions by `x` after a full 256-bit carry-less product — the slow, obvious
47/// way, chosen because the fast way is where implementations go wrong and the
48/// cost is paid once per block either way.
49fn mul(a: &[u8; BLOCK_LEN], b: &[u8; BLOCK_LEN]) -> [u8; BLOCK_LEN] {
50 let a0 = u64::from_le_bytes(a[0..8].try_into().unwrap());
51 let a1 = u64::from_le_bytes(a[8..16].try_into().unwrap());
52 let b0 = u64::from_le_bytes(b[0..8].try_into().unwrap());
53 let b1 = u64::from_le_bytes(b[8..16].try_into().unwrap());
54
55 // Carry-less product into 256 bits. `v` holds `a << i`, widened as it goes.
56 let mut z = [0u64; 4];
57 let mut v = [a0, a1, 0u64, 0u64];
58 for i in 0..128 {
59 let bit = if i < 64 {
60 (b0 >> i) & 1
61 } else {
62 (b1 >> (i - 64)) & 1
63 };
64 let mask = 0u64.wrapping_sub(bit);
65 for (zw, vw) in z.iter_mut().zip(v.iter()) {
66 *zw ^= *vw & mask;
67 }
68 v[3] = (v[3] << 1) | (v[2] >> 63);
69 v[2] = (v[2] << 1) | (v[1] >> 63);
70 v[1] = (v[1] << 1) | (v[0] >> 63);
71 v[0] <<= 1;
72 }
73
74 reduce(z)
75}
76
77/// `x^-1` in POLYVAL's field, as the high word of a little-endian 128-bit value.
78///
79/// Derived rather than recalled. The modulus is
80/// `x^128 + x^127 + x^126 + x^121 + 1`, so in characteristic two
81///
82/// ```text
83/// 1 = x^128 + x^127 + x^126 + x^121 = x * (x^127 + x^126 + x^125 + x^120)
84/// ```
85///
86/// which makes `x^-1 = x^127 + x^126 + x^125 + x^120`. Bits 127, 126, 125 and
87/// 120 sit at positions 63, 62, 61 and 56 of the high word, giving the constant
88/// below. A different POLYVAL implementation will show a different constant
89/// because it reduces a different way; this one belongs to division by `x`.
90const X_INVERSE_HIGH: u64 = (1 << 63) | (1 << 62) | (1 << 61) | (1 << 56);
91
92/// Multiply a 256-bit carry-less product by `x^-128`, reducing modulo POLYVAL's
93/// polynomial.
94fn reduce(mut acc: [u64; 4]) -> [u8; BLOCK_LEN] {
95 for _ in 0..128 {
96 // Divide by x: shift the whole 256-bit value right one bit, and fold
97 // the coefficient that falls off the bottom back in as x^-1.
98 let carry = acc[0] & 1;
99 acc[0] = (acc[0] >> 1) | (acc[1] << 63);
100 acc[1] = (acc[1] >> 1) | (acc[2] << 63);
101 acc[2] = (acc[2] >> 1) | (acc[3] << 63);
102 acc[3] >>= 1;
103 acc[1] ^= 0u64.wrapping_sub(carry) & X_INVERSE_HIGH;
104 }
105
106 // After 128 divisions the value fits the low 128 bits.
107 debug_assert_eq!(acc[2], 0, "reduction left a high word set");
108 debug_assert_eq!(acc[3], 0, "reduction left a high word set");
109
110 let mut out = [0u8; BLOCK_LEN];
111 out[0..8].copy_from_slice(&acc[0].to_le_bytes());
112 out[8..16].copy_from_slice(&acc[1].to_le_bytes());
113 out
114}
115
116/// A POLYVAL accumulator.
117pub struct Polyval {
118 h: [u8; BLOCK_LEN],
119 acc: [u8; BLOCK_LEN],
120}
121
122impl Polyval {
123 /// Start with the hash key `h`.
124 pub fn new(h: [u8; BLOCK_LEN]) -> Self {
125 Self {
126 h,
127 acc: [0u8; BLOCK_LEN],
128 }
129 }
130
131 /// Absorb one whole block.
132 pub fn update_block(&mut self, block: &[u8; BLOCK_LEN]) {
133 for (a, b) in self.acc.iter_mut().zip(block.iter()) {
134 *a ^= *b;
135 }
136 self.acc = mul(&self.acc, &self.h);
137 }
138
139 /// Absorb a byte string, zero-padding the final partial block.
140 pub fn update_padded(&mut self, data: &[u8]) {
141 for chunk in data.chunks(BLOCK_LEN) {
142 let mut block = [0u8; BLOCK_LEN];
143 block[..chunk.len()].copy_from_slice(chunk);
144 self.update_block(&block);
145 }
146 }
147
148 /// The accumulated value.
149 pub fn finish(self) -> [u8; BLOCK_LEN] {
150 self.acc
151 }
152}
153
154impl Drop for Polyval {
155 fn drop(&mut self) {
156 self.h.zeroize();
157 self.acc.zeroize();
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 /// `ByteReverse` from RFC 8452 Appendix A.
166 fn byte_reverse(x: &[u8; 16]) -> [u8; 16] {
167 let mut out = [0u8; 16];
168 for (i, b) in x.iter().enumerate() {
169 out[15 - i] = *b;
170 }
171 out
172 }
173
174 /// `mulX_GHASH` from RFC 8452 Appendix A: multiply by x in GHASH's field.
175 ///
176 /// GHASH is big-endian in bit order, so "multiply by x" is a right shift of
177 /// the bit string, with the reduction constant folded into the top byte.
178 fn mul_x_ghash(x: &[u8; 16]) -> [u8; 16] {
179 let mut out = [0u8; 16];
180 let mut carry = 0u8;
181 for i in 0..16 {
182 let next = x[i] & 1;
183 out[i] = (x[i] >> 1) | (carry << 7);
184 carry = next;
185 }
186 if carry != 0 {
187 out[0] ^= 0xe1;
188 }
189 out
190 }
191
192 /// POLYVAL computed the other way, through the validated GHASH.
193 ///
194 /// This is RFC 8452 Appendix A's identity, implemented over
195 /// [`crate::gcm::portable_ghash_mul`], which the GCM vectors validate.
196 fn polyval_via_ghash(h: &[u8; 16], blocks: &[[u8; 16]]) -> [u8; 16] {
197 let ghash_h = mul_x_ghash(&byte_reverse(h));
198 let mut acc = [0u8; 16];
199 for block in blocks {
200 let reversed = byte_reverse(block);
201 for (a, b) in acc.iter_mut().zip(reversed.iter()) {
202 *a ^= *b;
203 }
204 crate::gcm::portable_ghash_mul(&mut acc, &ghash_h);
205 }
206 byte_reverse(&acc)
207 }
208
209 /// The whole reason this module implements POLYVAL directly rather than
210 /// borrowing GHASH: two independent routes to the same answer.
211 #[test]
212 fn polyval_matches_the_ghash_construction() {
213 // A small deterministic generator, so a failure is reproducible.
214 let mut state = 0x243f_6a88_85a3_08d3u64;
215 let mut next = || {
216 state ^= state << 13;
217 state ^= state >> 7;
218 state ^= state << 17;
219 state
220 };
221
222 for count in 0..12usize {
223 let mut h = [0u8; 16];
224 h[0..8].copy_from_slice(&next().to_le_bytes());
225 h[8..16].copy_from_slice(&next().to_le_bytes());
226
227 let mut blocks = Vec::new();
228 for _ in 0..count {
229 let mut b = [0u8; 16];
230 b[0..8].copy_from_slice(&next().to_le_bytes());
231 b[8..16].copy_from_slice(&next().to_le_bytes());
232 blocks.push(b);
233 }
234
235 let mut p = Polyval::new(h);
236 for b in &blocks {
237 p.update_block(b);
238 }
239 let direct = p.finish();
240 let via = polyval_via_ghash(&h, &blocks);
241 assert_eq!(
242 direct, via,
243 "POLYVAL disagreed with the GHASH construction at {count} blocks"
244 );
245 }
246 }
247
248 /// Edge cases the random inputs above are unlikely to reach.
249 #[test]
250 fn polyval_handles_degenerate_inputs() {
251 let zero = [0u8; 16];
252 let mut one = [0u8; 16];
253 one[0] = 1;
254
255 for h in [zero, one, [0xffu8; 16]] {
256 for blocks in [vec![], vec![zero], vec![one, zero], vec![[0xffu8; 16]; 3]] {
257 let mut p = Polyval::new(h);
258 for b in &blocks {
259 p.update_block(b);
260 }
261 assert_eq!(p.finish(), polyval_via_ghash(&h, &blocks));
262 }
263 }
264 }
265
266 #[test]
267 fn padding_matches_explicit_blocks() {
268 let h = [0x42u8; 16];
269 let data = b"a partial final block";
270
271 let mut padded = Polyval::new(h);
272 padded.update_padded(data);
273
274 let mut explicit = Polyval::new(h);
275 for chunk in data.chunks(16) {
276 let mut block = [0u8; 16];
277 block[..chunk.len()].copy_from_slice(chunk);
278 explicit.update_block(&block);
279 }
280 assert_eq!(padded.finish(), explicit.finish());
281 }
282}