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