Skip to main content

ic_cipher/
gf.rs

1//! Constant-time GF(2^8) arithmetic for AES.
2//!
3//! The AES S-box is computed *algebraically* rather than read from a lookup
4//! table:
5//!
6//! ```text
7//! S(x)    = A · x^-1  ⊕ 0x63       (inversion, then the affine map)
8//! S^-1(y) = (A^-1 · y ⊕ 0x05)^-1
9//! ```
10//!
11//! with `x^-1 = x^254` evaluated by square-and-multiply over the Rijndael
12//! field. Every operation is a branch-free bitwise sequence, so no secret ever
13//! reaches an address bus. That closes the cache-timing channel that table-based
14//! AES (the default in many portable C implementations) leaves open.
15//!
16//! What is here is the byte-at-a-time form. It still runs AES decryption and
17//! the key schedule, but encryption goes through [`crate::aes::bitslice`],
18//! which does the same algebra for sixty-four bytes at once and is some forty
19//! times faster for it.
20
21/// The Rijndael reduction polynomial, x^8 + x^4 + x^3 + x + 1, low byte.
22const MODULUS: u8 = 0x1b;
23
24/// Constant-time multiplication in GF(2^8).
25#[inline(always)]
26pub const fn mul(mut a: u8, mut b: u8) -> u8 {
27    let mut p: u8 = 0;
28    let mut i = 0;
29    while i < 8 {
30        // Add `a` into the accumulator iff the low bit of `b` is set.
31        p ^= a & (b & 1).wrapping_neg();
32        // xtime(a): shift left, reduce iff the high bit was set.
33        let hi = (a >> 7) & 1;
34        a <<= 1;
35        a ^= MODULUS & hi.wrapping_neg();
36        b >>= 1;
37        i += 1;
38    }
39    p
40}
41
42/// `x * x` in GF(2^8), without the bit-serial loop.
43///
44/// Squaring is linear over GF(2) -- `(a + b)^2 = a^2 + b^2`, since the cross
45/// term appears twice and cancels -- so squaring a polynomial doubles every
46/// exponent and nothing else. That is the bits of `x` spread apart with zeros
47/// between them, followed by a reduction, where the general multiply runs its
48/// bit-serial loop eight times round.
49///
50/// It matters because [`inv`] squares seven times and multiplies six: more than
51/// half of the S-box was the general routine doing work that squaring does not
52/// need. Only the even positions above 7 can be set after spreading, so the
53/// reduction folds in four constants rather than seven.
54///
55/// Constant time: each constant is masked by its own bit, no branches.
56///
57/// `squaring_matches_the_general_multiply` checks this against `mul(x, x)` for
58/// every one of the 256 inputs, which is the whole domain.
59#[inline(always)]
60pub const fn square(x: u8) -> u8 {
61    // Spread bit i to position 2i.
62    let t = x as u16;
63    let t = (t | (t << 4)) & 0x0f0f;
64    let t = (t | (t << 2)) & 0x3333;
65    let t = (t | (t << 1)) & 0x5555;
66
67    // x^8, x^10, x^12 and x^14 reduced mod x^8 + x^4 + x^3 + x + 1.
68    let mut r = (t & 0xff) as u8;
69    r ^= 0x1b & (((t >> 8) & 1) as u8).wrapping_neg();
70    r ^= 0x6c & (((t >> 10) & 1) as u8).wrapping_neg();
71    r ^= 0xab & (((t >> 12) & 1) as u8).wrapping_neg();
72    r ^= 0x9a & (((t >> 14) & 1) as u8).wrapping_neg();
73    r
74}
75
76/// `x * 2` in GF(2^8), the AES `xtime` operation.
77#[inline(always)]
78pub const fn xtime(a: u8) -> u8 {
79    let hi = (a >> 7) & 1;
80    (a << 1) ^ (MODULUS & hi.wrapping_neg())
81}
82
83/// Multiplicative inverse in GF(2^8), with `inv(0) == 0`.
84///
85/// Computed as `x^254` via the square-and-multiply chain for `0b1111_1110`,
86/// which is 7 squarings and 6 multiplications, all constant-time. The
87/// squarings go through [`square`] rather than the general multiply.
88#[inline(always)]
89pub const fn inv(x: u8) -> u8 {
90    let mut r = x;
91    let mut bit = 6i32;
92    // Exponent 254 = 0b11111110; the leading 1 is the initial `r = x`.
93    while bit >= 0 {
94        r = square(r);
95        if bit > 0 {
96            r = mul(r, x);
97        }
98        bit -= 1;
99    }
100    r
101}
102
103/// The AES forward S-box, computed in constant time.
104#[inline(always)]
105pub const fn sbox(x: u8) -> u8 {
106    let y = inv(x);
107    y ^ y.rotate_left(1) ^ y.rotate_left(2) ^ y.rotate_left(3) ^ y.rotate_left(4) ^ 0x63
108}
109
110/// The AES inverse S-box, computed in constant time.
111#[inline(always)]
112pub const fn inv_sbox(y: u8) -> u8 {
113    let t = y.rotate_left(1) ^ y.rotate_left(3) ^ y.rotate_left(6) ^ 0x05;
114    inv(t)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    /// The published FIPS 197 S-box, used only to validate the algebraic
122    /// construction. The library itself never indexes this table.
123    const SBOX_REFERENCE: [u8; 256] = [
124        0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab,
125        0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4,
126        0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71,
127        0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2,
128        0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6,
129        0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb,
130        0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45,
131        0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5,
132        0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44,
133        0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a,
134        0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49,
135        0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d,
136        0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25,
137        0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e,
138        0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1,
139        0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
140        0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb,
141        0x16,
142    ];
143
144    #[test]
145    fn algebraic_sbox_matches_fips197() {
146        for x in 0..=255u8 {
147            assert_eq!(sbox(x), SBOX_REFERENCE[x as usize], "S({x:#04x})");
148        }
149    }
150
151    #[test]
152    fn inverse_sbox_undoes_sbox() {
153        for x in 0..=255u8 {
154            assert_eq!(inv_sbox(sbox(x)), x, "S^-1(S({x:#04x}))");
155        }
156    }
157
158    #[test]
159    fn field_inversion_is_correct() {
160        assert_eq!(inv(0), 0);
161        for x in 1..=255u8 {
162            assert_eq!(mul(x, inv(x)), 1, "x * x^-1 for {x:#04x}");
163        }
164    }
165
166    #[test]
167    fn xtime_matches_multiplication_by_two() {
168        for x in 0..=255u8 {
169            assert_eq!(xtime(x), mul(x, 2));
170        }
171    }
172
173    /// Closed-form squaring against the general multiply, over the whole domain.
174    ///
175    /// Not a sample: GF(2^8) has 256 elements, so this is every input there is,
176    /// checked against the routine the S-box vectors already validate. A
177    /// squaring that is wrong for one byte cannot hide from it.
178    #[test]
179    fn squaring_matches_the_general_multiply() {
180        for x in 0..=u8::MAX {
181            assert_eq!(square(x), mul(x, x), "square({x:#04x})");
182        }
183    }
184}