ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
portable_aes128gcm.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 portable_aes128gcm.cpp
6 * @brief AES-128-GCM and the AES-128 block in software - the backend for a target with no accelerated AEAD.
7 *
8 * Software AES-128 plus a 4-bit-table GHASH. Selected explicitly by a vendor profile that sets
9 * PC_HAS_HW_AESGCM 0; never a fallback. There is no weak symbol anywhere in the chain, so linking no
10 * backend is an undefined reference and linking two is a duplicate definition.
11 *
12 * The context is keyed: the AES schedule, H and the GHASH table are built once per key. Only J0 and the
13 * counter are per record.
14 */
15
19#include "crypto/crypto_opt.h"
20#include "crypto/crypto_scratch.h" // pc_ct_eq
21#include "crypto/mac/ghash.h"
22#include "protocore_config.h" // PC_ENABLE_* gate the whole file; pc_platform.h does not pull this in
23#include "server/mmgr/secure.h"
24#include <string.h>
25
26#if (PC_ENABLE_HTTP3 || PC_ENABLE_DTLS || PC_ENABLE_SMB)
27#if !PC_HAS_HW_AESGCM
28
30
31// pc_aes128 - definition private to this backend; aes128gcm.h forward-declares the symbol only.
32struct pc_aes128
33{
34 uint32_t rk[44]; ///< AES-128 expanded round-key schedule (11 round keys x 4 words).
35};
36
37static_assert(sizeof(pc_aes128) <= PC_WORK_AES128, "pc_aes128 outgrew PC_WORK_AES128 - raise it in protocore_config.h");
38
39pc_aes128 *pc_aes128_wants(void)
40{
41 pc_span ws = pc_secure_span(sizeof(pc_aes128), alignof(pc_aes128));
42 return pc_span_ok(ws) ? reinterpret_cast<pc_aes128 *>(ws.buf) : nullptr;
43}
44
45void pc_aes128_init(pc_aes128 *ctx, const uint8_t key[16])
46{
47 pc_aes_key_expand(key, 4, ctx->rk);
48}
49
50void pc_aes128_encrypt_block(pc_aes128 *ctx, const uint8_t in[16], uint8_t out[16])
51{
52 pc_aes_encrypt_block(ctx->rk, 10, in, out);
53}
54
55void pc_aes128_wipe(pc_aes128 *ctx)
56{
57 pc_secure_wipe(ctx, sizeof(pc_aes128));
58}
59
60// ===========================================================================
61// AEAD_AES_128_GCM (NIST SP 800-38D). The struct IS the keyed context: aes/h/ghk are built once per key
62// by pc_aes128gcm_key_init, the rest is per-record scratch.
63// ===========================================================================
64
65struct Aes128GcmWork
66{
67 pc_aes128 aes; ///< AES-128 key schedule.
68 uint8_t h[16]; ///< GHASH subkey H = E(K, 0^128).
69 GhashKey ghk; ///< 4-bit GHASH table built from H.
70 uint8_t ks[16]; ///< GCTR keystream block.
71 uint8_t acc[16]; ///< GHASH accumulator.
72 uint8_t lb[16]; ///< length block (aad_len || cipher_len, in bits).
73 uint8_t ej0[16]; ///< E(K, J0), the tag mask.
74 uint8_t j0[16]; ///< pre-counter block J0 = nonce || 0^31 || 1.
75 uint8_t ctr[16]; ///< running GCTR counter.
76 uint8_t tag[16]; ///< computed tag (open: compared; seal writes the caller's buffer directly).
77};
78static_assert(sizeof(Aes128GcmWork) <= PC_WORK_AES128GCM,
79 "Aes128GcmWork outgrew PC_WORK_AES128GCM - raise it in protocore_config.h, which derives "
80 "PC_SECURE_ARENA_SIZE from it");
81
82namespace
83{
84inline void xor16(uint8_t *dst, const uint8_t *src)
85{
86 // One block = four words. Both pointers must be 4-aligned: Xtensa has no native unaligned 32-bit
87 // load and the trap handler costs more than the byte loop, so test them together (OR then mask)
88 // and fall back rather than gamble.
89 if (((((uintptr_t)dst) | ((uintptr_t)src)) & 3u) == 0u)
90 {
91 uint32_t *d = (uint32_t *)dst;
92 const uint32_t *s = (const uint32_t *)src;
93 d[0] ^= s[0];
94 d[1] ^= s[1];
95 d[2] ^= s[2];
96 d[3] ^= s[3];
97 return;
98 }
99 for (int i = 0; i < 16; i++)
100 {
101 dst[i] ^= src[i];
102 }
103}
104
105// GHASH (acc *= H, and fold buffers into acc) is the shared 4-bit-table primitive in crypto/ghash.h:
106// ghash_key_init(&w->ghk, w->h) once, then ghash_update / ghash_mul on w->ghk.
107
108inline void put_be64(uint8_t *p, uint64_t v)
109{
110 for (int i = 7; i >= 0; i--)
111 {
112 p[i] = (uint8_t)(v & 0xff);
113 v >>= 8;
114 }
115}
116
117// Increment the low 32 bits of a 16-byte counter block, big-endian, mod 2^32 (GCM inc32).
118inline void inc32(uint8_t ctr[16])
119{
120 // A single-byte carry (i=15 wrapping into i=14, etc.) only needs ctr[15] to roll 0xff -> 0x00,
121 // i.e. ~256 GCTR blocks (~4 KiB of plaintext) through the public seal()/open() API - well within
122 // reach of a host test, so that carry-continue arm is exercised below and is NOT excluded.
123 // What genuinely cannot be reached is the loop running all 4 iterations to exhaustion with no
124 // break at all, i.e. every one of the 4 bytes carrying in the SAME inc32() call, which only
125 // happens when the full 32-bit counter was 0xffffffff before this call. ctr always starts at
126 // inc32(J0) with J0[12..15] fixed to 0,0,0,1 (96-bit-nonce J0, NIST SP 800-38D) - not
127 // caller-controlled - so reaching that requires one seal()/open() call over ~2^32 contiguous
128 // GCTR blocks (~64 GiB of plaintext in one call): infeasible in a host test.
129 for (int i = 15; i >= 12; // GCOVR_EXCL_BR_LINE loop-exhausted-with-no-break needs the ~64GiB full wrap; see above
130 i--)
131 {
132 if (++ctr[i])
133 {
134 break;
135 }
136 }
137}
138
139// GCTR (NIST SP 800-38D sec 6.5): out = in XOR AES-CTR keystream from w->ctr, advanced in place. Uses
140// w->ks. @p in / @p out may alias.
141void gctr(Aes128GcmWork *w, const uint8_t *in, size_t len, uint8_t *out)
142{
143 size_t off = 0;
144 while (off < len)
145 {
146 pc_aes128_encrypt_block(&w->aes, w->ctr, w->ks);
147 inc32(w->ctr);
148 size_t take = len - off;
149 if (take > 16)
150 {
151 take = 16;
152 }
153 for (size_t i = 0; i < take; i++)
154 {
155 out[off + i] = in[off + i] ^ w->ks[i];
156 }
157 off += take;
158 }
159}
160
161// Compute H, the GHASH table, J0, GHASH(aad || cipher) and the 16-byte tag for a 96-bit-nonce GCM
162// operation. @p cipher is the ciphertext to authenticate (== output for seal, == input for open). Uses
163// w->h/ghk/j0/acc/lb/ej0; writes @p tag_out.
164void gcm_core(Aes128GcmWork *w, const uint8_t nonce[12], const uint8_t *aad, size_t aad_len, const uint8_t *cipher,
165 size_t cipher_len, uint8_t tag_out[16])
166{
167 memset(w->h, 0, 16);
168 pc_aes128_encrypt_block(&w->aes, w->h, w->h); // H = E(K, 0^128)
169 ghash_key_init(&w->ghk, w->h);
170
171 // 96-bit nonce: J0 = nonce || 0^31 || 1.
172 memcpy(w->j0, nonce, 12);
173 w->j0[12] = 0;
174 w->j0[13] = 0;
175 w->j0[14] = 0;
176 w->j0[15] = 1;
177
178 memset(w->acc, 0, 16);
179 ghash_update(&w->ghk, w->acc, aad, aad_len);
180 ghash_update(&w->ghk, w->acc, cipher, cipher_len);
181 put_be64(w->lb, (uint64_t)aad_len * 8);
182 put_be64(w->lb + 8, (uint64_t)cipher_len * 8);
183 xor16(w->acc, w->lb);
184 ghash_mul(&w->ghk, w->acc);
185
186 pc_aes128_encrypt_block(&w->aes, w->j0, w->ej0);
187 for (int i = 0; i < 16; i++)
188 {
189 tag_out[i] = w->acc[i] ^ w->ej0[i];
190 }
191}
192
193// 96-bit nonce: J0 = nonce || 0^31 || 1. Per record.
194inline void set_j0(Aes128GcmWork *w, const uint8_t nonce[12])
195{
196 memcpy(w->j0, nonce, 12);
197 w->j0[12] = 0;
198 w->j0[13] = 0;
199 w->j0[14] = 0;
200 w->j0[15] = 1;
201}
202
203// GHASH(aad || cipher) and the 16-byte tag, against the table already built for this key. Requires
204// set_j0() first; leaves w->ctr alone so the caller controls the GCTR pass order.
205void gcm_tag(Aes128GcmWork *w, const uint8_t *aad, size_t aad_len, const uint8_t *cipher, size_t cipher_len,
206 uint8_t tag_out[16])
207{
208 memset(w->acc, 0, 16);
209 ghash_update(&w->ghk, w->acc, aad, aad_len);
210 ghash_update(&w->ghk, w->acc, cipher, cipher_len);
211 put_be64(w->lb, (uint64_t)aad_len * 8);
212 put_be64(w->lb + 8, (uint64_t)cipher_len * 8);
213 xor16(w->acc, w->lb);
214 ghash_mul(&w->ghk, w->acc);
215
216 pc_aes128_encrypt_block(&w->aes, w->j0, w->ej0);
217 for (int i = 0; i < 16; i++)
218 {
219 tag_out[i] = w->acc[i] ^ w->ej0[i];
220 }
221}
222} // namespace
223
224pc_aes128gcm_key *pc_aes128gcm_key_init(void *storage, const uint8_t key[PC_AES128GCM_KEY_LEN])
225{
226 Aes128GcmWork *w = reinterpret_cast<Aes128GcmWork *>(storage);
227 pc_aes128_init(&w->aes, key);
228 memset(w->h, 0, 16);
229 pc_aes128_encrypt_block(&w->aes, w->h, w->h); // H = E(K, 0^128)
230 ghash_key_init(&w->ghk, w->h);
231 return reinterpret_cast<pc_aes128gcm_key *>(w);
232}
233
234void pc_aes128gcm_key_wipe(pc_aes128gcm_key *k)
235{
236 Aes128GcmWork *w = reinterpret_cast<Aes128GcmWork *>(k);
237 pc_aes128_wipe(&w->aes);
238 pc_secure_wipe(reinterpret_cast<uint8_t *>(w), sizeof(Aes128GcmWork));
239}
240
241pc_cspan pc_aes128gcm_seal(pc_aes128gcm_key *k, const uint8_t nonce[PC_AES128GCM_IV_LEN], const uint8_t *aad,
242 size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out,
243 uint8_t tag_out[PC_AES128GCM_TAG_LEN])
244{
245 Aes128GcmWork *w = reinterpret_cast<Aes128GcmWork *>(k);
246 set_j0(w, nonce);
247 // Encrypt first (counter starts at inc32(J0)), then GHASH the resulting ciphertext.
248 memcpy(w->ctr, w->j0, 16);
249 inc32(w->ctr);
250 gctr(w, pt, pt_len, ct_out);
251 gcm_tag(w, aad, aad_len, ct_out, pt_len, tag_out);
252 return pc_cspan_from(ct_out, pt_len); // the tag rides in tag_out, not in this span
253}
254
255bool pc_aes128gcm_open(pc_aes128gcm_key *k, const uint8_t nonce[PC_AES128GCM_IV_LEN], const uint8_t *aad,
256 size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AES128GCM_TAG_LEN],
257 uint8_t *out)
258{
259 Aes128GcmWork *w = reinterpret_cast<Aes128GcmWork *>(k);
260 set_j0(w, nonce);
261 // Authenticate over the received ciphertext BEFORE producing any plaintext.
262 gcm_tag(w, aad, aad_len, ct, ct_len, w->tag);
263 if (!pc_ct_eq(w->tag, tag, PC_AES128GCM_TAG_LEN))
264 {
265 return false; // tag mismatch: nothing written
266 }
267 memcpy(w->ctr, w->j0, 16);
268 inc32(w->ctr);
269 gctr(w, ct, ct_len, out);
270 return true;
271}
272
273#endif // !PC_HAS_HW_AESGCM
274#endif // PC_ENABLE_HTTP3 || PC_ENABLE_DTLS || PC_ENABLE_SMB
AES-128 block cipher + AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
Compact table-free software AES key schedule + single-block encrypt (FIPS 197) - one source of truth.
void pc_aes_key_expand(const uint8_t *key, int nk, uint32_t *rk)
AES key expansion (FIPS 197 sec 5.2). nk key words (4=AES-128, 8=AES-256); rk receives 4*(nk + 7) rou...
Definition aes_block.h:52
void pc_aes_encrypt_block(const uint32_t *rk, int nr, const uint8_t in[16], uint8_t out[16])
AES single-block encrypt (FIPS 197 sec 5.1), nr rounds (10=AES-128, 14=AES-256). State is column-majo...
Definition aes_block.h:82
Per-translation-unit optimization override for hot, pure-integer crypto.
Constant-time comparison for secret-dependent checks.
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:39
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:124
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:80
The one vendor/die selector for the whole library.
User-facing configuration for ProtoCore.
#define PC_WORK_AES128
#define PC_WORK_AES128GCM
pc_span pc_secure_span(size_t n, size_t align)
Borrow n secure bytes as a span whose capacity is bound to the allocation.
Definition secure.cpp:142
Secure pool accessor - borrows that hold key material.
pc_cspan pc_cspan_from(const uint8_t *p, size_t len)
Bind a read-only span to memory whose extent is only known at run time.
Definition span.h:103
bool pc_span_ok(const pc_span &s)
True when the span refers to real storage and every write so far has fit.
Definition span.h:114
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:34
A read-only byte region.
Definition span.h:79
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65
uint8_t * buf
first byte, or nullptr when the region could not be obtained
Definition span.h:66