Layer: L4 Transport ยท Build flags: PC_ENABLE_TLS, PC_ENABLE_MTLS
What this example teaches
Mutual TLS turns the handshake into two-way authentication: the server demands a client certificate and verifies it against a configured trust-anchor CA. A client that presents no certificate, or one not signed by that CA, is rejected before any HTTP is exchanged - strong transport-level client auth with no passwords. Inside a handler, tls_client_subject() gives you the verified peer's subject DN to identify or authorize the caller.
A different setup path than plain HTTPS. Where HTTPS used the one-shot begin_tls(), mTLS configures the engine in steps, then begin():
server.tls_cert(SERVER_CERT_PEM, ..., SERVER_KEY_PEM, ...);
server.tls_require_client_cert(CA_CERT_PEM, ...);
server.listen_tls(443);
int32_t result = server.begin();
Reading the verified identity. Once the handshake succeeds, the peer's certificate subject is available to handlers:
if (server.tls_client_subject(id, subject, sizeof(subject)) > 0)
server.send(id, 200, "text/plain", subject);
else
server.send(id, 200, "text/plain", "(no client certificate)");
#define PC_MTLS_SUBJECT_MAX
Maximum length of a verified mTLS peer subject DN string (incl. NUL).
A request with no client cert never reaches the handler - it fails in the handshake - so the /whoami route is reached only by verified peers.
Demo material warning. The CA, server, and client certs/keys in the sketch are throwaway demo material committed for the example - the keys are public. Generate your own in production. The README elides the PEM blocks (opaque base64); the full CA/server/client PEMs are in the .ino, including the client pair to split into client.crt / client.key.
Build and run
pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPC_ENABLE_TLS=1 -DPC_ENABLE_MTLS=1 -DMAX_CONNS=4 -DPC_TLS_ARENA_SIZE=32768" \
--lib="." examples/L4-Transport/mTLS/mTLS.ino
Extract the demo client PEM blocks from the .ino into client.crt/client.key, then:
curl -k --cert client.crt --key client.key https://<ip>/whoami # 200 + your DN
curl -k https://<ip>/whoami # handshake fails (no cert)
Annotated source
The complete sketch (mTLS.ino). The demo CA/server/client PEM blocks are elided here for brevity (see the .ino); the C++ is verbatim with comments.
#define PC_ENABLE_TLS 1
#define PC_ENABLE_MTLS 1
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
static const char SERVER_CERT_PEM[] = R"PEM(-----BEGIN CERTIFICATE-----
... demo server cert (full PEM in the .ino) ...
-----END CERTIFICATE-----
)PEM";
static const char SERVER_KEY_PEM[] = R"PEM(-----BEGIN EC PRIVATE KEY-----
... demo server key - PUBLIC (full PEM in the .ino) ...
-----END EC PRIVATE KEY-----
)PEM";
static const char CA_CERT_PEM[] = R"PEM(-----BEGIN CERTIFICATE-----
... demo CA cert (full PEM in the .ino) ...
-----END CERTIFICATE-----
)PEM";
void setup()
{
Serial.print("Connecting to WiFi");
{
delay(250);
Serial.print('.');
}
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
if (server.tls_client_subject(id, subject, sizeof(subject)) > 0)
server.
send(
id, 200,
"text/plain", subject);
else
server.
send(
id, 200,
"text/plain",
"(no client certificate)");
});
if (!server.tls_cert((const uint8_t *)SERVER_CERT_PEM, sizeof(SERVER_CERT_PEM), (const uint8_t *)SERVER_KEY_PEM,
sizeof(SERVER_KEY_PEM)))
{
Serial.println("TLS cert/key load failed");
return;
}
if (!server.tls_require_client_cert((const uint8_t *)CA_CERT_PEM, sizeof(CA_CERT_PEM)))
{
Serial.println("client CA load failed");
return;
}
server.listen_tls(443);
int32_t result = server.
begin();
if (result < 0)
Serial.printf("begin() failed (error %d)\n", result);
else
Serial.println("mTLS server on :443 (curl -k --cert client.crt --key client.key https://<ip>/whoami)");
}
void loop()
{
}
Single-port HTTP server with deterministic, zero-allocation execution.
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void handle()
Drive the server - call every Arduino loop() iteration.
bool init_wifi_physical(const char *, const char *)
Connect to a WiFi access point.
uint32_t pc_net_egress_ip(void)
IPv4 (network byte order) of the current egress interface, or 0 if none.
bool wifi_ready()
True if the WiFi station link is up (associated + an IP is assigned).
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
Layer 7 (Application) - public HTTP routing API.
@ HTTP_GET
Safe, idempotent read.
Fully-parsed HTTP/1.1 request.