DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dtls_conn.h
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 dtls_conn.h
6 * @brief DTLS 1.3 server handshake state machine (RFC 9147 §5-6).
7 *
8 * The transport-neutral core that drives one DTLS 1.3 server handshake: it consumes inbound
9 * datagrams and produces the outbound flight, wiring the reused TLS 1.3 message builders and key
10 * schedule (tls13_msg, tls13_kdf) through the DTLS record layer (dtls_record) and handshake framing
11 * (dtls_handshake). Like coap_server_process it has no sockets - the UDP glue (a later CoAPs
12 * front-end) feeds it datagrams and sends whatever it emits.
13 *
14 * Profile: the single spec-valid suite the whole hand-rolled TLS 1.3 stack uses -
15 * TLS_AES_128_GCM_SHA256, X25519 key exchange, an Ed25519 server certificate. The handshake is the
16 * one-round-trip full handshake (no PSK, no 0-RTT, no client auth):
17 *
18 * epoch 0 ClientHello ->
19 * <- ServerHello (epoch 0, DTLSPlaintext)
20 * epoch 2 <- EncryptedExtensions, Certificate, CertificateVerify, Finished (DTLSCiphertext)
21 * epoch 2 Finished ->
22 * epoch 3 application data (CoAP) protected with the app-traffic keys
23 *
24 * Each handshake message fits one record in this profile. When the client does not offer an X25519
25 * key_share up front, the server answers the first ClientHello with a HelloRetryRequest carrying a
26 * stateless, address-bound cookie and renegotiates the group to X25519 (RFC 9147 §5.1); the second
27 * ClientHello must echo the cookie before any asymmetric crypto is spent. Full ACK/timeout
28 * retransmission (§5.8, §7) beyond the Finished acknowledgement is a follow-on increment; the framing
29 * it needs already exists in dtls_handshake.
30 *
31 * @author Douglas Quigg (dstroy0)
32 * @date 2026
33 */
34
35#ifndef DETERMINISTICESPASYNCWEBSERVER_DTLS_CONN_H
36#define DETERMINISTICESPASYNCWEBSERVER_DTLS_CONN_H
37
38#include "ServerConfig.h"
39
40#if DETWS_ENABLE_DTLS
41
46#include <stddef.h>
47#include <stdint.h>
48
49/** @brief Largest inbound handshake message body reassembled (ClientHello / client Finished). */
50static constexpr size_t DTLS_CONN_REASM_CAP = 1024;
51
52/** @brief Largest single outbound handshake message (Certificate-dominated; one record per message
53 * in this phase, so the certificate plus framing must fit one record). */
54static constexpr size_t DTLS_CONN_MSG_CAP = 1024;
55
56/** @brief Largest serialized peer address the HelloRetryRequest cookie binds (IPv6 16 + port 2). */
57static constexpr size_t DTLS_PEER_ADDR_MAX = 18;
58
59/** @brief Length of the connection id the server chooses for itself (RFC 9146 / RFC 9147 §9): the id the
60 * client must place in every record it sends, so the server can route by it across an address
61 * change. 4 bytes is ample per-connection entropy; must be <= @ref DTLS_CID_MAX. */
62static constexpr size_t DTLS_CONN_LOCAL_CID_LEN = 4;
63
64/** @brief Most handshake messages in one outbound flight (ServerHello + EE + Cert + CV + Finished). */
65static constexpr size_t DTLS_FLIGHT_MSGS = 6;
66
67/** @brief Buffer for the current flight's DTLS handshake fragments, so it can be retransmitted with
68 * fresh record sequence numbers. Sized for the Certificate-dominated server flight. */
69static constexpr size_t DTLS_FLIGHT_CAP = DTLS_CONN_MSG_CAP + 512;
70
71/** @brief Retransmission timer (RFC 9147 §5.8.1): initial PTO, its cap, and the retransmission ceiling
72 * after which the handshake is abandoned. Times are in the units of @ref detws_millis (ms). */
73static constexpr uint32_t DTLS_PTO_INITIAL_MS = 1000u;
74static constexpr uint32_t DTLS_PTO_MAX_MS = 60000u;
75static constexpr uint8_t DTLS_MAX_RETRANSMITS = 8;
76
77/** @brief One buffered outbound handshake message: where its DTLS fragment sits in @ref DtlsConn.flight_buf
78 * and which epoch protects it. */
79struct DtlsFlightMsg
80{
81 uint16_t off; ///< byte offset of the fragment in flight_buf
82 uint16_t len; ///< fragment length
83 uint8_t epoch; ///< 0 (DTLSPlaintext) or 2 (DTLSCiphertext)
84};
85
86/** @brief Handshake progress. */
87enum class DtlsConnState : uint8_t
88{
89 START, ///< awaiting ClientHello
90 WAIT_FINISHED, ///< server flight sent; awaiting client Finished
91 DONE, ///< handshake complete; application keys installed
92 FAILED ///< fatal error (see @ref dtls_conn_alert)
93};
94
95/**
96 * @brief The server's long-lived identity plus this handshake's fresh randomness.
97 *
98 * @c cert_der / @c ed25519_seed are the server's certificate and matching signing key (long-lived).
99 * @c ephemeral_priv and @c server_random must be freshly generated per connection by the caller
100 * (from a CSPRNG); they are the X25519 ephemeral private key and the ServerHello random.
101 */
102struct DtlsServerConfig
103{
104 const uint8_t *cert_der; ///< Ed25519 leaf certificate, DER
105 size_t cert_len;
106 const uint8_t *ed25519_seed; ///< 32-byte Ed25519 signing seed (matches @c cert_der)
107 const uint8_t *ephemeral_priv; ///< 32-byte X25519 server ephemeral private key (fresh per handshake)
108 const uint8_t *server_random; ///< 32-byte ServerHello random (fresh per handshake)
109 const uint8_t *cookie_key; ///< 32-byte server-wide secret keying the HelloRetryRequest cookie MAC (§5.1)
110};
111
112/** @brief One DTLS 1.3 server handshake. Owns all per-connection state; no heap. */
113struct DtlsConn
114{
115 DtlsServerConfig cfg;
116 DtlsConnState state;
117 uint8_t alert; ///< RFC 8446 §6 alert code when @c state is FAILED (0 otherwise)
118
119 SshSha256Ctx transcript; ///< running Transcript-Hash over the TLS handshake messages
120 Tls13KeySchedule ks; ///< TLS 1.3 key schedule
121 DtlsRecordKeys ep2_srv; ///< epoch 2 server write keys (handshake traffic)
122 DtlsRecordKeys ep2_cli; ///< epoch 2 client read keys
123 DtlsRecordKeys ep3_srv; ///< epoch 3 server write keys (application traffic)
124 DtlsRecordKeys ep3_cli; ///< epoch 3 client read keys
125 bool ep2_ready; ///< epoch 2 keys installed
126 bool ep3_ready; ///< epoch 3 keys installed
127 uint8_t hs_finished_hash[SSH_SHA256_DIGEST_LEN]; ///< Transcript-Hash(CH..server Finished)
128
129 uint64_t tx_seq_ep0; ///< next outbound record sequence number, epoch 0
130 uint64_t tx_seq_ep2; ///< next outbound record sequence number, epoch 2
131 uint64_t tx_seq_ep3; ///< next outbound record sequence number, epoch 3
132 uint16_t tx_msg_seq; ///< next outbound handshake message_seq (advances across an optional HRR)
133 bool hrr_sent; ///< a HelloRetryRequest was sent; the next ClientHello is the retry (§5.1)
134 uint16_t next_recv_msg_seq; ///< handshake message_seq expected next from the client
135 DtlsReplayWindow replay_ep2; ///< anti-replay window for inbound epoch-2 records
136 DtlsReplayWindow replay_ep3; ///< anti-replay window for inbound epoch-3 (application) records
137 uint64_t rx_ep2_seq; ///< sequence number of the last inbound epoch-2 record (the client Finished)
138 bool hs_ack_sent; ///< the client Finished has been acknowledged (RFC 9147 §5.8.3 / §7)
139 uint8_t peer_addr[DTLS_PEER_ADDR_MAX]; ///< serialized peer address the HRR cookie is bound to (§5.1)
140 uint8_t peer_addr_len; ///< bytes of @ref peer_addr in use (0 = no address bound)
141
142 // Connection ids (RFC 9146 / RFC 9147 §9), negotiated by the connection_id extension.
143 bool cid_negotiated; ///< the client offered connection_id and we accepted it
144 uint8_t peer_cid[DTLS_CID_MAX]; ///< the client's CID: placed in every record we send to the client (may be empty)
145 uint8_t peer_cid_len; ///< bytes of @ref peer_cid in use
146 uint8_t local_cid[DTLS_CID_MAX]; ///< the CID we chose: the client places it in every record it sends to us
147 uint8_t local_cid_len; ///< bytes of @ref local_cid in use
148
149 // Retransmission (RFC 9147 §5.8): the current outbound flight, buffered as fragments so it can be
150 // re-sent with fresh record sequence numbers, plus the exponential-backoff timer state.
151 DtlsFlightMsg flight_msgs[DTLS_FLIGHT_MSGS]; ///< the flight's messages (index into @ref flight_buf)
152 DtlsRecordNumber flight_rec[DTLS_FLIGHT_MSGS]; ///< record numbers of each message's last transmission (for ACKs)
153 uint8_t flight_count; ///< messages in the current flight
154 uint16_t flight_len; ///< bytes used in @ref flight_buf
155 bool awaiting_reply; ///< a flight is outstanding and a peer reply is expected (timer runs)
156 uint8_t retransmits; ///< times the current flight has been retransmitted
157 uint32_t pto_ms; ///< current retransmission timeout (doubles each retransmit)
158 uint32_t flight_sent_ms; ///< detws_millis() when the flight was last (re)transmitted
159
160 DtlsHsReasm reasm; ///< inbound handshake reassembler
161 uint8_t reasm_buf[4 + DTLS_CONN_REASM_CAP]; ///< TLS message = 4-byte header [0..3] + body [4..]
162 uint8_t msgbuf[DTLS_CONN_MSG_CAP]; ///< scratch for one outbound TLS message
163 uint8_t flight_buf[DTLS_FLIGHT_CAP]; ///< the current flight's DTLS handshake fragments, for retransmission
164};
165
166/**
167 * @brief Initialize a connection for a new handshake. @p cfg is copied (its pointers must outlive @p c).
168 *
169 * @param peer_addr serialized peer address (e.g. IP || port) the HelloRetryRequest cookie binds,
170 * so a cookie minted for one peer is worthless to another (RFC 9147 §5.1). May
171 * be NULL / 0 when no HRR is expected (the one-round-trip happy path).
172 * @param peer_addr_len length of @p peer_addr; clamped to @ref DTLS_PEER_ADDR_MAX.
173 */
174void dtls_conn_init(DtlsConn *c, const DtlsServerConfig *cfg, const uint8_t *peer_addr, size_t peer_addr_len);
175
176/**
177 * @brief Feed one inbound datagram; append any response records to @p out.
178 *
179 * Demultiplexes the datagram's records (DTLSPlaintext for epoch 0, DTLSCiphertext for epoch 2),
180 * unprotects and reassembles the handshake, and drives the state machine, writing the server's
181 * response flight to @p out.
182 *
183 * @return the number of bytes written to @p out (0 if nothing to send), or -1 on a fatal error
184 * (then @c state is FAILED and @ref dtls_conn_alert gives the reason).
185 */
186int dtls_conn_process(DtlsConn *c, const uint8_t *dgram, size_t len, uint8_t *out, size_t out_cap);
187
188/**
189 * @brief Milliseconds (in @ref detws_millis units) until the retransmission timer fires, or -1 if no
190 * timer is running (no flight is outstanding, or the handshake is done or failed).
191 *
192 * The transport-neutral core keeps no timer of its own; the caller polls this to schedule a wake-up and
193 * calls @ref dtls_conn_on_timeout when it expires (RFC 9147 §5.8). 0 means the timer is already due.
194 */
195int dtls_conn_timeout_ms(const DtlsConn *c);
196
197/**
198 * @brief Fire the retransmission timer: re-send the outstanding flight into @p out with fresh record
199 * sequence numbers and double the timeout (RFC 9147 §5.8.1 exponential backoff).
200 *
201 * Retransmits only if the PTO has actually elapsed, so a spurious/early call is a no-op (returns 0).
202 * After @ref DTLS_MAX_RETRANSMITS attempts the handshake is abandoned (state becomes FAILED, returns -1).
203 *
204 * @return bytes written to @p out (0 if nothing was due), or -1 if the retransmission ceiling was hit.
205 */
206int dtls_conn_on_timeout(DtlsConn *c, uint8_t *out, size_t out_cap);
207
208/** @brief True once the handshake has completed and the application-traffic keys are installed. */
209bool dtls_conn_established(const DtlsConn *c);
210
211/** @brief The alert code (RFC 8446 §6) set when the handshake failed, or 0. */
212uint8_t dtls_conn_alert(const DtlsConn *c);
213
214/** @brief Application-epoch (epoch 3) server write keys - protect outbound application records. */
215const DtlsRecordKeys *dtls_conn_app_write_keys(const DtlsConn *c);
216
217/** @brief Application-epoch (epoch 3) client read keys - unprotect inbound application records. */
218const DtlsRecordKeys *dtls_conn_app_read_keys(const DtlsConn *c);
219
220/**
221 * @brief The server's connection id (RFC 9146 / RFC 9147 §9) for this connection, if one was negotiated.
222 *
223 * The client places this id in every record it sends, so a UDP front-end can route inbound records by it
224 * and follow the peer across an address change (NAT rebinding). Copies the id to @p out (which must hold
225 * at least @ref DTLS_CID_MAX bytes).
226 *
227 * @return the connection-id length, or 0 if no connection id was negotiated.
228 */
229size_t dtls_conn_local_cid(const DtlsConn *c, uint8_t *out);
230
231/**
232 * @brief Decrypt one inbound epoch-3 application record into @p out (RFC 9147 §4).
233 *
234 * Only valid once established. Enforces the epoch-3 anti-replay window (§4.5.1) and that the record's
235 * true content type is application_data. A replayed, too-old, or non-application record yields false.
236 *
237 * @return true and sets @p *out_len on success; false if the connection is not established, the record
238 * fails to open, it is a replay, or it is not application data.
239 */
240bool dtls_conn_open_app(DtlsConn *c, const uint8_t *rec, size_t rec_len, uint8_t *out, size_t out_cap, size_t *out_len);
241
242/**
243 * @brief Seal @p data as one outbound epoch-3 application record (RFC 9147 §4), advancing the shared
244 * epoch-3 send sequence so it never collides with the handshake-completion ACK.
245 * @return record bytes written to @p out, or 0 if not established or on overflow.
246 */
247size_t dtls_conn_seal_app(DtlsConn *c, const uint8_t *data, size_t len, uint8_t *out, size_t out_cap);
248
249#endif // DETWS_ENABLE_DTLS
250#endif // DETERMINISTICESPASYNCWEBSERVER_DTLS_CONN_H
User-facing configuration for DeterministicESPAsyncWebServer.
DTLS 1.3 handshake framing and reliability (RFC 9147 §5, §7).
DTLS 1.3 record layer (RFC 9147 §4).
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define SSH_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition ssh_sha256.h:29
Streaming SHA-256 context.
Definition ssh_sha256.h:44
TLS 1.3 key schedule (RFC 8446 sec 7.1) for the QUIC handshake.