ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
portable_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 portable_aesgcm.cpp
6 * @brief AES-256-GCM in software - the backend for a target with no accelerated AEAD.
7 *
8 * Software AES-256 plus a 4-bit-table GHASH. Selected explicitly by a vendor profile that sets
9 * PC_HAS_HW_AESGCM 0; never a fallback. Software crypto is a legitimate choice and on some parts the
10 * only one, but arriving here by default is not - there is no weak symbol anywhere in the chain, so
11 * linking no backend is an undefined reference and linking two is a duplicate definition.
12 *
13 * Expect roughly an order of magnitude less throughput than a vendor AEAD (measured on an ESP32-S3:
14 * 616k cycles/KiB for the software GHASH path against 81k for the vendor's). A part that HAS an
15 * accelerated GCM should never be pointed here.
16 */
17
19#include "crypto/aead/aesgcm.h"
21#include "crypto/crypto_opt.h"
22#include "crypto/crypto_scratch.h" // pc_ct_eq
23#include "crypto/mac/ghash.h"
24#include "server/mmgr/secure.h"
25#include <string.h>
26
27#if !PC_HAS_HW_AESGCM
28
30
31// ===========================================================================
32// GcmWork: the entire AES-256-GCM working set, laid over the shared crypto scratch. No cipher state on
33// the stack; the whole struct is wiped after each operation.
34// ===========================================================================
35struct GcmWork
36{
37 uint32_t rk[60]; ///< AES-256 expanded round-key schedule (software).
38 uint8_t h[16]; ///< GHASH subkey H = E(K, 0^128).
39 GhashKey ghk; ///< 4-bit GHASH table built from H.
40 uint8_t ks[16]; ///< GCTR keystream block (also the zero input used to derive H).
41 uint8_t acc[16]; ///< GHASH accumulator.
42 uint8_t lb[16]; ///< length block (aad_len || cipher_len, in bits).
43 uint8_t ej0[16]; ///< E(K, J0), the tag mask.
44 uint8_t j0[16]; ///< pre-counter block J0 = nonce || 0^31 || 1.
45 uint8_t ctr[16]; ///< running GCTR counter.
46};
47static_assert(
48 sizeof(GcmWork) <= PC_WORK_AESGCM,
49 "GcmWork outgrew PC_WORK_AESGCM - raise it in protocore_config.h, which derives PC_SECURE_ARENA_SIZE from it");
50
51// ---------------------------------------------------------------------------
52// AES-256 single-block primitive (operates on the schedule inside GcmWork)
53// ---------------------------------------------------------------------------
54namespace
55{
56inline void aes256_ecb(GcmWork *w, const uint8_t in[16], uint8_t out[16])
57{
58 pc_aes_encrypt_block(w->rk, 14, in, out);
59}
60inline void aes256_load_key(GcmWork *w, const uint8_t key[32])
61{
62 pc_aes_key_expand(key, 8, w->rk);
63}
64inline void aes256_free_key(GcmWork *w)
65{
66 (void)w; // software schedule lives in-place in GcmWork; nothing external to release
67}
68
69inline void xor16(uint8_t *dst, const uint8_t *src)
70{
71 // One block = four words. Both pointers must be 4-aligned: Xtensa has no native unaligned 32-bit
72 // load and the trap handler costs more than the byte loop, so test them together (OR then mask)
73 // and fall back rather than gamble.
74 if (((((uintptr_t)dst) | ((uintptr_t)src)) & 3u) == 0u)
75 {
76 uint32_t *d = (uint32_t *)dst;
77 const uint32_t *s = (const uint32_t *)src;
78 d[0] ^= s[0];
79 d[1] ^= s[1];
80 d[2] ^= s[2];
81 d[3] ^= s[3];
82 return;
83 }
84 for (int i = 0; i < 16; i++)
85 {
86 dst[i] ^= src[i];
87 }
88}
89
90inline void put_be64(uint8_t *p, uint64_t v)
91{
92 for (int i = 7; i >= 0; i--)
93 {
94 p[i] = (uint8_t)(v & 0xff);
95 v >>= 8;
96 }
97}
98
99// Increment the low 32 bits of a 16-byte counter block, big-endian, mod 2^32 (GCM inc32).
100inline void inc32(uint8_t ctr[16])
101{
102 // A single-byte carry (ctr[15] 0xff -> 0x00 into ctr[14]) is cheap to reach and exercised by
103 // test_aesgcm_gctr_counter_byte_carry; the full 2^32 wrap (~64 GiB in one call) is the only branch a
104 // host test cannot reach.
105 for (int i = 15; i >= 12; // GCOVR_EXCL_BR_LINE full 2^32 counter wrap (~64 GiB/call) unreachable
106 i--)
107 {
108 if (++ctr[i])
109 {
110 break;
111 }
112 }
113}
114
115// Derive the key-dependent state: H and the GHASH table. Done once per key, not once per record.
116void gcm_key_setup(GcmWork *w)
117{
118 memset(w->ks, 0, 16); // zero input for H (reuses the keystream slot; overwritten by gctr later)
119 aes256_ecb(w, w->ks, w->h); // H = E(K, 0^128)
120 ghash_key_init(&w->ghk, w->h);
121}
122
123// Per-record: J0 = nonce || 0^31 || 1.
124void gcm_set_nonce(GcmWork *w, const uint8_t nonce[12])
125{
126 memcpy(w->j0, nonce, 12);
127 w->j0[12] = 0;
128 w->j0[13] = 0;
129 w->j0[14] = 0;
130 w->j0[15] = 1;
131}
132
133// GCTR (NIST SP 800-38D sec 6.5): out = in XOR AES-CTR keystream from @p w->ctr, advanced in place.
134void gctr(GcmWork *w, const uint8_t *in, size_t len, uint8_t *out)
135{
136 size_t off = 0;
137 while (off < len)
138 {
139 aes256_ecb(w, w->ctr, w->ks);
140 inc32(w->ctr);
141 size_t take = len - off;
142 if (take > 16)
143 {
144 take = 16;
145 }
146 for (size_t i = 0; i < take; i++)
147 {
148 out[off + i] = in[off + i] ^ w->ks[i];
149 }
150 off += take;
151 }
152}
153
154// GHASH over aad || cipher, fold in the lengths, and produce the 16-byte tag (acc XOR E(K, J0)).
155void gcm_tag(GcmWork *w, const uint8_t *aad, size_t aad_len, const uint8_t *cipher, size_t cipher_len,
156 uint8_t tag_out[16])
157{
158 memset(w->acc, 0, 16);
159 ghash_update(&w->ghk, w->acc, aad, aad_len);
160 ghash_update(&w->ghk, w->acc, cipher, cipher_len);
161 put_be64(w->lb, (uint64_t)aad_len * 8);
162 put_be64(w->lb + 8, (uint64_t)cipher_len * 8);
163 xor16(w->acc, w->lb);
164 ghash_mul(&w->ghk, w->acc);
165 aes256_ecb(w, w->j0, w->ej0);
166 for (int i = 0; i < 16; i++)
167 {
168 tag_out[i] = w->acc[i] ^ w->ej0[i];
169 }
170}
171} // namespace
172
173// ===========================================================================
174// Public API (keyed)
175// ===========================================================================
176
177pc_aesgcm_key *pc_aesgcm_key_init(void *storage, const uint8_t key[PC_AESGCM_KEY_LEN])
178{
179 GcmWork *w = reinterpret_cast<GcmWork *>(storage);
180 aes256_load_key(w, key);
181 gcm_key_setup(w);
182 return reinterpret_cast<pc_aesgcm_key *>(w);
183}
184
185void pc_aesgcm_key_wipe(pc_aesgcm_key *k)
186{
187 GcmWork *w = reinterpret_cast<GcmWork *>(k);
188 aes256_free_key(w);
189 pc_secure_wipe(reinterpret_cast<uint8_t *>(w), sizeof(GcmWork));
190}
191
192pc_cspan pc_aesgcm_seal(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len,
193 const uint8_t *pt, size_t pt_len, uint8_t *ct_out, uint8_t tag_out[PC_AESGCM_TAG_LEN])
194{
195 GcmWork *w = reinterpret_cast<GcmWork *>(k);
196 gcm_set_nonce(w, nonce);
197 // Encrypt with the CTR starting at inc32(J0), then GHASH the resulting ciphertext.
198 memcpy(w->ctr, w->j0, 16);
199 inc32(w->ctr);
200 gctr(w, pt, pt_len, ct_out);
201 gcm_tag(w, aad, aad_len, ct_out, pt_len, tag_out);
202 return pc_cspan_from(ct_out, pt_len); // the tag rides in tag_out, not in this span
203}
204
205bool pc_aesgcm_open(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len,
206 const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESGCM_TAG_LEN], uint8_t *out)
207{
208 GcmWork *w = reinterpret_cast<GcmWork *>(k);
209 gcm_set_nonce(w, nonce);
210 // Authenticate over the received ciphertext BEFORE producing any plaintext (tag reuses the ej0 slot).
211 gcm_tag(w, aad, aad_len, ct, ct_len, w->ej0);
212 if (!pc_ct_eq(w->ej0, tag, PC_AESGCM_TAG_LEN))
213 {
214 return false; // tag mismatch: nothing written
215 }
216 memcpy(w->ctr, w->j0, 16);
217 inc32(w->ctr);
218 gctr(w, ct, ct_len, out);
219 return true;
220}
221
222#endif // !PC_HAS_HW_AESGCM
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
AES-256-GCM AEAD (RFC 5116) - stateless, detached-tag API.
#define PC_AESGCM_TAG_LEN
GCM authentication tag length (bytes).
Definition aesgcm.h:42
#define PC_AESGCM_IV_LEN
GCM nonce length (bytes) = fixed_field(4) || invocation_counter(8).
Definition aesgcm.h:40
#define PC_AESGCM_KEY_LEN
AES-256-GCM key length (bytes).
Definition aesgcm.h:38
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.
bool pc_aesgcm_open(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESGCM_TAG_LEN], uint8_t *out)
Open one record: verify tag over aad || ct in constant time, then (only on success) decrypt ct into o...
pc_aesgcm_key * pc_aesgcm_key_init(void *storage, const uint8_t key[PC_AESGCM_KEY_LEN])
Bind storage as a context keyed with key.
pc_cspan pc_aesgcm_seal(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out, uint8_t tag_out[PC_AESGCM_TAG_LEN])
Seal one record under k and nonce.
void pc_aesgcm_key_wipe(pc_aesgcm_key *k)
Wipe the expanded schedule. Call on rekey and on close; the storage stays the caller's.
#define PC_WORK_AESGCM
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
uint8_t ctr[16]
running GCTR counter.
uint8_t ks[16]
GCTR keystream block (also the zero input used to derive H).
uint8_t lb[16]
length block (aad_len || cipher_len, in bits).
uint8_t j0[16]
pre-counter block J0 = nonce || 0^31 || 1.
uint8_t h[16]
GHASH subkey H = E(K, 0^128).
uint32_t rk[60]
AES-256 expanded round-key schedule (software).
uint8_t ej0[16]
E(K, J0), the tag mask.
uint8_t acc[16]
GHASH accumulator.
GhashKey ghk
4-bit GHASH table built from H.
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