DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
tls.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 tls.cpp
6 * @brief Deterministic TLS engine implementation (mbedTLS + static pool).
7 *
8 * ESP32/Arduino only. All mbedTLS allocations are served from a fixed BSS arena
9 * (MBEDTLS_MEMORY_BUFFER_ALLOC_C) so no system heap is touched; the RNG is the
10 * ESP32 hardware CSPRNG; the transport BIO reads ciphertext straight from the
11 * connection's rx ring and writes via tcp_write. v2/v3 mbedTLS differences are
12 * bridged with MBEDTLS_VERSION_MAJOR guards (same approach as the SSH layer).
13 */
14
16
17#if DETWS_ENABLE_TLS && defined(ARDUINO)
18
19#include "lwip/tcp.h"
21#include <Arduino.h> // millis(), delay() for the blocking client loop
22#include <esp_system.h> // esp_fill_random (HW CSPRNG)
23#include <string.h>
24
25#include <mbedtls/error.h>
26#include <mbedtls/pk.h>
27#include <mbedtls/platform.h> // mbedtls_platform_set_calloc_free
28#include <mbedtls/sha256.h> // peer-cert pin hashing (client verification)
29#include <mbedtls/ssl.h>
30#include <mbedtls/version.h>
31#include <mbedtls/x509_crt.h>
32#if DETWS_ENABLE_TLS_RESUMPTION
33#include <mbedtls/ssl_ticket.h> // RFC 5077 session tickets (server-side resumption)
34#if !defined(MBEDTLS_SSL_TICKET_C) || !defined(MBEDTLS_SSL_SESSION_TICKETS)
35#error "DETWS_ENABLE_TLS_RESUMPTION needs an mbedTLS build with MBEDTLS_SSL_TICKET_C + MBEDTLS_SSL_SESSION_TICKETS"
36#endif
37#endif
38
39// ---------------------------------------------------------------------------
40// Static memory pool - a minimal first-fit allocator over a fixed BSS arena.
41//
42// The precompiled Arduino mbedTLS does not ship MBEDTLS_MEMORY_BUFFER_ALLOC_C,
43// so instead we install our own calloc/free via mbedtls_platform_set_calloc_free
44// (MBEDTLS_PLATFORM_MEMORY). Every mbedTLS allocation (record buffers, handshake
45// temporaries, cert/key) is then served from s_pool.arena - no system heap, so the
46// determinism guarantee holds. Bounded by the arena: exhaustion fails the
47// handshake cleanly (calloc returns NULL) rather than corrupting anything.
48//
49// Placement: by default the arena is internal DRAM (.bss). On a board with PSRAM,
50// set DETWS_TLS_ARENA_IN_PSRAM=1 to move it to external RAM (frees ~DETWS_TLS_ARENA_SIZE
51// of the ~122 KB internal dram0_0_seg budget, so several concurrent connections fit).
52//
53// This needs a framework built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y, which the
54// STOCK arduino-esp32 core (2.x and 3.x, both PlatformIO and arduino-cli) ships OFF. With it
55// off, EXT_RAM_BSS_ATTR silently expands to nothing and the arena falls back to internal DRAM
56// - i.e. the PSRAM offload silently does not happen. Rather than fail silently we FAIL THE
57// COMPILE and point at the rebuild recipe (tools/psram/README.md). Rebuild the core (pioarduino
58// custom_sdkconfig, or esp32-arduino-lib-builder) or unset DETWS_TLS_ARENA_IN_PSRAM.
59//
60// FLASH-CACHE CAVEAT: PSRAM is on the flash cache bus, so while flash is being written (an NVS
61// commit, an OTA) PSRAM is briefly unreadable - TLS code that touches the arena during that
62// window faults. It is a single pool, so this is a whole-build choice: use PSRAM for a TLS
63// workload that does not write flash while serving; keep the arena in internal DRAM (the
64// default) if you do OTA / NVS / file-serving concurrently with live TLS. See
65// docs/KNOWN_LIMITATIONS.md (TLS -> "Flash-cache / OTA caveat").
66// ---------------------------------------------------------------------------
67#if DETWS_TLS_ARENA_IN_PSRAM && defined(ARDUINO)
68#include <esp_attr.h> // pulls in sdkconfig.h -> CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
69#if !defined(CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY)
70#error \
71 "DETWS_TLS_ARENA_IN_PSRAM needs a framework built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y. The stock arduino-esp32 core ships it OFF, so EXT_RAM_BSS_ATTR silently no-ops and the arena would stay in internal RAM. Rebuild the core (see tools/psram/README.md) or unset DETWS_TLS_ARENA_IN_PSRAM."
72#endif
73#if defined(EXT_RAM_BSS_ATTR)
74#define DETWS_TLS_ARENA_ATTR EXT_RAM_BSS_ATTR // IDF v5 / arduino-esp32 3.x
75#elif defined(EXT_RAM_ATTR)
76#define DETWS_TLS_ARENA_ATTR EXT_RAM_ATTR // IDF v4 / arduino-esp32 2.x
77#else
78#error "DETWS_TLS_ARENA_IN_PSRAM: no EXT_RAM_BSS_ATTR/EXT_RAM_ATTR from the framework (unexpected)."
79#endif
80#else
81#define DETWS_TLS_ARENA_ATTR
82#endif
83// Arena allocator state, owned by one instance (internal linkage): the static arena backing
84// every mbedTLS object plus the first-fit pool cursors. One named owner, unreachable cross-TU.
85struct TlsPoolCtx
86{
87 uint8_t arena[DETWS_TLS_ARENA_SIZE];
88 bool inited = false;
89 size_t used = 0;
90 size_t peak = 0;
91};
92DETWS_TLS_ARENA_ATTR static TlsPoolCtx s_pool;
93
94#define TLS_ALIGN 8u
95struct PoolBlk
96{
97 size_t size; // payload bytes
98 uint8_t used; // 0 = free, 1 = in use
99};
100static const size_t POOL_HDR = (sizeof(PoolBlk) + (TLS_ALIGN - 1)) & ~(size_t)(TLS_ALIGN - 1);
101
102static void pool_init()
103{
104 PoolBlk *b = (PoolBlk *)s_pool.arena;
105 b->size = sizeof(s_pool.arena) - POOL_HDR;
106 b->used = 0;
107 s_pool.used = 0;
108 s_pool.peak = 0;
109 s_pool.inited = true;
110}
111
112static void pool_coalesce()
113{
114 uint8_t *p = s_pool.arena;
115 uint8_t *end = s_pool.arena + sizeof(s_pool.arena);
116 while (p < end)
117 {
118 PoolBlk *b = (PoolBlk *)p;
119 uint8_t *next = p + POOL_HDR + b->size;
120 if (!b->used && next < end)
121 {
122 PoolBlk *nb = (PoolBlk *)next;
123 if (!nb->used)
124 {
125 b->size += POOL_HDR + nb->size; // merge
126 continue; // try merging further
127 }
128 }
129 p = next;
130 }
131}
132
133static void *pool_calloc(size_t n, size_t size)
134{
135 if (!s_pool.inited)
136 pool_init();
137 if (n != 0 && size > (size_t)-1 / n)
138 return nullptr; // overflow
139 size_t want = n * size;
140 want = (want + (TLS_ALIGN - 1)) & ~(size_t)(TLS_ALIGN - 1);
141 if (want == 0)
142 want = TLS_ALIGN;
143
144 uint8_t *p = s_pool.arena;
145 uint8_t *end = s_pool.arena + sizeof(s_pool.arena);
146 while (p < end)
147 {
148 PoolBlk *b = (PoolBlk *)p;
149 if (!b->used && b->size >= want)
150 {
151 // Split if the remainder can hold another header + a min payload.
152 if (b->size >= want + POOL_HDR + TLS_ALIGN)
153 {
154 PoolBlk *nb = (PoolBlk *)(p + POOL_HDR + want);
155 nb->size = b->size - want - POOL_HDR;
156 nb->used = 0;
157 b->size = want;
158 }
159 b->used = 1;
160 s_pool.used += b->size;
161 if (s_pool.used > s_pool.peak)
162 s_pool.peak = s_pool.used;
163 void *payload = p + POOL_HDR;
164 memset(payload, 0, b->size);
165 return payload;
166 }
167 p += POOL_HDR + b->size;
168 }
169 return nullptr; // arena exhausted
170}
171
172static void pool_free(void *ptr)
173{
174 if (!ptr)
175 return;
176 PoolBlk *b = (PoolBlk *)((uint8_t *)ptr - POOL_HDR);
177 if (b->used)
178 {
179 b->used = 0;
180 if (s_pool.used >= b->size)
181 s_pool.used -= b->size;
182 }
183 pool_coalesce();
184}
185
186// The server-config "ready" flag, owned separately from the multi-hundred-byte mbedTLS
187// server state below. det_tls_ready() (and the client-side guards) read this flag every
188// call; grouping it into TlsServerCtx would anchor the whole server config/cert/key/ticket
189// via that always-live reference, keeping ~600 bytes linked even in a client-only firmware
190// that never runs det_tls_configure(). Kept apart, --gc-sections drops s_srv when server
191// setup is unused.
192struct TlsServerReadyCtx
193{
194 bool ready = false;
195};
196static TlsServerReadyCtx s_srv_ready;
197
198// TLS server config, owned by one instance (internal linkage): the mbedTLS config/cert/key,
199// the optional mTLS client-cert trust anchor, and the resumption ticket key. Referenced only
200// by the server-setup path, so a client-only build garbage-collects it. One named owner.
201struct TlsServerCtx
202{
203 mbedtls_ssl_config conf;
204 mbedtls_x509_crt cert;
205 mbedtls_pk_context key;
206#if DETWS_ENABLE_MTLS
207 mbedtls_x509_crt ca; // client-cert trust anchor (mTLS)
208#endif
209#if DETWS_ENABLE_TLS_RESUMPTION
210 mbedtls_ssl_ticket_context ticket_ctx; // server-held key for sealing session tickets
211#endif
212};
213static TlsServerCtx s_srv;
214
215struct TlsConn
216{
217 mbedtls_ssl_context ssl;
218 struct tcp_pcb *pcb; // captured at begin: the senders null conn->pcb mid-write
219 uint8_t slot;
220 bool active;
221 bool established;
222};
223// TLS connection pool, owned by one instance (internal linkage): the per-slot mbedTLS session
224// contexts. One named owner, unreachable from any other translation unit.
225struct TlsConnsCtx
226{
227 TlsConn conns[MAX_TLS_CONNS];
228};
229static TlsConnsCtx s_conns;
230
231static TlsConn *find(uint8_t slot)
232{
233 for (uint8_t i = 0; i < MAX_TLS_CONNS; i++)
234 if (s_conns.conns[i].active && s_conns.conns[i].slot == slot)
235 return &s_conns.conns[i];
236 return nullptr;
237}
238
239// ---------------------------------------------------------------------------
240// RNG + transport BIO
241// ---------------------------------------------------------------------------
242static int tls_rng(void *ctx, unsigned char *out, size_t len)
243{
244 (void)ctx;
245 esp_fill_random(out, len); // ESP32 hardware CSPRNG
246 return 0;
247}
248
249// Server BIO recv (a det_tls_bio_recv_fn): pull ciphertext from the connection's
250// rx ring (filled by the lwIP recv callback). No bytes -> WANT_READ so mbedTLS
251// yields to the loop. Reads only the ring, so it is safe from either context.
252static int server_bio_recv(void *ctx, unsigned char *buf, size_t len)
253{
254 TlsConn *e = (TlsConn *)ctx;
255 size_t n = det_conn_read(e->slot, buf, len); // ciphertext from the rx ring
256 if (n == 0)
257 return MBEDTLS_ERR_SSL_WANT_READ;
258 return (int)n;
259}
260
261// Server BIO send (a det_tls_bio_send_fn): emit ciphertext through the transport's
262// context-safe raw write (det_conn_raw_send), so the handshake - pumped from the
263// main loop - never does an unsynchronized tcp_write racing the lwIP thread, while
264// app-data writes (already in the lwIP thread) still go out directly. Uses the pcb
265// captured at begin() because the response senders null conn->pcb before writing.
266static int server_bio_send(void *ctx, const unsigned char *buf, size_t len)
267{
268 TlsConn *e = (TlsConn *)ctx;
269 if (!e->pcb)
270 return MBEDTLS_ERR_SSL_INTERNAL_ERROR;
271
272 size_t avail = tcp_sndbuf(e->pcb);
273 if (avail == 0)
274 return MBEDTLS_ERR_SSL_WANT_WRITE; // backpressure; retry next pump
275 size_t to = len;
276 if (to > avail)
277 to = avail;
278 if (to > 0xFFFF)
279 to = 0xFFFF;
280
281 if (det_conn_raw_send(e->pcb, buf, (u16_t)to))
282 return (int)to;
283 return MBEDTLS_ERR_SSL_WANT_WRITE; // send buffer full -> mbedTLS retries
284}
285
286// Apply the configured TLS Maximum Fragment Length (RFC 6066) to @p conf: records are
287// capped at DETWS_TLS_MAX_FRAG_LEN. With a variable-buffer-length mbedTLS build this
288// also shrinks per-connection arena use; otherwise it bounds the on-wire record size
289// (bandwidth / latency on a constrained link) and honors a client's MFL request. A
290// no-op when the knob is 0 or the mbedTLS build lacks MBEDTLS_SSL_MAX_FRAGMENT_LENGTH.
291static void tls_apply_max_frag_len(mbedtls_ssl_config *conf)
292{
293#if DETWS_TLS_MAX_FRAG_LEN && defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH)
294#if DETWS_TLS_MAX_FRAG_LEN <= 512
295 mbedtls_ssl_conf_max_frag_len(conf, MBEDTLS_SSL_MAX_FRAG_LEN_512);
296#elif DETWS_TLS_MAX_FRAG_LEN <= 1024
297 mbedtls_ssl_conf_max_frag_len(conf, MBEDTLS_SSL_MAX_FRAG_LEN_1024);
298#elif DETWS_TLS_MAX_FRAG_LEN <= 2048
299 mbedtls_ssl_conf_max_frag_len(conf, MBEDTLS_SSL_MAX_FRAG_LEN_2048);
300#else
301 mbedtls_ssl_conf_max_frag_len(conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096);
302#endif
303#else
304 (void)conf;
305#endif
306}
307
308// Pin the ECDHE curve/group preference (RFC 8446 supported_groups / RFC 8422 for TLS 1.2).
309// This is PERFORMANCE-CRITICAL on a chip with no ECC accelerator: mbedTLS, given no explicit
310// preference, negotiates the FIRST curve in its own default list that the client also offers,
311// and on the esp-idf mbedTLS build that is secp521r1 - the MOST expensive curve. The ECDHE
312// variable-base scalar multiply is the single dominant handshake op, and a P-521 one runs ~2.4x
313// a P-256/x25519 one in software. Measured end-to-end on an ESP32-S3 (full TLS 1.2 handshake,
314// ECDHE-ECDSA-AES256-GCM): default(secp521r1) ~1000 ms -> x25519 / secp256r1 ~487 ms (2.05x).
315// (For reference the fixed ECDSA-P256 server signature is only ~63 ms; the curve is what moves.)
316//
317// We pin a fast, modern order: x25519 then secp256r1 (both 128-bit-security, the industry-default
318// curves) ahead of secp384r1/secp521r1 (kept only for interop with a peer that offers nothing
319// cheaper). Every curve stays available - this only reorders PREFERENCE, so a client that supports
320// just one of them still connects. Applied to both the server config and the outbound-client config
321// so the device never pays for an oversized curve it did not need.
322static void tls_apply_curve_pref(mbedtls_ssl_config *conf)
323{
324#if MBEDTLS_VERSION_MAJOR >= 3
325 static const uint16_t kGroupPref[] = {
326#if defined(MBEDTLS_ECP_DP_CURVE25519_ENABLED)
327 MBEDTLS_SSL_IANA_TLS_GROUP_X25519,
328#endif
329#if defined(MBEDTLS_ECP_DP_SECP256R1_ENABLED)
330 MBEDTLS_SSL_IANA_TLS_GROUP_SECP256R1,
331#endif
332#if defined(MBEDTLS_ECP_DP_SECP384R1_ENABLED)
333 MBEDTLS_SSL_IANA_TLS_GROUP_SECP384R1,
334#endif
335#if defined(MBEDTLS_ECP_DP_SECP521R1_ENABLED)
336 MBEDTLS_SSL_IANA_TLS_GROUP_SECP521R1,
337#endif
338 0,
339 };
340 mbedtls_ssl_conf_groups(conf, kGroupPref);
341#else
342 static const mbedtls_ecp_group_id kCurvePref[] = {
343#if defined(MBEDTLS_ECP_DP_CURVE25519_ENABLED)
344 MBEDTLS_ECP_DP_CURVE25519,
345#endif
346#if defined(MBEDTLS_ECP_DP_SECP256R1_ENABLED)
347 MBEDTLS_ECP_DP_SECP256R1,
348#endif
349#if defined(MBEDTLS_ECP_DP_SECP384R1_ENABLED)
350 MBEDTLS_ECP_DP_SECP384R1,
351#endif
352#if defined(MBEDTLS_ECP_DP_SECP521R1_ENABLED)
353 MBEDTLS_ECP_DP_SECP521R1,
354#endif
355 MBEDTLS_ECP_DP_NONE,
356 };
357 mbedtls_ssl_conf_curves(conf, kCurvePref);
358#endif
359}
360
361// ---------------------------------------------------------------------------
362// Public API
363// ---------------------------------------------------------------------------
364bool det_tls_global_init(const uint8_t *cert, size_t cert_len, const uint8_t *key, size_t key_len)
365{
366 if (s_srv_ready.ready)
367 return true;
368 if (!cert || !key)
369 return false;
370
371 // Route ALL mbedTLS allocations through our static arena before any mbedTLS
372 // object is initialized.
373 pool_init();
374 mbedtls_platform_set_calloc_free(pool_calloc, pool_free);
375
376 mbedtls_x509_crt_init(&s_srv.cert);
377 mbedtls_pk_init(&s_srv.key);
378 mbedtls_ssl_config_init(&s_srv.conf);
379
380 if (mbedtls_x509_crt_parse(&s_srv.cert, cert, cert_len) != 0)
381 return false;
382
383#if MBEDTLS_VERSION_MAJOR >= 3
384 if (mbedtls_pk_parse_key(&s_srv.key, key, key_len, nullptr, 0, tls_rng, nullptr) != 0)
385 return false;
386#else
387 if (mbedtls_pk_parse_key(&s_srv.key, key, key_len, nullptr, 0) != 0)
388 return false;
389#endif
390
391 if (mbedtls_ssl_config_defaults(&s_srv.conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM,
392 MBEDTLS_SSL_PRESET_DEFAULT) != 0)
393 return false;
394
395 mbedtls_ssl_conf_rng(&s_srv.conf, tls_rng, nullptr);
396 tls_apply_max_frag_len(&s_srv.conf); // RFC 6066 record cap (DETWS_TLS_MAX_FRAG_LEN)
397 tls_apply_curve_pref(&s_srv.conf); // prefer cheap curves (no ECC HW on the S3) - see helper
398#if MBEDTLS_VERSION_MAJOR >= 3
399 mbedtls_ssl_conf_min_tls_version(&s_srv.conf, MBEDTLS_SSL_VERSION_TLS1_2);
400#else
401 mbedtls_ssl_conf_min_version(&s_srv.conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
402#endif
403
404 if (mbedtls_ssl_conf_own_cert(&s_srv.conf, &s_srv.cert, &s_srv.key) != 0)
405 return false;
406
407#if DETWS_ENABLE_HTTP2
408 // Offer HTTP/2 over TLS via ALPN (RFC 7301), falling back to HTTP/1.1. The list must outlive
409 // the config, so it is static; det_tls_alpn() reports the negotiated choice post-handshake.
410 static const char *s_alpn[] = {"h2", "http/1.1", nullptr}; // mbedTLS keeps this pointer
411 if (mbedtls_ssl_conf_alpn_protocols(&s_srv.conf, s_alpn) != 0)
412 return false;
413#endif
414
415#if DETWS_ENABLE_TLS_RESUMPTION
416 // RFC 5077 session tickets: a returning client resumes with an abbreviated
417 // handshake. Stateless (the session lives in the client's sealed ticket), so
418 // no per-session cache grows in the arena. The ticket key rotates on the
419 // configured lifetime. mbedtls_ssl_ticket_write/parse are the default codec.
420 mbedtls_ssl_ticket_init(&s_srv.ticket_ctx);
421 if (mbedtls_ssl_ticket_setup(&s_srv.ticket_ctx, tls_rng, nullptr, MBEDTLS_CIPHER_AES_256_GCM,
423 return false;
424 mbedtls_ssl_conf_session_tickets_cb(&s_srv.conf, mbedtls_ssl_ticket_write, mbedtls_ssl_ticket_parse,
425 &s_srv.ticket_ctx);
426#endif
427
428 for (uint8_t i = 0; i < MAX_TLS_CONNS; i++)
429 s_conns.conns[i].active = false;
430
431 s_srv_ready.ready = true;
432 return true;
433}
434
435bool det_tls_ready()
436{
437 return s_srv_ready.ready;
438}
439
440const char *det_tls_alpn(uint8_t slot)
441{
442 TlsConn *c = find(slot);
443 return c ? mbedtls_ssl_get_alpn_protocol(&c->ssl) : nullptr;
444}
445
446bool det_tls_conn_begin(uint8_t slot)
447{
448 if (!s_srv_ready.ready)
449 return false;
450 TlsConn *e = nullptr;
451 for (uint8_t i = 0; i < MAX_TLS_CONNS; i++)
452 {
453 if (!s_conns.conns[i].active)
454 {
455 e = &s_conns.conns[i];
456 break;
457 }
458 }
459 if (!e)
460 return false; // TLS connection pool full
461
462 e->slot = slot;
463 e->pcb = conn_pool[slot].pcb;
464 e->active = true;
465 e->established = false;
466 mbedtls_ssl_init(&e->ssl);
467 if (mbedtls_ssl_setup(&e->ssl, &s_srv.conf) != 0)
468 {
469 mbedtls_ssl_free(&e->ssl);
470 e->active = false;
471 return false;
472 }
473 mbedtls_ssl_set_bio(&e->ssl, e, server_bio_send, server_bio_recv, nullptr);
474 return true;
475}
476
477int det_tls_handshake(uint8_t slot)
478{
479 TlsConn *e = find(slot);
480 if (!e)
481 return -1;
482 int ret = mbedtls_ssl_handshake(&e->ssl);
483 if (ret == 0)
484 {
485 e->established = true;
486 return 1;
487 }
488 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
489 return 0;
490 return -1;
491}
492
493bool det_tls_established(uint8_t slot)
494{
495 TlsConn *e = find(slot);
496 return e && e->established;
497}
498
499int det_tls_read(uint8_t slot, uint8_t *buf, size_t len)
500{
501 TlsConn *e = find(slot);
502 if (!e)
503 return -1;
504 int ret = mbedtls_ssl_read(&e->ssl, buf, len);
505 if (ret > 0)
506 return ret;
507 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
508 return 0; // no plaintext available yet
509 return -1; // close_notify, peer close, or fatal
510}
511
512int det_tls_write(uint8_t slot, const void *data, size_t len)
513{
514 TlsConn *e = find(slot);
515 if (!e)
516 return -1;
517 const unsigned char *p = (const unsigned char *)data;
518 size_t sent = 0;
519 uint16_t guard = 0; // bound retries so a stuck send buffer cannot spin forever
520 while (sent < len)
521 {
522 int ret = mbedtls_ssl_write(&e->ssl, p + sent, len - sent);
523 if (ret > 0)
524 {
525 sent += (size_t)ret;
526 guard = 0;
527 continue;
528 }
529 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
530 {
531 if (++guard > 64)
532 break; // give up this flush; backpressure
533 continue;
534 }
535 return -1;
536 }
537 return (int)sent;
538}
539
540void det_tls_conn_end(uint8_t slot)
541{
542 TlsConn *e = find(slot);
543 if (!e)
544 return;
545 mbedtls_ssl_close_notify(&e->ssl);
546 mbedtls_ssl_free(&e->ssl);
547 e->active = false;
548 e->established = false;
549 e->pcb = nullptr;
550}
551
552void det_tls_conn_free(uint8_t slot)
553{
554 TlsConn *e = find(slot);
555 if (!e)
556 return;
557 mbedtls_ssl_free(&e->ssl); // abrupt teardown: no close_notify (peer is gone)
558 e->active = false;
559 e->established = false;
560 e->pcb = nullptr;
561}
562
563size_t det_tls_arena_peak()
564{
565 return s_pool.peak;
566}
567
568#if DETWS_ENABLE_MTLS
569bool det_tls_set_client_ca(const uint8_t *ca, size_t ca_len)
570{
571 if (!s_srv_ready.ready || !ca)
572 return false;
573 mbedtls_x509_crt_init(&s_srv.ca);
574 if (mbedtls_x509_crt_parse(&s_srv.ca, ca, ca_len) != 0)
575 return false;
576 // Trust anchor for the client chain + demand a (valid) client cert: an absent
577 // or untrusted client certificate now fails the handshake.
578 mbedtls_ssl_conf_ca_chain(&s_srv.conf, &s_srv.ca, nullptr);
579 mbedtls_ssl_conf_authmode(&s_srv.conf, MBEDTLS_SSL_VERIFY_REQUIRED);
580 return true;
581}
582
583int det_tls_peer_subject(uint8_t slot, char *out, size_t out_len)
584{
585 if (!out || out_len == 0)
586 return -1;
587 out[0] = '\0';
588 TlsConn *e = find(slot);
589 if (!e || !e->established)
590 return -1;
591 const mbedtls_x509_crt *peer = mbedtls_ssl_get_peer_cert(&e->ssl);
592 if (!peer)
593 return -1;
594 int n = mbedtls_x509_dn_gets(out, out_len, &peer->subject);
595 return n; // bytes written (excl. NUL), or <0 on error
596}
597#endif // DETWS_ENABLE_MTLS
598
599#if DETWS_ENABLE_CLIENT_TLS
600// Optional client-side server authentication (default off = encrypt-only):
601// - a CA trust anchor -> mbedTLS verifies the chain + hostname during handshake;
602// - a 32-byte SHA-256 cert pin -> the peer's certificate DER is hashed and
603// constant-time compared after the handshake.
604// Either, both, or neither may be set; both must pass when both are set. Shared by
605// the one-shot HTTP client (det_tls_client_run) and the persistent session (csess).
606// Client-side server-authentication config, owned by one instance (internal linkage): the CA
607// trust anchor (+set flag) and the SHA-256 cert pin (+set flag). Shared by the one-shot HTTP
608// client and the persistent session. One named owner, unreachable from any other TU.
609struct TlsClientAuthCtx
610{
611 mbedtls_x509_crt ca;
612 bool ca_set = false;
613 uint8_t pin[32];
614 bool pin_set = false;
615};
616static TlsClientAuthCtx s_cli;
617
618// Route mbedTLS allocations through the static arena (the client may run before
619// any server-side TLS init has installed the allocator).
620static void client_arena_ensure()
621{
622 if (!s_pool.inited)
623 {
624 pool_init();
625 mbedtls_platform_set_calloc_free(pool_calloc, pool_free);
626 }
627}
628
629void det_tls_client_set_ca(const uint8_t *ca, size_t ca_len)
630{
631 client_arena_ensure();
632 if (s_cli.ca_set)
633 mbedtls_x509_crt_free(&s_cli.ca);
634 s_cli.ca_set = false;
635 if (!ca || ca_len == 0)
636 return;
637 mbedtls_x509_crt_init(&s_cli.ca);
638 s_cli.ca_set = (mbedtls_x509_crt_parse(&s_cli.ca, ca, ca_len) == 0);
639 if (!s_cli.ca_set)
640 mbedtls_x509_crt_free(&s_cli.ca);
641}
642
643void det_tls_client_set_pin(const uint8_t sha256[32])
644{
645 if (!sha256)
646 {
647 s_cli.pin_set = false;
648 return;
649 }
650 memcpy(s_cli.pin, sha256, 32);
651 s_cli.pin_set = true;
652}
653
654void det_tls_client_clear_verify()
655{
656 if (s_cli.ca_set)
657 mbedtls_x509_crt_free(&s_cli.ca);
658 s_cli.ca_set = false;
659 s_cli.pin_set = false;
660}
661
662// Constant-time 32-byte compare (no early-out on the first differing byte).
663static bool ct_eq32(const uint8_t *a, const uint8_t *b)
664{
665 uint8_t d = 0;
666 for (int i = 0; i < 32; i++)
667 d |= (uint8_t)(a[i] ^ b[i]);
668 return d == 0;
669}
670
671// Apply the shared client config: RNG, server-auth mode (CA verify or
672// encrypt-only), TLS >= 1.2, and the CA chain when one is installed.
673static int client_conf_apply(mbedtls_ssl_config *conf)
674{
675 if (mbedtls_ssl_config_defaults(conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
676 MBEDTLS_SSL_PRESET_DEFAULT) != 0)
677 return -1;
678 mbedtls_ssl_conf_rng(conf, tls_rng, nullptr);
679 tls_apply_max_frag_len(conf); // RFC 6066 record cap (DETWS_TLS_MAX_FRAG_LEN)
680 tls_apply_curve_pref(conf); // offer cheap curves first (no ECC HW on the S3) - see helper
681 if (s_cli.ca_set)
682 {
683 mbedtls_ssl_conf_ca_chain(conf, &s_cli.ca, nullptr);
684 mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_REQUIRED);
685 }
686 else
687 mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_NONE); // encrypt-only (no trust store)
688#if MBEDTLS_VERSION_MAJOR >= 3
689 mbedtls_ssl_conf_min_tls_version(conf, MBEDTLS_SSL_VERSION_TLS1_2);
690#else
691 mbedtls_ssl_conf_min_version(conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
692#endif
693#if DETWS_ENABLE_TLS_RESUMPTION
694 // Accept a server-issued session ticket (RFC 5077) so the client can resume an
695 // abbreviated handshake on reconnect (see the csess save/restore below).
696 mbedtls_ssl_conf_session_tickets(conf, MBEDTLS_SSL_SESSION_TICKETS_ENABLED);
697#endif
698 return 0;
699}
700
701// Post-handshake certificate pin check: true if no pin is set, or the peer's
702// certificate DER hashes to the installed pin (constant-time compared).
703static bool client_pin_ok(mbedtls_ssl_context *ssl)
704{
705 if (!s_cli.pin_set)
706 return true;
707 const mbedtls_x509_crt *peer = mbedtls_ssl_get_peer_cert(ssl);
708 if (!peer)
709 return false;
710 uint8_t hash[32];
711#if MBEDTLS_VERSION_MAJOR >= 3
712 int hret = mbedtls_sha256(peer->raw.p, peer->raw.len, hash, 0);
713#else
714 int hret = mbedtls_sha256_ret(peer->raw.p, peer->raw.len, hash, 0);
715#endif
716 return (hret == 0) && ct_eq32(hash, s_cli.pin);
717}
718
719#if DETWS_ENABLE_HTTP_CLIENT_TLS
720// Blocking client-side TLS exchange over caller-supplied BIO callbacks. Used by
721// the outbound HTTP client for https://. The transport (raw lwIP tcp_write + the
722// receive ring) lives in http_client.cpp; here we own only the mbedTLS client
723// session, served from the same static arena as the server side.
724//
725// Server authentication is OFF by default (the device has no trust store): the
726// transport is encrypted but the peer is unauthenticated unless a CA
727// (det_tls_client_set_ca) and/or a cert pin (det_tls_client_set_pin) is installed.
728int det_tls_client_run(const char *host, const uint8_t *req, size_t reqlen, uint8_t *out, size_t out_cap,
729 size_t *out_len, det_tls_bio_send_fn send_fn, det_tls_bio_recv_fn recv_fn, uint32_t deadline_ms)
730{
731 if (out_len)
732 *out_len = 0;
733 if (!req || !out || out_cap == 0 || !send_fn || !recv_fn)
734 return -1;
735
736 client_arena_ensure();
737
738 mbedtls_ssl_context ssl;
739 mbedtls_ssl_config conf;
740 mbedtls_ssl_init(&ssl);
741 mbedtls_ssl_config_init(&conf);
742
743 int rc = -1;
744 int ret;
745 do
746 {
747 if (client_conf_apply(&conf) != 0)
748 break;
749 if (mbedtls_ssl_setup(&ssl, &conf) != 0)
750 break;
751 if (host)
752 mbedtls_ssl_set_hostname(&ssl, host); // SNI (ignored if unsupported)
753 mbedtls_ssl_set_bio(&ssl, nullptr, send_fn, recv_fn, nullptr);
754
755 // Handshake (BIO callbacks yield WANT_READ/WANT_WRITE while data is pending).
756 while ((ret = mbedtls_ssl_handshake(&ssl)) != 0)
757 {
758 if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE)
759 break;
760 if ((int32_t)(deadline_ms - millis()) <= 0)
761 {
762 ret = -1;
763 break;
764 }
765 delay(5);
766 }
767 if (ret != 0)
768 {
769#ifdef DETWS_HTTP_CLIENT_DEBUG
770 printf("[tls] handshake ret=-0x%04x arena_peak=%u\n", (unsigned)(-ret), (unsigned)det_tls_arena_peak());
771#endif
772 break;
773 }
774
775 // Certificate pinning (mismatch or no peer cert aborts).
776 if (!client_pin_ok(&ssl))
777 {
778#ifdef DETWS_HTTP_CLIENT_DEBUG
779 printf("[tls] cert pin mismatch\n");
780#endif
781 ret = -1;
782 break;
783 }
784
785 // Send the (plaintext) request; mbedTLS encrypts and pushes via send_fn.
786 size_t sent = 0;
787 while (sent < reqlen)
788 {
789 ret = mbedtls_ssl_write(&ssl, req + sent, reqlen - sent);
790 if (ret > 0)
791 {
792 sent += (size_t)ret;
793 continue;
794 }
795 if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE)
796 break;
797 if ((int32_t)(deadline_ms - millis()) <= 0)
798 break;
799 delay(5);
800 }
801 if (sent < reqlen)
802 break;
803
804 // Read the decrypted response into out until the peer closes / buffer fills.
805 size_t total = 0;
806 while (total < out_cap)
807 {
808 ret = mbedtls_ssl_read(&ssl, out + total, out_cap - total);
809 if (ret > 0)
810 {
811 total += (size_t)ret;
812 continue;
813 }
814 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
815 {
816 if ((int32_t)(deadline_ms - millis()) <= 0)
817 break;
818 delay(5);
819 continue;
820 }
821 break; // 0 / PEER_CLOSE_NOTIFY / fatal -> response complete
822 }
823 if (out_len)
824 *out_len = total;
825 rc = (total > 0) ? 0 : -1;
826 } while (0);
827
828 mbedtls_ssl_close_notify(&ssl);
829 mbedtls_ssl_free(&ssl);
830 mbedtls_ssl_config_free(&conf);
831 return rc;
832}
833#endif // DETWS_ENABLE_HTTP_CLIENT_TLS
834
835// --- Persistent client session (csess): one long-lived outbound TLS connection
836// (e.g. MQTTS). Handshake once, then read/write application data over the
837// caller's BIO until det_tls_csess_end(). Honors the CA/pin trust config above. ---
838// Persistent client session (csess) state, owned by one instance (internal linkage): the
839// long-lived outbound TLS ssl/config + active flag, and (with resumption) the saved session
840// holding the server's ticket. One named owner, unreachable from any other translation unit.
841struct TlsCsessCtx
842{
843 mbedtls_ssl_context ssl;
844 mbedtls_ssl_config conf;
845 bool active = false;
846#if DETWS_ENABLE_TLS_RESUMPTION
847 mbedtls_ssl_session saved;
848 bool saved_valid = false;
849#endif
850};
851static TlsCsessCtx s_csess;
852
853#if DETWS_ENABLE_TLS_RESUMPTION
854// Session saved from the last successful csess handshake (holds the server's
855// ticket). Presented on the next begin() for an abbreviated handshake. Lives in
856// the static arena like every other mbedTLS object - no heap growth.
857
858void det_tls_csess_forget_session()
859{
860 if (s_csess.saved_valid)
861 mbedtls_ssl_session_free(&s_csess.saved);
862 s_csess.saved_valid = false;
863}
864#else
865void det_tls_csess_forget_session()
866{
867}
868#endif
869
870bool det_tls_csess_begin(const char *host, det_tls_bio_send_fn send_fn, det_tls_bio_recv_fn recv_fn)
871{
872 if (!send_fn || !recv_fn)
873 return false;
874 if (s_csess.active)
875 det_tls_csess_end();
876 client_arena_ensure();
877 mbedtls_ssl_init(&s_csess.ssl);
878 mbedtls_ssl_config_init(&s_csess.conf);
879 if (client_conf_apply(&s_csess.conf) != 0 || mbedtls_ssl_setup(&s_csess.ssl, &s_csess.conf) != 0)
880 {
881 mbedtls_ssl_free(&s_csess.ssl);
882 mbedtls_ssl_config_free(&s_csess.conf);
883 return false;
884 }
885 if (host)
886 mbedtls_ssl_set_hostname(&s_csess.ssl, host);
887#if DETWS_ENABLE_TLS_RESUMPTION
888 // Present the saved session (server ticket) so this handshake resumes if the
889 // server still honors it; a full handshake transparently replaces it below.
890 if (s_csess.saved_valid)
891 mbedtls_ssl_set_session(&s_csess.ssl, &s_csess.saved);
892#endif
893 mbedtls_ssl_set_bio(&s_csess.ssl, nullptr, send_fn, recv_fn, nullptr);
894 s_csess.active = true;
895 return true;
896}
897
898int det_tls_csess_handshake()
899{
900 if (!s_csess.active)
901 return -1;
902 int ret = mbedtls_ssl_handshake(&s_csess.ssl);
903 if (ret == 0)
904 {
905 if (!client_pin_ok(&s_csess.ssl)) // verify the pin once established
906 return -1;
907#if DETWS_ENABLE_TLS_RESUMPTION
908 // Capture the established session (incl. any new ticket) for next time.
909 det_tls_csess_forget_session();
910 mbedtls_ssl_session_init(&s_csess.saved);
911 s_csess.saved_valid = (mbedtls_ssl_get_session(&s_csess.ssl, &s_csess.saved) == 0);
912#endif
913 return 1;
914 }
915 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
916 return 0;
917 return -1;
918}
919
920int det_tls_csess_read(uint8_t *buf, size_t len)
921{
922 if (!s_csess.active)
923 return -1;
924 int ret = mbedtls_ssl_read(&s_csess.ssl, buf, len);
925 if (ret > 0)
926 return ret;
927 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
928 return 0; // no plaintext available yet
929 return -1; // close_notify / peer close / fatal
930}
931
932int det_tls_csess_write(const uint8_t *data, size_t len)
933{
934 if (!s_csess.active)
935 return -1;
936 size_t sent = 0;
937 uint16_t guard = 0;
938 while (sent < len)
939 {
940 int ret = mbedtls_ssl_write(&s_csess.ssl, data + sent, len - sent);
941 if (ret > 0)
942 {
943 sent += (size_t)ret;
944 guard = 0;
945 continue;
946 }
947 if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
948 {
949 if (++guard > 64)
950 break; // send buffer stuck; report a short write
951 delay(2);
952 continue;
953 }
954 return -1;
955 }
956 return (int)sent;
957}
958
959void det_tls_csess_end()
960{
961 if (!s_csess.active)
962 return;
963 mbedtls_ssl_close_notify(&s_csess.ssl);
964 mbedtls_ssl_free(&s_csess.ssl);
965 mbedtls_ssl_config_free(&s_csess.conf);
966 s_csess.active = false;
967}
968
969#endif // DETWS_ENABLE_CLIENT_TLS
970
971#endif // DETWS_ENABLE_TLS && ARDUINO
#define MAX_TLS_CONNS
Maximum simultaneous TLS connections (each holds mbedTLS record buffers).
#define DETWS_TLS_ARENA_SIZE
Bytes of the static BSS arena mbedTLS allocates from (DETWS_ENABLE_TLS).
#define DETWS_TLS_TICKET_LIFETIME_S
Session-ticket lifetime / key-rotation period in seconds (see DETWS_ENABLE_TLS_RESUMPTION).
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
Definition tcp.h:75
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:347
bool det_conn_raw_send(struct tcp_pcb *pcb, const void *data, u16_t len)
Write raw bytes straight to pcb (no TLS), context-safe.
Definition tcp.cpp:449
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).