Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 42 of 56

TLS 1.3, in Ada, with no C library

A complete client handshake — ECDHE, AEAD, certificate chain validation to a pinned root, and session resumption — with every line of crypto in Ada or the chip's own accelerators.

What it actually does

The headline claim is easy to under-read, so here is the pipeline the in-tree esp32s3_tls_weather example runs end to end against a live public server:

DNS -> TCP connect :443 -> TLS 1.3 handshake
   (X25519 ECDHE, AES-128-GCM, HKDF, RSA-PSS CertificateVerify, Finished)
-> validate the server's chain to a PINNED root (ISRG Root X1)
-> encrypted HTTP GET -> decrypt and parse the reply

No external C TLS library is involved. The crypto is Ada — SPARKNaCl plus this repository's own P-256/P-384 — over the chip's SHA and AES accelerators.

The client surface

type Session is limited private;

procedure Hello (...);                                --  ClientHello / ServerHello
function  Keys_Ready            (S : Session) return Boolean;
function  Have_Server_Cert      (S : Session) return Boolean;
function  Server_Cert_Verify_OK (S : Session) return Boolean;
function  Server_Finished_OK    (S : Session) return Boolean;
function  Ready                 (S : Session) return Boolean;

procedure Send (S : in out Session; Sock : GNAT.Sockets.Socket_Type; Data : Byte_Array);
procedure Recv (...);

function  Has_Ticket          (S : Session) return Boolean;
function  Server_Accepted_PSK (S : Session) return Boolean;
procedure Resume (...);

The handshake state is inspectable rather than a single opaque boolean, which matters when a connection fails: you can tell "the certificate chain was rejected" from "the server never finished" from "we never got keys at all".

Hello runs the whole handshake, not just the opening exchange — ClientHello, ServerHello, the key schedule, the server's encrypted flight (EncryptedExtensions, Certificate, CertificateVerify, Finished) and our Finished. When it returns, Ready reports the application channel is open.

A detail worth copying if you ever write a client: the ClientHello sends a key_share entry for both x25519 and P-256, not just the preferred one. Offering only one group costs a HelloRetryRequest round trip whenever the server prefers the other. Both paths are hardware-verified — a P-256-only ClientHello completes a handshake just as an x25519 one does.

Chain validation is the hard part

function Validate (Chain, Anchors : Cert_List; Host : String;
                   Now : X509.Time_64) return Result;

A handshake that completes proves you are talking to somebody. What makes it TLS is Chain_Verify, which puts the pieces together:

The Now parameter is the detail worth internalising: without a real clock, certificate expiry cannot be checked, and a chain that expired years ago validates happily. On a board that means fetching the time before you can meaningfully verify anything — which is why NTP comes first in the pipeline above.

The elliptic curves

function Verify     (Pub_X, Pub_Y, Hash, R, S : Bytes_32) return Boolean;
function Public_Key (Priv : Bytes_32; Pub_X, Pub_Y : out Bytes_32) return Boolean;
function ECDH       (...);
function Sign       (Priv, Hash : Bytes_32; R, S : out Bytes_32) return Boolean;

P-256 and P-384 are pure Ada with no chip dependency. Two implementation choices are worth noting because they are the right ones:

Examples: esp32s3_tls_hello (handshake), esp32s3_tls_weather (real-world HTTPS), esp32s3_tls_resume (session resumption), plus esp32s3_wifi_tls over the radio.