DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
quic_aead.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file quic_aead.cpp
6 * @brief AES-128 block cipher and AEAD_AES_128_GCM (see quic_aead.h).
7 *
8 * Arduino: the AES block is mbedtls_aes_crypt_ecb() (ESP32 HW accelerator). Native: a compact
9 * software AES-128 (forward S-box + GF(2^8) xtime, no large tables). GHASH and the counter loop
10 * are software on both targets.
11 */
12
14
15#if (DETWS_ENABLE_HTTP3 || DETWS_ENABLE_DTLS)
16
18#include <string.h>
19
20// ===========================================================================
21// AES-128 single-block primitive
22// ===========================================================================
23
24#ifdef ARDUINO
25
26void quic_aes128_init(QuicAes128 *ctx, const uint8_t key[16])
27{
28 mbedtls_aes_init(&ctx->mbed);
29 mbedtls_aes_setkey_enc(&ctx->mbed, key, 128);
30}
31
32void quic_aes128_encrypt_block(QuicAes128 *ctx, const uint8_t in[16], uint8_t out[16])
33{
34 mbedtls_aes_crypt_ecb(&ctx->mbed, MBEDTLS_AES_ENCRYPT, in, out);
35}
36
37void quic_aes128_wipe(QuicAes128 *ctx)
38{
39 mbedtls_aes_free(&ctx->mbed);
40}
41
42#else // Native software AES-128
43
45
46namespace
47{
48
49// AES round constants (Rcon[1..10]; index 0 unused).
50const uint8_t RCON[11] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36};
51
52inline uint8_t xtime(uint8_t a)
53{
54 return (uint8_t)((a << 1) ^ ((a >> 7) ? 0x1bu : 0x00u));
55}
56
57// AES SubWord (FIPS 197 sec 5.2): apply the S-box to each of the four bytes of a 32-bit word.
58uint32_t aes_sub_word(uint32_t w)
59{
60 return ((uint32_t)DET_AES_SBOX[w >> 24] << 24) | ((uint32_t)DET_AES_SBOX[(w >> 16) & 0xff] << 16) |
61 ((uint32_t)DET_AES_SBOX[(w >> 8) & 0xff] << 8) | (uint32_t)DET_AES_SBOX[w & 0xff];
62}
63// AES RotWord (FIPS 197 sec 5.2): cyclically rotate a 32-bit word one byte left.
64uint32_t aes_rot_word(uint32_t w)
65{
66 return (w << 8) | (w >> 24);
67}
68
69// AES-128 key schedule (FIPS 197 sec 5.2): Nk=4, Nr=10 -> 11 round keys x 4 words = 44 words.
70void aes128_key_expand(const uint8_t key[16], uint32_t rk[44])
71{
72 for (int i = 0; i < 4; i++)
73 rk[i] = ((uint32_t)key[4 * i] << 24) | ((uint32_t)key[4 * i + 1] << 16) | ((uint32_t)key[4 * i + 2] << 8) |
74 (uint32_t)key[4 * i + 3];
75
76 for (int i = 4; i < 44; i++)
77 {
78 uint32_t t = rk[i - 1];
79 if (i % 4 == 0)
80 t = aes_sub_word(aes_rot_word(t)) ^ ((uint32_t)RCON[i / 4] << 24);
81 rk[i] = rk[i - 4] ^ t;
82 }
83}
84
85// AES-128 block encrypt (FIPS 197 sec 5.1). State column-major: s[col*4 + row].
86void aes128_encrypt_block(const uint32_t rk[44], const uint8_t in[16], uint8_t out[16])
87{
88 uint8_t s[16];
89 for (int i = 0; i < 16; i++)
90 s[i] = in[i] ^ (uint8_t)(rk[i / 4] >> (24 - (i % 4) * 8));
91
92 for (int r = 1; r <= 9; r++)
93 {
94 for (int i = 0; i < 16; i++)
95 s[i] = DET_AES_SBOX[s[i]];
96
97 uint8_t t;
98 t = s[1]; // row 1 <<< 1
99 s[1] = s[5];
100 s[5] = s[9];
101 s[9] = s[13];
102 s[13] = t;
103 t = s[2]; // row 2 <<< 2
104 s[2] = s[10];
105 s[10] = t;
106 t = s[6];
107 s[6] = s[14];
108 s[14] = t;
109 t = s[15]; // row 3 <<< 3
110 s[15] = s[11];
111 s[11] = s[7];
112 s[7] = s[3];
113 s[3] = t;
114
115 for (int c = 0; c < 4; c++)
116 {
117 uint8_t a = s[c * 4];
118 uint8_t b = s[c * 4 + 1];
119 uint8_t cc = s[c * 4 + 2];
120 uint8_t d = s[c * 4 + 3];
121 uint8_t e = a ^ b ^ cc ^ d;
122 s[c * 4] = a ^ e ^ xtime(a ^ b);
123 s[c * 4 + 1] = b ^ e ^ xtime(b ^ cc);
124 s[c * 4 + 2] = cc ^ e ^ xtime(cc ^ d);
125 s[c * 4 + 3] = d ^ e ^ xtime(d ^ a);
126 }
127
128 for (int i = 0; i < 16; i++)
129 s[i] ^= (uint8_t)(rk[r * 4 + i / 4] >> (24 - (i % 4) * 8));
130 }
131
132 for (int i = 0; i < 16; i++)
133 s[i] = DET_AES_SBOX[s[i]];
134
135 uint8_t t;
136 t = s[1];
137 s[1] = s[5];
138 s[5] = s[9];
139 s[9] = s[13];
140 s[13] = t;
141 t = s[2];
142 s[2] = s[10];
143 s[10] = t;
144 t = s[6];
145 s[6] = s[14];
146 s[14] = t;
147 t = s[15];
148 s[15] = s[11];
149 s[11] = s[7];
150 s[7] = s[3];
151 s[3] = t;
152
153 for (int i = 0; i < 16; i++)
154 s[i] ^= (uint8_t)(rk[40 + i / 4] >> (24 - (i % 4) * 8));
155
156 memcpy(out, s, 16);
157}
158} // namespace
159
160void quic_aes128_init(QuicAes128 *ctx, const uint8_t key[16])
161{
162 aes128_key_expand(key, ctx->rk);
163}
164
165void quic_aes128_encrypt_block(QuicAes128 *ctx, const uint8_t in[16], uint8_t out[16])
166{
167 aes128_encrypt_block(ctx->rk, in, out);
168}
169
170void quic_aes128_wipe(QuicAes128 *ctx)
171{
172 volatile uint8_t *p = (volatile uint8_t *)ctx;
173 for (size_t i = 0; i < sizeof(QuicAes128); i++)
174 p[i] = 0;
175}
176
177#endif // ARDUINO
178
179// ===========================================================================
180// AEAD_AES_128_GCM (NIST SP 800-38D) - software on all targets
181// ===========================================================================
182
183namespace
184{
185inline void xor16(uint8_t *dst, const uint8_t *src)
186{
187 for (int i = 0; i < 16; i++)
188 dst[i] ^= src[i];
189}
190
191// GHASH (acc *= H, and fold buffers into acc) is the shared 4-bit-table primitive in
192// shared_primitives/ghash.h: build a GhashKey from H per packet (ghash_key_init), then ghash_update /
193// ghash_mul. Replaced the old 128-iteration bitwise multiply (~37x faster on-device).
194
195inline void put_be64(uint8_t *p, uint64_t v)
196{
197 for (int i = 7; i >= 0; i--)
198 {
199 p[i] = (uint8_t)(v & 0xff);
200 v >>= 8;
201 }
202}
203
204// Increment the low 32 bits of a 16-byte counter block, big-endian, mod 2^32 (GCM inc32).
205inline void inc32(uint8_t ctr[16])
206{
207 for (int i = 15; i >= 12; i--)
208 if (++ctr[i])
209 break;
210}
211
212// GCTR (NIST SP 800-38D sec 6.5): out = in XOR AES-CTR keystream starting from counter @p ctr,
213// which is advanced in place. @p in / @p out may alias.
214void gctr(QuicAes128 *aes, uint8_t ctr[16], const uint8_t *in, size_t len, uint8_t *out)
215{
216 uint8_t ks[16];
217 size_t off = 0;
218 while (off < len)
219 {
220 quic_aes128_encrypt_block(aes, ctr, ks);
221 inc32(ctr);
222 size_t take = len - off;
223 if (take > 16)
224 take = 16;
225 for (size_t i = 0; i < take; i++)
226 out[off + i] = in[off + i] ^ ks[i];
227 off += take;
228 }
229}
230
231// Compute H, J0, the GHASH tag and the ciphertext for a 96-bit-nonce GCM operation. @p cipher is
232// the ciphertext to authenticate (== output for seal, == input for open). Writes the 16-byte tag.
233void gcm_core(QuicAes128 *aes, const uint8_t nonce[12], const uint8_t *aad, size_t aad_len, const uint8_t *cipher,
234 size_t cipher_len, uint8_t j0[16], uint8_t tag[16])
235{
236 uint8_t h[16] = {0};
237 quic_aes128_encrypt_block(aes, h, h); // H = E(K, 0^128)
238 GhashKey ghk;
239 ghash_key_init(&ghk, h);
240
241 // 96-bit nonce: J0 = nonce || 0^31 || 1.
242 memcpy(j0, nonce, 12);
243 j0[12] = 0;
244 j0[13] = 0;
245 j0[14] = 0;
246 j0[15] = 1;
247
248 uint8_t s[16] = {0};
249 ghash_update(&ghk, s, aad, aad_len);
250 ghash_update(&ghk, s, cipher, cipher_len);
251 uint8_t lb[16];
252 put_be64(lb, (uint64_t)aad_len * 8);
253 put_be64(lb + 8, (uint64_t)cipher_len * 8);
254 xor16(s, lb);
255 ghash_mul(&ghk, s);
256
257 uint8_t ej0[16];
258 quic_aes128_encrypt_block(aes, j0, ej0);
259 for (int i = 0; i < 16; i++)
260 tag[i] = s[i] ^ ej0[i];
261}
262} // namespace
263
264void quic_aes128_gcm_seal(const uint8_t key[16], const uint8_t nonce[12], const uint8_t *aad, size_t aad_len,
265 const uint8_t *pt, size_t pt_len, uint8_t *out)
266{
267 QuicAes128 aes;
268 quic_aes128_init(&aes, key);
269
270 // Encrypt first (counter starts at inc32(J0)), then GHASH the resulting ciphertext.
271 uint8_t j0[16];
272 memcpy(j0, nonce, 12);
273 j0[12] = 0;
274 j0[13] = 0;
275 j0[14] = 0;
276 j0[15] = 1;
277 uint8_t ctr[16];
278 memcpy(ctr, j0, 16);
279 inc32(ctr);
280 gctr(&aes, ctr, pt, pt_len, out);
281
282 uint8_t j0b[16];
283 uint8_t tag[16];
284 gcm_core(&aes, nonce, aad, aad_len, out, pt_len, j0b, tag);
285 memcpy(out + pt_len, tag, QUIC_AEAD_TAG_LEN);
286
287 quic_aes128_wipe(&aes);
288}
289
290bool quic_aes128_gcm_open(const uint8_t key[16], const uint8_t nonce[12], const uint8_t *aad, size_t aad_len,
291 const uint8_t *ct, size_t ct_len, uint8_t *out)
292{
293 if (ct_len < QUIC_AEAD_TAG_LEN)
294 return false;
295 size_t pt_len = ct_len - QUIC_AEAD_TAG_LEN;
296
297 QuicAes128 aes;
298 quic_aes128_init(&aes, key);
299
300 // Authenticate over the received ciphertext before producing any plaintext.
301 uint8_t j0[16];
302 uint8_t tag[16];
303 gcm_core(&aes, nonce, aad, aad_len, ct, pt_len, j0, tag);
304
305 uint8_t diff = 0;
306 for (int i = 0; i < QUIC_AEAD_TAG_LEN; i++)
307 diff |= (uint8_t)(tag[i] ^ ct[pt_len + i]);
308 if (diff != 0)
309 {
310 quic_aes128_wipe(&aes);
311 return false;
312 }
313
314 uint8_t ctr[16];
315 memcpy(ctr, j0, 16);
316 inc32(ctr);
317 gctr(&aes, ctr, ct, pt_len, out);
318
319 quic_aes128_wipe(&aes);
320 return true;
321}
322
323#endif // DETWS_ENABLE_HTTP3 || DETWS_ENABLE_DTLS
The AES forward S-box (FIPS 197 Figure 7) - one shared copy.
GHASH (the GF(2^128) universal hash under AES-GCM, NIST SP 800-38D sec 6.3), 4-bit table.
void ghash_key_init(GhashKey *t, const uint8_t h[16])
Build the 4-bit multiplication table from the 16-byte subkey h. Call once per key.
Definition ghash.h:53
void ghash_update(const GhashKey *t, uint8_t acc[16], const uint8_t *data, size_t len)
Fold len bytes of data into acc: acc = (acc XOR block) * H per 16 bytes, a final short block MSB-zero...
Definition ghash.h:128
void ghash_mul(const GhashKey *t, uint8_t acc[16])
acc = acc * H in GF(2^128) with the GCM reduction, using the precomputed table t.
Definition ghash.h:87
AES-128 block cipher and AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
4-bit GHASH table for a fixed subkey H = E(K, 0^128): M[i] = i*H as four big-endian uint32 words (M[i...
Definition ghash.h:33