ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dtls_conn.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file protocore_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 (protocore_tls13_msg, protocore_tls13_kdf) through the DTLS record layer (protocore_dtls_record) and
11 * handshake framing (protocore_dtls_handshake). Like Coap.process it has no sockets - the UDP glue (a
12 * later CoAPs 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 protocore_dtls_handshake.
30 *
31 * @author Douglas Quigg (dstroy0)
32 * @date 2026
33 */
34
35#ifndef PROTOCORE_DTLS_CONN_H
36#define PROTOCORE_DTLS_CONN_H
37
38#include "protocore_config.h" // the entry point: protocore_types.h for the widths
39
40#if PROTOCORE_ENABLE_DTLS
41
42// DtlsConn embeds these by value: the reassembler and record numbers, the epoch key sets and the
43// replay windows, and the key schedule the handshake runs through.
47
49
50// This module holds nothing between calls, so it carves no borrow and states none. An entry
51// takes one all the same, and never reads it, so every namespace in the tree is invoked the
52// same way.
53
54/** @brief Largest inbound handshake message body reassembled (ClientHello / client Finished). */
55#define PROTOCORE_DTLS_CONN_REASM_CAP 1024
56
57/** @brief Largest single outbound handshake message (Certificate-dominated; one record per message
58 * in this phase, so the certificate plus framing must fit one record). */
59#define PROTOCORE_DTLS_CONN_MSG_CAP 1024
60
61/** @brief Largest serialized peer address the HelloRetryRequest cookie binds (IPv6 16 + port 2). */
62#define PROTOCORE_DTLS_PEER_ADDR_MAX 18
63
64/** @brief Length of the connection id the server chooses for itself (RFC 9146 / RFC 9147 §9): the id the
65 * client must place in every record it sends, so the server can route by it across an address
66 * change. 4 bytes is ample per-connection entropy; must be <= @ref PROTOCORE_DTLS_CID_MAX. */
67#define PROTOCORE_DTLS_CONN_LOCAL_CID_LEN 4
68
69/** @brief Most handshake fragments in one outbound flight. A message longer than the connection's
70 * PMTU becomes several, so this is a fragment count, not a message count (§4.3, §5.5). */
71#define PROTOCORE_DTLS_FLIGHT_MSGS 16
72
73/** @brief Most a record adds around a handshake fragment: the unified header carrying a connection
74 * id, a 16-bit sequence and a length, plus the AEAD tag (§4). The plaintext form is smaller,
75 * so this bounds both. */
76#define PROTOCORE_DTLS_REC_OVERHEAD_MAX (1 + PROTOCORE_DTLS_CID_MAX + 2 + 2 + PROTOCORE_DTLS_TAG_LEN)
77
78/** @brief Buffer for the current flight's DTLS handshake fragments, so it can be retransmitted with
79 * fresh record sequence numbers. Sized for the Certificate-dominated server flight. */
80#define PROTOCORE_DTLS_FLIGHT_CAP (PROTOCORE_DTLS_CONN_MSG_CAP + 512)
81
82/** @brief Retransmission timer (RFC 9147 §5.8.1): initial PTO, its cap, and the retransmission ceiling
83 * after which the handshake is abandoned. Times are in the units of @ref protocore_millis (ms). */
84#define PROTOCORE_DTLS_PTO_INITIAL_MS 1000u
85#define PROTOCORE_DTLS_PTO_MAX_MS 60000u
86#define PROTOCORE_DTLS_MAX_RETRANSMITS 8
87
88/** @brief One buffered outbound handshake message: where its DTLS fragment sits in @ref DtlsConn.flight_buf
89 * and which epoch protects it. */
90typedef struct
91{
92 uint16_t off; ///< byte offset of the fragment in flight_buf
93 uint16_t len; ///< fragment length
94 uint8_t epoch; ///< 0 (DTLSPlaintext) or 2 (DTLSCiphertext)
95} DtlsFlightMsg;
96
97/** @brief Handshake progress. */
98typedef enum PROTO_ENUM_PACKED
99{
100 DTLS_CONN_STATE_START, ///< awaiting ClientHello
101 DTLS_CONN_STATE_WAIT_FINISHED, ///< server flight sent; awaiting client Finished
102 DTLS_CONN_STATE_DONE, ///< handshake complete; application keys installed
103 DTLS_CONN_STATE_FAILED ///< fatal error (see @ref protocore_dtls_conn_alert)
104} DtlsConnState;
105
106/**
107 * @brief The server's long-lived identity plus this handshake's fresh randomness.
108 *
109 * @c cert_der / @c ed25519_seed are the server's certificate and matching signing key (long-lived).
110 * @c ephemeral_priv and @c server_random must be freshly generated per connection by the caller
111 * (from a CSPRNG); they are the X25519 ephemeral private key and the ServerHello random.
112 */
113typedef struct
114{
115 const uint8_t *cert_der; ///< Ed25519 leaf certificate, DER
116 size_t cert_len;
117 const uint8_t *ed25519_seed; ///< 32-byte Ed25519 signing seed (matches @c cert_der)
118 const uint8_t *ephemeral_priv; ///< 32-byte X25519 server ephemeral private key (fresh per handshake)
119 const uint8_t *server_random; ///< 32-byte ServerHello random (fresh per handshake)
120 const uint8_t *cookie_key; ///< 32-byte server-wide secret keying the HelloRetryRequest cookie MAC (§5.1)
121 uint16_t pmtu; ///< largest datagram this path takes; 0 uses PROTOCORE_DTLS_PMTU_DEFAULT (§4.3)
122} DtlsServerConfig;
123
124/** @brief One DTLS 1.3 server handshake. Owns all per-connection state; no heap. */
125typedef struct
126{
127 DtlsServerConfig cfg;
128 DtlsConnState state;
129 uint8_t alert; ///< RFC 8446 §6 alert code when @c state is FAILED (0 otherwise)
130
131 uint8_t *transcript; ///< running Transcript-Hash over the TLS handshake messages
132 Tls13KeySchedule ks; ///< TLS 1.3 key schedule, over @ref ks_store
133 uint8_t ks_store[PROTOCORE_TLS13_KS_BORROW]; ///< the schedule's terms and its HKDF's bytes
134 // The transcript hash and the one-off hashes taken beside it work out of these. Live and die with
135 // this connection, so no hash on the handshake path touches a pool.
136 uint8_t hash_work[PROTOCORE_TLS13_TRANSCRIPT_BORROW];
137 uint8_t hash_work2[PROTOCORE_TLS13_TRANSCRIPT_BORROW];
138 uint8_t sign_work[PROTOCORE_SHA512_BORROW]; ///< the CertificateVerify signature's SHA-512
139 uint8_t mac_work[PROTOCORE_HMAC_SHA256_BORROW]; ///< the stateless HelloRetryRequest cookie's MAC
140 DtlsRecordKeys ep2_srv; ///< epoch 2 server write keys (handshake traffic)
141 DtlsRecordKeys ep2_cli; ///< epoch 2 client read keys
142 DtlsRecordKeys ep3_srv; ///< epoch 3 server write keys (application traffic)
143 DtlsRecordKeys ep3_cli; ///< epoch 3 client read keys
144 proto_bool ep2_ready; ///< epoch 2 keys installed
145 proto_bool ep3_ready; ///< epoch 3 keys installed
146 uint8_t hs_finished_hash[TLS13_SECRET_MAX]; ///< Transcript-Hash(CH..server Finished)
147
148 uint64_t tx_seq_ep0; ///< next outbound record sequence number, epoch 0
149 uint64_t tx_seq_ep2; ///< next outbound record sequence number, epoch 2
150 uint64_t tx_seq_ep3; ///< next outbound record sequence number, epoch 3
151 uint16_t tx_msg_seq; ///< next outbound handshake message_seq (advances across an optional HRR)
152 proto_bool hrr_sent; ///< a HelloRetryRequest was sent; the next ClientHello is the retry (§5.1)
153 uint16_t next_recv_msg_seq; ///< handshake message_seq expected next from the client
154 DtlsReplayWindow replay_ep2; ///< anti-replay window for inbound epoch-2 records
155 DtlsReplayWindow replay_ep3; ///< anti-replay window for inbound epoch-3 (application) records
156 uint64_t rx_ep2_seq; ///< sequence number of the last inbound epoch-2 record (the client Finished)
157 proto_bool hs_ack_sent; ///< the client Finished has been acknowledged (RFC 9147 §5.8.3 / §7)
158 uint8_t peer_addr[PROTOCORE_DTLS_PEER_ADDR_MAX]; ///< serialized peer address the HRR cookie is bound to (§5.1)
159 uint8_t peer_addr_len; ///< bytes of @ref peer_addr in use (0 = no address bound)
160
161 // Connection ids (RFC 9146 / RFC 9147 §9), negotiated by the connection_id extension.
162 proto_bool cid_negotiated; ///< the client offered connection_id and we accepted it
163 uint8_t peer_cid[PROTOCORE_DTLS_CID_MAX]; ///< the client's CID: placed in every record we send to the client (may
164 ///< be empty)
165 uint8_t peer_cid_len; ///< bytes of @ref peer_cid in use
166 uint8_t
167 local_cid[PROTOCORE_DTLS_CID_MAX]; ///< the CID we chose: the client places it in every record it sends to us
168 uint8_t local_cid_len; ///< bytes of @ref local_cid in use
169
170 // Retransmission (RFC 9147 §5.8): the current outbound flight, buffered as fragments so it can be
171 // re-sent with fresh record sequence numbers, plus the exponential-backoff timer state.
172 DtlsFlightMsg flight_msgs[PROTOCORE_DTLS_FLIGHT_MSGS]; ///< the flight's messages (index into @ref flight_buf)
174 flight_rec[PROTOCORE_DTLS_FLIGHT_MSGS]; ///< record numbers of each message's last transmission (for ACKs)
175 uint8_t flight_count; ///< messages in the current flight
176 uint16_t flight_len; ///< bytes used in @ref flight_buf
177 proto_bool awaiting_reply; ///< a flight is outstanding and a peer reply is expected (timer runs)
178 uint8_t retransmits; ///< times the current flight has been retransmitted
179 uint16_t pmtu; ///< largest datagram this connection puts on the wire (sec 4.3)
180 uint32_t pto_ms; ///< current retransmission timeout (doubles each retransmit)
181 uint32_t flight_sent_ms; ///< protocore_millis() when the flight was last (re)transmitted
182
183 DtlsHsReasm reasm; ///< inbound handshake reassembler
184 uint8_t reasm_buf[4 + PROTOCORE_DTLS_CONN_REASM_CAP]; ///< TLS message = 4-byte header [0..3] + body [4..]
185 uint8_t msgbuf[PROTOCORE_DTLS_CONN_MSG_CAP]; ///< scratch for one outbound TLS message
186 uint8_t
187 flight_buf[PROTOCORE_DTLS_FLIGHT_CAP]; ///< the current flight's DTLS handshake fragments, for retransmission
188} DtlsConn;
189
190/** @brief What init takes: c, cfg, peer_addr, peer_addr_len. */
191typedef struct
192{
193 DtlsConn *c;
194 const DtlsServerConfig *cfg;
195 const uint8_t *peer_addr;
196 size_t peer_addr_len;
197} DtlsServerInitArgs;
198
199/** @brief What process takes: c, dgram, len, out, out_cap. */
200typedef struct
201{
202 DtlsConn *c;
203 const uint8_t *dgram;
204 size_t len;
205 uint8_t *out;
206 size_t out_cap;
207} DtlsServerProcessArgs;
208
209/** @brief What timeout_ms takes: c. */
210typedef struct
211{
212 const DtlsConn *c;
213} DtlsServerTimeoutMsArgs;
214
215/** @brief What on_timeout takes: c, out, out_cap. */
216typedef struct
217{
218 DtlsConn *c;
219 uint8_t *out;
220 size_t out_cap;
221} DtlsServerOnTimeoutArgs;
222
223/** @brief What established takes: c. */
224typedef struct
225{
226 const DtlsConn *c;
227} DtlsServerEstablishedArgs;
228
229/** @brief What alert takes: c. */
230typedef struct
231{
232 const DtlsConn *c;
233} DtlsServerAlertArgs;
234
235/** @brief What app_write_keys takes: c. */
236typedef struct
237{
238 DtlsConn *c;
239} DtlsServerAppWriteKeysArgs;
240
241/** @brief What app_read_keys takes: c. */
242typedef struct
243{
244 DtlsConn *c;
245} DtlsServerAppReadKeysArgs;
246
247/** @brief What local_cid takes: c, out. */
248typedef struct
249{
250 const DtlsConn *c;
251 uint8_t *out;
252} DtlsServerLocalCidArgs;
253
254/** @brief What open_app takes: c, rec, rec_len, out, out_cap, out_len. */
255typedef struct
256{
257 DtlsConn *c;
258 const uint8_t *rec;
259 size_t rec_len;
260 uint8_t *out;
261 size_t out_cap;
262 size_t *out_len;
263} DtlsServerOpenAppArgs;
264
265/** @brief What seal_app takes: c, data, len, out, out_cap. */
266typedef struct
267{
268 DtlsConn *c;
269 const uint8_t *data;
270 size_t len;
271 uint8_t *out;
272 size_t out_cap;
273} DtlsServerSealAppArgs;
274
275/**
276 * @brief DTLS 1.3 server handshake state machine (RFC 9147 §5-6).
277 *
278 * A caller sets the members a call takes, invokes it through ::DtlsServer with the bytes it runs
279 * out of, and reads the outcome off the same handle.
280 *
281 * DtlsServer.init_args.c = ...;
282 * DtlsServer.init_args.cfg = ...;
283 * DtlsServer.init_args.peer_addr = ...;
284 * DtlsServer.init_args.peer_addr_len = ...;
285 * DtlsServer.init(work);
286 *
287 * @var DtlsConnNs::init_args what init takes: c, cfg, peer_addr, peer_addr_len
288 * @var DtlsConnNs::process_args what process takes: c, dgram, len, out, out_cap
289 * @var DtlsConnNs::timeout_ms_args what timeout_ms takes: c
290 * @var DtlsConnNs::on_timeout_args what on_timeout takes: c, out, out_cap
291 * @var DtlsConnNs::established_args what established takes: c
292 * @var DtlsConnNs::alert_args what alert takes: c
293 * @var DtlsConnNs::app_write_keys_args what app_write_keys takes: c
294 * @var DtlsConnNs::app_read_keys_args what app_read_keys takes: c
295 * @var DtlsConnNs::local_cid_args what local_cid takes: c, out
296 * @var DtlsConnNs::open_app_args what open_app takes: c, rec, rec_len, out, out_cap, out_len
297 * @var DtlsConnNs::seal_app_args what seal_app takes: c, data, len, out, out_cap
298 * @var DtlsConnNs::ok a call's true/false outcome
299 * @var DtlsConnNs::n the count a call reports
300 * @var DtlsConnNs::value the value a call reports
301 * @var DtlsConnNs::ptr the pointer a call reports
302 * @var DtlsConnNs::init bind a connection to its configuration and peer address
303 * @var DtlsConnNs::process turn one received datagram, writing whatever it owes back into out
304 * @var DtlsConnNs::timeout_ms milliseconds until the flight needs retransmitting
305 * @var DtlsConnNs::on_timeout retransmit the current flight once that timeout has passed
306 * @var DtlsConnNs::established whether the handshake has completed
307 * @var DtlsConnNs::alert the alert that ended the connection, or 0
308 * @var DtlsConnNs::app_write_keys the application-epoch write keys
309 * @var DtlsConnNs::app_read_keys the application-epoch read keys
310 * @var DtlsConnNs::local_cid this side's connection id, and its length
311 * @var DtlsConnNs::open_app open one application-data record once the handshake is done
312 * @var DtlsConnNs::seal_app seal application data into one record
313 *
314 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
315 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
316 * a caller drives every namespace the same way.
317 */
318typedef struct
319{
320 DtlsServerInitArgs init_args;
321 DtlsServerProcessArgs process_args;
322 DtlsServerTimeoutMsArgs timeout_ms_args;
323 DtlsServerOnTimeoutArgs on_timeout_args;
324 DtlsServerEstablishedArgs established_args;
325 DtlsServerAlertArgs alert_args;
326 DtlsServerAppWriteKeysArgs app_write_keys_args;
327 DtlsServerAppReadKeysArgs app_read_keys_args;
328 DtlsServerLocalCidArgs local_cid_args;
329 DtlsServerOpenAppArgs open_app_args;
330 DtlsServerSealAppArgs seal_app_args;
331 proto_bool ok;
332 int n;
333 uint8_t value;
334 DtlsRecordKeys *ptr;
335} DtlsServerVars;
336
337/** @brief The operands and the outcome. */
338extern DtlsServerVars DtlsServerV;
339
340/** @brief The entries. */
341typedef struct
342{
343 void (*const init)(uint8_t *work);
344 void (*const process)(uint8_t *work);
345 void (*const timeout_ms)(uint8_t *work);
346 void (*const on_timeout)(uint8_t *work);
347 void (*const established)(uint8_t *work);
348 void (*const alert)(uint8_t *work);
349 void (*const app_write_keys)(uint8_t *work);
350 void (*const app_read_keys)(uint8_t *work);
351 void (*const local_cid)(uint8_t *work);
352 void (*const open_app)(uint8_t *work);
353 void (*const seal_app)(uint8_t *work);
354} DtlsConnNs;
355
356// What the table binds, defined once in the .c and taking one parameter each: everything
357// else an entry needs is an operand in DtlsServerV or a region of the borrow at a fixed offset.
358void protocore_dtls_server_init(uint8_t *work);
359void protocore_dtls_server_process(uint8_t *work);
360void protocore_dtls_server_timeout_ms(uint8_t *work);
361void protocore_dtls_server_on_timeout(uint8_t *work);
362void protocore_dtls_server_established(uint8_t *work);
363void protocore_dtls_server_alert(uint8_t *work);
364void protocore_dtls_server_app_write_keys(uint8_t *work);
365void protocore_dtls_server_app_read_keys(uint8_t *work);
366void protocore_dtls_server_local_cid(uint8_t *work);
367void protocore_dtls_server_open_app(uint8_t *work);
368void protocore_dtls_server_seal_app(uint8_t *work);
369
370// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
371// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
372// `DtlsServer.init(work)` resolves to a named function and becomes a DIRECT call. An extern table
373// leaves the call indirect and the symbol live at every level, -O2 -flto included.
374static const DtlsConnNs DtlsServer __attribute__((unused)) = {
375 .init = protocore_dtls_server_init,
376 .process = protocore_dtls_server_process,
377 .timeout_ms = protocore_dtls_server_timeout_ms,
378 .on_timeout = protocore_dtls_server_on_timeout,
379 .established = protocore_dtls_server_established,
380 .alert = protocore_dtls_server_alert,
381 .app_write_keys = protocore_dtls_server_app_write_keys,
382 .app_read_keys = protocore_dtls_server_app_read_keys,
383 .local_cid = protocore_dtls_server_local_cid,
384 .open_app = protocore_dtls_server_open_app,
385 .seal_app = protocore_dtls_server_seal_app,
386};
387
389
390#endif // PROTOCORE_ENABLE_DTLS
391
392#endif // PROTOCORE_DTLS_CONN_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
DTLS 1.3 handshake framing and reliability (RFC 9147 §5, §7).
DTLS 1.3 record layer (RFC 9147 §4).
#define PROTOCORE_DTLS_CID_MAX
Largest connection id carried in a DTLSCiphertext header (RFC 9146 / RFC 9147 §9)....
Definition dtls_record.h:69
#define PROTOCORE_SHA512_BORROW
#define PROTOCORE_TLS13_KS_BORROW
#define PROTOCORE_HMAC_SHA256_BORROW
#define PROTOCORE_TLS13_TRANSCRIPT_BORROW
TLS 1.3 key schedule (RFC 8446 sec 7.1) for the QUIC handshake.
Reassembles the fragments of a single handshake message into a contiguous body.
One direction's record-protection keys for one epoch (RFC 9147 §4).
Definition dtls_record.h:84
A record identified for acknowledgement: (epoch, sequence_number), 8 bytes each on the wire (RFC 9147...
64-record sliding replay window over the highest sequence number accepted in an epoch.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97