DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ssh_aesgcm.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 ssh_aesgcm.cpp
6 * @brief AES-256-GCM AEAD for SSH (aes256-gcm@openssh.com, RFC 5647) - see ssh_aesgcm.h.
7 *
8 * Arduino: the AES-256 block is mbedtls_aes_crypt_ecb() (ESP32 HW accelerator). Native: a compact
9 * software AES-256 (shared forward S-box + GF(2^8) xtime, no large tables). GHASH and the counter
10 * loop are software on both targets; the GF(2^128) reduction mirrors quic_aead.cpp (independent
11 * module: AES-256 vs AES-128 and a different build gate, so it is kept self-contained here).
12 */
13
15#include <string.h>
16
17// ===========================================================================
18// AES-256 single-block primitive
19// ===========================================================================
20
21#ifdef ARDUINO
22
23static inline void aes256_ecb(SshAesGcmCtx *ctx, const uint8_t in[16], uint8_t out[16])
24{
25 mbedtls_aes_crypt_ecb(&ctx->mbed, MBEDTLS_AES_ENCRYPT, in, out);
26}
27static inline void aes256_load_key(SshAesGcmCtx *ctx, const uint8_t key[32])
28{
29 mbedtls_aes_init(&ctx->mbed);
30 mbedtls_aes_setkey_enc(&ctx->mbed, key, 256);
31}
32static inline void aes256_free_key(SshAesGcmCtx *ctx)
33{
34 mbedtls_aes_free(&ctx->mbed);
35}
36
37#else // Native software AES-256
38
40
41namespace
42{
43// AES round constants (Rcon[1..7] are used by the 256-bit schedule; index 0 unused).
44const uint8_t RCON[8] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40};
45
46inline uint8_t xtime(uint8_t a)
47{
48 return (uint8_t)((a << 1) ^ ((a >> 7) ? 0x1bu : 0x00u));
49}
50
51// AES SubWord (FIPS 197 sec 5.2): apply the S-box to each of the four bytes of a 32-bit word.
52uint32_t aes_sub_word(uint32_t w)
53{
54 return ((uint32_t)DET_AES_SBOX[w >> 24] << 24) | ((uint32_t)DET_AES_SBOX[(w >> 16) & 0xff] << 16) |
55 ((uint32_t)DET_AES_SBOX[(w >> 8) & 0xff] << 8) | (uint32_t)DET_AES_SBOX[w & 0xff];
56}
57// AES RotWord (FIPS 197 sec 5.2): cyclically rotate a 32-bit word one byte left.
58uint32_t aes_rot_word(uint32_t w)
59{
60 return (w << 8) | (w >> 24);
61}
62
63// AES-256 key schedule (FIPS 197 sec 5.2): Nk=8, Nr=14 -> 15 round keys x 4 words = 60 words.
64void aes256_key_expand(const uint8_t key[32], uint32_t rk[60])
65{
66 for (int i = 0; i < 8; i++)
67 rk[i] = ((uint32_t)key[4 * i] << 24) | ((uint32_t)key[4 * i + 1] << 16) | ((uint32_t)key[4 * i + 2] << 8) |
68 (uint32_t)key[4 * i + 3];
69
70 for (int i = 8; i < 60; i++)
71 {
72 uint32_t t = rk[i - 1];
73 if (i % 8 == 0)
74 t = aes_sub_word(aes_rot_word(t)) ^ ((uint32_t)RCON[i / 8] << 24);
75 else if (i % 8 == 4) // AES-256 applies an extra SubWord at the mid-point of each 8-word run.
76 t = aes_sub_word(t);
77 rk[i] = rk[i - 8] ^ t;
78 }
79}
80
81// AES-256 block encrypt (FIPS 197 sec 5.1), 14 rounds. State column-major: s[col*4 + row].
82void aes256_encrypt_block(const uint32_t rk[60], const uint8_t in[16], uint8_t out[16])
83{
84 uint8_t s[16];
85 for (int i = 0; i < 16; i++)
86 s[i] = in[i] ^ (uint8_t)(rk[i / 4] >> (24 - (i % 4) * 8));
87
88 for (int r = 1; r <= 13; r++)
89 {
90 for (int i = 0; i < 16; i++)
91 s[i] = DET_AES_SBOX[s[i]];
92
93 uint8_t t;
94 t = s[1]; // row 1 <<< 1
95 s[1] = s[5];
96 s[5] = s[9];
97 s[9] = s[13];
98 s[13] = t;
99 t = s[2]; // row 2 <<< 2
100 s[2] = s[10];
101 s[10] = t;
102 t = s[6];
103 s[6] = s[14];
104 s[14] = t;
105 t = s[15]; // row 3 <<< 3
106 s[15] = s[11];
107 s[11] = s[7];
108 s[7] = s[3];
109 s[3] = t;
110
111 for (int c = 0; c < 4; c++)
112 {
113 uint8_t a = s[c * 4];
114 uint8_t b = s[c * 4 + 1];
115 uint8_t cc = s[c * 4 + 2];
116 uint8_t d = s[c * 4 + 3];
117 uint8_t e = a ^ b ^ cc ^ d;
118 s[c * 4] = a ^ e ^ xtime(a ^ b);
119 s[c * 4 + 1] = b ^ e ^ xtime(b ^ cc);
120 s[c * 4 + 2] = cc ^ e ^ xtime(cc ^ d);
121 s[c * 4 + 3] = d ^ e ^ xtime(d ^ a);
122 }
123
124 for (int i = 0; i < 16; i++)
125 s[i] ^= (uint8_t)(rk[r * 4 + i / 4] >> (24 - (i % 4) * 8));
126 }
127
128 for (int i = 0; i < 16; i++)
129 s[i] = DET_AES_SBOX[s[i]];
130
131 uint8_t t;
132 t = s[1];
133 s[1] = s[5];
134 s[5] = s[9];
135 s[9] = s[13];
136 s[13] = t;
137 t = s[2];
138 s[2] = s[10];
139 s[10] = t;
140 t = s[6];
141 s[6] = s[14];
142 s[14] = t;
143 t = s[15];
144 s[15] = s[11];
145 s[11] = s[7];
146 s[7] = s[3];
147 s[3] = t;
148
149 for (int i = 0; i < 16; i++)
150 s[i] ^= (uint8_t)(rk[56 + i / 4] >> (24 - (i % 4) * 8));
151
152 memcpy(out, s, 16);
153}
154} // namespace
155
156// Non-const ctx for signature parity with the ARDUINO path (below), where aes256_ecb wraps
157// mbedtls_aes_crypt_ecb() on a non-const mbedtls_aes_context. (S995 does not apply portably.)
158static inline void aes256_ecb(SshAesGcmCtx *ctx, const uint8_t in[16], uint8_t out[16])
159{
160 aes256_encrypt_block(ctx->rk, in, out);
161}
162static inline void aes256_load_key(SshAesGcmCtx *ctx, const uint8_t key[32])
163{
164 aes256_key_expand(key, ctx->rk);
165}
166static inline void aes256_free_key(SshAesGcmCtx *)
167{
168 // no-op: the key schedule lives in-place in SshAesGcmCtx (a plain array), so there is
169 // no external key handle to release. Kept for signature parity with the hardware path.
170}
171
172#endif // ARDUINO
173
174// ===========================================================================
175// AEAD_AES_256_GCM (NIST SP 800-38D) - software GHASH/GCTR on all targets
176// ===========================================================================
177
178namespace
179{
180inline void xor16(uint8_t *dst, const uint8_t *src)
181{
182 for (int i = 0; i < 16; i++)
183 dst[i] ^= src[i];
184}
185
186// GHASH (acc *= H, and fold buffers into acc) is the shared 4-bit-table primitive in
187// shared_primitives/ghash.h - ghash_key_init(&ctx->ghk, ctx->h) once at init, then ghash_mul /
188// ghash_update on ctx->ghk. Replaced the old 128-iteration bitwise multiply (~37x faster on-device).
189
190inline void put_be64(uint8_t *p, uint64_t v)
191{
192 for (int i = 7; i >= 0; i--)
193 {
194 p[i] = (uint8_t)(v & 0xff);
195 v >>= 8;
196 }
197}
198
199// Increment the low 32 bits of a 16-byte counter block, big-endian, mod 2^32 (GCM inc32).
200inline void inc32(uint8_t ctr[16])
201{
202 for (int i = 15; i >= 12; i--)
203 if (++ctr[i])
204 break;
205}
206
207// GCTR (NIST SP 800-38D sec 6.5): out = in XOR AES-CTR keystream starting from counter @p ctr,
208// advanced in place. @p in / @p out may alias.
209void gctr(SshAesGcmCtx *ctx, uint8_t ctr[16], const uint8_t *in, size_t len, uint8_t *out)
210{
211 uint8_t ks[16];
212 size_t off = 0;
213 while (off < len)
214 {
215 aes256_ecb(ctx, ctr, ks);
216 inc32(ctr);
217 size_t take = len - off;
218 if (take > 16)
219 take = 16;
220 for (size_t i = 0; i < take; i++)
221 out[off + i] = in[off + i] ^ ks[i];
222 off += take;
223 }
224}
225
226// Compute J0 (96-bit nonce), the GHASH tag over aad || cipher, and the tag for one GCM operation.
227// @p cipher is the ciphertext to authenticate (== output for seal, == input for open). Writes j0[16]
228// (for the caller to derive the CTR start) and the 16-byte tag.
229void gcm_core(SshAesGcmCtx *ctx, const uint8_t nonce[12], const uint8_t *aad, size_t aad_len, const uint8_t *cipher,
230 size_t cipher_len, uint8_t j0[16], uint8_t tag[16])
231{
232 // 96-bit nonce: J0 = nonce || 0^31 || 1.
233 memcpy(j0, nonce, 12);
234 j0[12] = 0;
235 j0[13] = 0;
236 j0[14] = 0;
237 j0[15] = 1;
238
239 uint8_t acc[16] = {0};
240 ghash_update(&ctx->ghk, acc, aad, aad_len);
241 ghash_update(&ctx->ghk, acc, cipher, cipher_len);
242 uint8_t lb[16];
243 put_be64(lb, (uint64_t)aad_len * 8);
244 put_be64(lb + 8, (uint64_t)cipher_len * 8);
245 xor16(acc, lb);
246 ghash_mul(&ctx->ghk, acc);
247
248 uint8_t ej0[16];
249 aes256_ecb(ctx, j0, ej0);
250 for (int i = 0; i < 16; i++)
251 tag[i] = acc[i] ^ ej0[i];
252}
253
254// Advance the RFC 5647 invocation counter: the low 8 bytes of the 12-byte nonce, big-endian; the
255// 4-byte fixed field never changes.
256inline void iv_increment(uint8_t iv[SSH_AESGCM_IV_LEN])
257{
258 for (int j = SSH_AESGCM_IV_LEN - 1; j >= 4; j--)
259 if (++iv[j])
260 break;
261}
262} // namespace
263
264// ===========================================================================
265// Public API
266// ===========================================================================
267
268void ssh_aesgcm_init(SshAesGcmCtx *ctx, const uint8_t key[SSH_AESGCM_KEY_LEN], const uint8_t iv[SSH_AESGCM_IV_LEN])
269{
270 aes256_load_key(ctx, key);
271 uint8_t zero[16] = {0};
272 aes256_ecb(ctx, zero, ctx->h); // H = E(K, 0^128)
273 ghash_key_init(&ctx->ghk, ctx->h);
274 memcpy(ctx->iv, iv, SSH_AESGCM_IV_LEN);
275 ctx->ready = true;
276}
277
278void ssh_aesgcm_seal(SshAesGcmCtx *ctx, const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len,
279 uint8_t *out)
280{
281 // Encrypt with the CTR starting at inc32(J0), then GHASH the resulting ciphertext.
282 uint8_t j0[16];
283 memcpy(j0, ctx->iv, 12);
284 j0[12] = 0;
285 j0[13] = 0;
286 j0[14] = 0;
287 j0[15] = 1;
288 uint8_t ctr[16];
289 memcpy(ctr, j0, 16);
290 inc32(ctr);
291 gctr(ctx, ctr, pt, pt_len, out);
292
293 uint8_t j0b[16];
294 uint8_t tag[16];
295 gcm_core(ctx, ctx->iv, aad, aad_len, out, pt_len, j0b, tag);
296 memcpy(out + pt_len, tag, SSH_AESGCM_TAG_LEN);
297
298 iv_increment(ctx->iv);
299}
300
301bool ssh_aesgcm_open(SshAesGcmCtx *ctx, const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len,
302 const uint8_t tag[SSH_AESGCM_TAG_LEN], uint8_t *out)
303{
304 // Authenticate over the received ciphertext BEFORE producing any plaintext.
305 uint8_t j0[16];
306 uint8_t exp_tag[16];
307 gcm_core(ctx, ctx->iv, aad, aad_len, ct, ct_len, j0, exp_tag);
308
309 uint8_t diff = 0;
310 for (int i = 0; i < SSH_AESGCM_TAG_LEN; i++)
311 diff |= (uint8_t)(exp_tag[i] ^ tag[i]);
312 if (diff != 0)
313 return false; // tag mismatch: nothing written, counter NOT advanced
314
315 uint8_t ctr[16];
316 memcpy(ctr, j0, 16);
317 inc32(ctr);
318 gctr(ctx, ctr, ct, ct_len, out);
319
320 iv_increment(ctx->iv);
321 return true;
322}
323
325{
326 aes256_free_key(ctx);
327 volatile uint8_t *p = (volatile uint8_t *)ctx;
328 for (size_t i = 0; i < sizeof(SshAesGcmCtx); i++)
329 p[i] = 0;
330}
The AES forward S-box (FIPS 197 Figure 7) - one shared copy.
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
void ssh_aesgcm_seal(SshAesGcmCtx *ctx, const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *out)
Seal one packet: AES-256-GCM encrypt pt (pt_len bytes) and authenticate it together with aad....
void ssh_aesgcm_init(SshAesGcmCtx *ctx, const uint8_t key[SSH_AESGCM_KEY_LEN], const uint8_t iv[SSH_AESGCM_IV_LEN])
Initialize an AES-256-GCM context: expand the key, precompute H, latch the initial nonce.
void ssh_aesgcm_wipe(SshAesGcmCtx *ctx)
Zero the key schedule, H, and nonce (volatile wipe). Call on disconnect.
bool ssh_aesgcm_open(SshAesGcmCtx *ctx, const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[SSH_AESGCM_TAG_LEN], uint8_t *out)
Open one packet: verify the 16-byte tag over aad || ct in constant time, and only on success decrypt ...
AES-256-GCM AEAD for SSH (aes256-gcm@openssh.com, RFC 5647).
AES-256-GCM context for one SSH direction (HW AES on ESP32).
Definition ssh_aesgcm.h:50
uint8_t h[16]
GHASH subkey H = E(K, 0^128).
Definition ssh_aesgcm.h:52
GhashKey ghk
4-bit GHASH table built from H (once at init).
Definition ssh_aesgcm.h:53
uint8_t iv[SSH_AESGCM_IV_LEN]
current nonce; low 8 bytes (invocation counter) ++ per packet.
Definition ssh_aesgcm.h:54
bool ready
true once a key/IV is installed.
Definition ssh_aesgcm.h:55
mbedtls_aes_context mbed
mbedtls context (HW-accelerated on ESP32), encrypt key schedule.
Definition ssh_aesgcm.h:51