ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
aesccm.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 aesccm.cpp
6 * @brief AEAD AES-CCM (NIST SP 800-38C) - see aesccm.h.
7 *
8 * Arduino: mbedtls_ccm (AES routed to the ESP32 HW accelerator), detached tag natively. Native host: a
9 * software CCM built on the shared table-free AES block (crypto/cipher/aes_block.h) - CBC-MAC for authentication
10 * (formatting function per SP 800-38C Appendix A) and AES-CTR for encryption, both under the one key.
11 *
12 * All working memory (key schedule, CBC-MAC accumulator, keystream, counter/format blocks) lives in the
13 * secure pool and is wiped when the borrow is released - no cipher state on the stack or in BSS.
14 */
15
16#include "crypto/aead/aesccm.h"
17#include "server/mmgr/secure.h"
18
19#if PC_ENABLE_SMB
20
21#include "crypto/crypto_opt.h"
22#include "crypto/crypto_scratch.h" // pc_ct_eq
23#include <string.h>
24#ifndef ARDUINO
25#include "crypto/cipher/aes_block.h" // native software AES-128/256 (mbedtls path uses its own on Arduino)
26#endif
28
29#ifdef ARDUINO
30// ===========================================================================
31// Hardware path: mbedtls_ccm -> ESP32 AES peripheral. Detached tag is native. The mbedtls context (AES
32// key schedule) lives in the shared crypto scratch, never on the stack.
33// ===========================================================================
34
35bool pc_aesccm_seal_tag(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t nonce_len, const uint8_t *aad,
36 size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out,
37 uint8_t tag_out[PC_AESCCM_TAG_LEN])
38{
39 if (!key || !nonce || !ct_out || !tag_out || (key_len != 16 && key_len != 32))
40 {
41 return false;
42 }
43 SecureBorrow ws_b(sizeof(mbedtls_ccm_context), alignof(mbedtls_ccm_context));
44 const pc_span &ws = ws_b.span();
45 if (!pc_span_ok(ws))
46 {
47 return false;
48 }
49 mbedtls_ccm_context *c = reinterpret_cast<mbedtls_ccm_context *>(ws.buf);
50 mbedtls_ccm_init(c);
51 if (mbedtls_ccm_setkey(c, MBEDTLS_CIPHER_ID_AES, key, (unsigned)(key_len * 8)) != 0)
52 {
53 mbedtls_ccm_free(c);
54 return false;
55 }
56 int rc =
57 mbedtls_ccm_encrypt_and_tag(c, pt_len, nonce, nonce_len, aad, aad_len, pt, ct_out, tag_out, PC_AESCCM_TAG_LEN);
58 mbedtls_ccm_free(c);
59 return rc == 0;
60}
61
62bool pc_aesccm_open_tag(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t nonce_len, const uint8_t *aad,
63 size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESCCM_TAG_LEN],
64 uint8_t *out)
65{
66 if (!key || !nonce || !ct || !out || !tag || (key_len != 16 && key_len != 32))
67 {
68 return false;
69 }
70 SecureBorrow ws_b(sizeof(mbedtls_ccm_context), alignof(mbedtls_ccm_context));
71 const pc_span &ws = ws_b.span();
72 if (!pc_span_ok(ws))
73 {
74 return false;
75 }
76 mbedtls_ccm_context *c = reinterpret_cast<mbedtls_ccm_context *>(ws.buf);
77 mbedtls_ccm_init(c);
78 if (mbedtls_ccm_setkey(c, MBEDTLS_CIPHER_ID_AES, key, (unsigned)(key_len * 8)) != 0)
79 {
80 mbedtls_ccm_free(c);
81 return false;
82 }
83 // mbedtls verifies the tag in constant time and only then keeps the plaintext; non-zero => bad tag.
84 int rc = mbedtls_ccm_auth_decrypt(c, ct_len, nonce, nonce_len, aad, aad_len, ct, out, tag, PC_AESCCM_TAG_LEN);
85 mbedtls_ccm_free(c);
86 if (rc != 0)
87 {
88 memset(out, 0, ct_len);
89 return false;
90 }
91 return true;
92}
93
94#else // native software CCM
95// ===========================================================================
96// Software path (NIST SP 800-38C): CBC-MAC over B0 || fmt(AAD) || fmt(PT), CTR encryption from A1, and
97// the tag encrypted with the counter block A0. The whole working set (CcmWork) is one pool borrow.
98// ===========================================================================
99
100namespace
101{
102// The entire CCM working set, laid over the shared crypto scratch. No key schedule or keystream on the
103// stack; the whole struct is wiped after each operation.
104struct CcmWork
105{
106 uint32_t rk[60]; ///< AES key schedule (44 words for AES-128, 60 for AES-256).
107 int nr; ///< rounds (10 or 14).
108 uint8_t X[16]; ///< CBC-MAC accumulator (holds the raw MAC T when done).
109 uint8_t blk[16]; ///< formatting block (B0 / AAD / payload / computed tag).
110 uint8_t A[16]; ///< CTR counter block.
111 uint8_t S[16]; ///< keystream / ECB output.
112};
113static_assert(
114 sizeof(CcmWork) <= PC_WORK_AESCCM,
115 "CcmWork outgrew PC_WORK_AESCCM - raise it in protocore_config.h, which derives PC_SECURE_ARENA_SIZE from it");
116
117inline void ccm_key_init(CcmWork *w, const uint8_t *key, size_t key_len)
118{
119 if (key_len == 32)
120 {
121 pc_aes_key_expand(key, 8, w->rk);
122 w->nr = 14;
123 }
124 else
125 {
126 pc_aes_key_expand(key, 4, w->rk);
127 w->nr = 10;
128 }
129}
130
131inline void ecb(const CcmWork *w, const uint8_t in[16], uint8_t out[16])
132{
133 pc_aes_encrypt_block(w->rk, w->nr, in, out);
134}
135
136// Build the counter block A_i = flags(L-1) || nonce || [i]_L (SP 800-38C Appendix A, the CTR formatting).
137inline void ctr_block(uint8_t A[16], const uint8_t *nonce, size_t nonce_len, size_t i)
138{
139 const size_t L = 15 - nonce_len;
140 memset(A, 0, 16);
141 A[0] = (uint8_t)(L - 1);
142 memcpy(A + 1, nonce, nonce_len);
143 for (size_t j = 0; j < L; j++)
144 {
145 A[15 - j] = (uint8_t)((i >> (8 * j)) & 0xff);
146 }
147}
148
149// CBC-MAC of B0 || fmt(aad) || fmt(pt) -> w->X (SP 800-38C ยง6.1 / Appendix A formatting). Uses w->blk.
150void cbc_mac(CcmWork *w, const uint8_t *nonce, size_t nonce_len, const uint8_t *aad, size_t aad_len, const uint8_t *pt,
151 size_t pt_len)
152{
153 const size_t L = 15 - nonce_len;
154 memset(w->X, 0, 16);
155
156 // B0: flags = 64*Adata + 8*((M-2)/2) + (L-1); then nonce; then Q = pt_len big-endian in L bytes.
157 memset(w->blk, 0, 16);
158 w->blk[0] = (uint8_t)((aad_len > 0 ? 0x40 : 0x00) | (((PC_AESCCM_TAG_LEN - 2) / 2) << 3) | (L - 1));
159 memcpy(w->blk + 1, nonce, nonce_len);
160 for (size_t j = 0; j < L; j++)
161 {
162 w->blk[15 - j] = (uint8_t)((pt_len >> (8 * j)) & 0xff);
163 }
164 for (int i = 0; i < 16; i++)
165 {
166 w->X[i] ^= w->blk[i];
167 }
168 ecb(w, w->X, w->X);
169
170 // Associated data: a 2-byte big-endian length prefix (for 0 < aad_len < 0xFF00) then the AAD, packed
171 // into 16-byte blocks and zero-padded.
172 if (aad_len > 0)
173 {
174 memset(w->blk, 0, 16);
175 w->blk[0] = (uint8_t)((aad_len >> 8) & 0xff);
176 w->blk[1] = (uint8_t)(aad_len & 0xff);
177 size_t fill = 2;
178 size_t off = 0;
179 while (off < aad_len)
180 {
181 size_t take = 16 - fill;
182 if (take > aad_len - off)
183 {
184 take = aad_len - off;
185 }
186 memcpy(w->blk + fill, aad + off, take);
187 off += take;
188 fill += take;
189 for (int i = 0; i < 16; i++)
190 {
191 w->X[i] ^= w->blk[i];
192 }
193 ecb(w, w->X, w->X);
194 memset(w->blk, 0, 16);
195 fill = 0;
196 }
197 }
198
199 // Payload blocks, zero-padded to 16.
200 size_t off = 0;
201 while (off < pt_len)
202 {
203 memset(w->blk, 0, 16);
204 size_t take = pt_len - off;
205 if (take > 16)
206 {
207 take = 16;
208 }
209 memcpy(w->blk, pt + off, take);
210 for (int i = 0; i < 16; i++)
211 {
212 w->X[i] ^= w->blk[i];
213 }
214 ecb(w, w->X, w->X);
215 off += take;
216 }
217}
218
219// AES-CTR from counter block index @p i0 (A_{i0}, A_{i0+1}, ...). @p in / @p out may alias. Uses w->A/w->S.
220void ctr_crypt(CcmWork *w, const uint8_t *nonce, size_t nonce_len, size_t i0, const uint8_t *in, size_t len,
221 uint8_t *out)
222{
223 size_t off = 0;
224 size_t i = i0;
225 while (off < len)
226 {
227 ctr_block(w->A, nonce, nonce_len, i);
228 ecb(w, w->A, w->S);
229 size_t take = len - off;
230 if (take > 16)
231 {
232 take = 16;
233 }
234 for (size_t j = 0; j < take; j++)
235 {
236 out[off + j] = in[off + j] ^ w->S[j];
237 }
238 off += take;
239 i++;
240 }
241}
242
243// Encrypted tag = T XOR AES(A0) (the counter block for i = 0 protects the MAC). Reads the MAC from w->X,
244// uses w->A/w->S, writes @p out_tag.
245void tag_encrypt(CcmWork *w, const uint8_t *nonce, size_t nonce_len, uint8_t out_tag[PC_AESCCM_TAG_LEN])
246{
247 ctr_block(w->A, nonce, nonce_len, 0);
248 ecb(w, w->A, w->S);
249 for (int i = 0; i < PC_AESCCM_TAG_LEN; i++)
250 {
251 out_tag[i] = (uint8_t)(w->X[i] ^ w->S[i]);
252 }
253}
254} // namespace
255
256bool pc_aesccm_seal_tag(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t nonce_len, const uint8_t *aad,
257 size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out,
258 uint8_t tag_out[PC_AESCCM_TAG_LEN])
259{
260 if (!key || !nonce || !ct_out || !tag_out || (key_len != 16 && key_len != 32) || nonce_len < 7 || nonce_len > 13)
261 {
262 return false;
263 }
264 SecureBorrow ws_b(sizeof(CcmWork), alignof(CcmWork));
265 const pc_span &ws = ws_b.span();
266 if (!pc_span_ok(ws))
267 {
268 return false;
269 }
270 CcmWork *w = reinterpret_cast<CcmWork *>(ws.buf);
271 ccm_key_init(w, key, key_len);
272 cbc_mac(w, nonce, nonce_len, aad, aad_len, pt, pt_len); // MAC -> w->X
273 ctr_crypt(w, nonce, nonce_len, 1, pt, pt_len, ct_out); // payload from A1
274 tag_encrypt(w, nonce, nonce_len, tag_out); // MAC protected by A0
275 return true;
276}
277
278bool pc_aesccm_open_tag(const uint8_t *key, size_t key_len, const uint8_t *nonce, size_t nonce_len, const uint8_t *aad,
279 size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESCCM_TAG_LEN],
280 uint8_t *out)
281{
282 if (!key || !nonce || !ct || !out || !tag || (key_len != 16 && key_len != 32) || nonce_len < 7 || nonce_len > 13)
283 {
284 return false;
285 }
286 SecureBorrow ws_b(sizeof(CcmWork), alignof(CcmWork));
287 const pc_span &ws = ws_b.span();
288 if (!pc_span_ok(ws))
289 {
290 return false;
291 }
292 CcmWork *w = reinterpret_cast<CcmWork *>(ws.buf);
293 ccm_key_init(w, key, key_len);
294 ctr_crypt(w, nonce, nonce_len, 1, ct, ct_len, out); // recover plaintext into out
295 cbc_mac(w, nonce, nonce_len, aad, aad_len, out, ct_len); // MAC over the recovered plaintext -> w->X
296 tag_encrypt(w, nonce, nonce_len, w->blk); // computed tag into w->blk (free after cbc_mac)
297 if (!pc_ct_eq(w->blk, tag, PC_AESCCM_TAG_LEN))
298 {
299 memset(out, 0, ct_len); // fail closed: no unauthenticated plaintext escapes
300 return false;
301 }
302 return true;
303}
304
305#endif // ARDUINO
306#endif // PC_ENABLE_SMB
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
AEAD AES-CCM (NIST SP 800-38C / RFC 3610), 128- and 256-bit keys, detached tag.
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
Per-translation-unit optimization override for hot, pure-integer crypto.
Constant-time comparison for secret-dependent checks.
#define PC_WORK_AESCCM
Secure pool accessor - borrows that hold key material.
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
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