DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dtls_conn.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 dtls_conn.cpp
6 * @brief DTLS 1.3 server handshake state machine (RFC 9147 §5-6). See dtls_conn.h.
7 */
8
10
11#if DETWS_ENABLE_DTLS
12
15#include "services/clock.h" // detws_millis() stamps / checks the HelloRetryRequest cookie freshness
16#include <string.h>
17
18namespace
19{
20// TLS alert codes used here (RFC 8446 §6).
21const uint8_t ALERT_UNEXPECTED_MESSAGE = 10;
22const uint8_t ALERT_HANDSHAKE_FAILURE = 40;
23const uint8_t ALERT_DECODE_ERROR = 50;
24const uint8_t ALERT_DECRYPT_ERROR = 51;
25const uint8_t ALERT_PROTOCOL_VERSION = 70;
26const uint8_t ALERT_INTERNAL_ERROR = 80;
27
28// HelloRetryRequest cookie freshness window: the client must echo the cookie within this many
29// milliseconds of it being minted (RFC 9147 §5.1). detws_millis() supplies both timestamps.
30const uint64_t DTLS_HRR_COOKIE_MAX_AGE_MS = 60000;
31
32// The record-layer demux (RFC 9147 §4): a first byte 0b001xxxxx is a DTLSCiphertext unified header.
33bool is_ciphertext(uint8_t b0)
34{
35 return (b0 & 0xE0) == 0x20;
36}
37
38// On-wire length of a DTLSCiphertext record from its (plaintext) header, so a datagram carrying more
39// than one record can be walked. Mirrors the header flags dtls_ciphertext_protect writes. @p cid_len is
40// our negotiated connection id length (the CID is not length-prefixed on the wire, RFC 9146), 0 if none.
41size_t ciphertext_record_len(const uint8_t *rec, size_t avail, size_t cid_len)
42{
43 if (avail < 1)
44 return 0;
45 uint8_t b0 = rec[0];
46 size_t off = 1;
47 if (b0 & 0x10) // C bit: connection id present, cid_len bytes (known only from negotiation)
48 off += cid_len;
49 off += (b0 & 0x08) ? 2 : 1; // S bit: 16- vs 8-bit sequence number
50 if (b0 & 0x04) // L bit: explicit length present
51 {
52 if (off + 2 > avail)
53 return 0;
54 size_t enc = ((size_t)rec[off] << 8) | rec[off + 1];
55 off += 2;
56 if (off + enc > avail)
57 return 0;
58 return off + enc;
59 }
60 return avail; // no length -> record runs to the end of the datagram
61}
62
63int fail(DtlsConn *c, uint8_t alert)
64{
65 c->state = DtlsConnState::FAILED;
66 c->alert = alert;
67 return -1;
68}
69
70// Finalize a copy of the running transcript without disturbing it (RFC 8446 intermediate hashes).
71void snapshot(const SshSha256Ctx *ctx, uint8_t out[SSH_SHA256_DIGEST_LEN])
72{
73 SshSha256Ctx copy = *ctx;
74 ssh_sha256_final(&copy, out);
75}
76
77// Begin a new outbound flight (RFC 9147 §5.8): drop whatever was buffered for the previous one.
78void flight_reset(DtlsConn *c)
79{
80 c->flight_count = 0;
81 c->flight_len = 0;
82}
83
84// Append one TLS handshake message (@p tls_msg, 4-byte TLS header + body) to the current flight: wrap
85// it in a DTLS handshake header (message_seq from the running counter, so an optional HelloRetryRequest
86// shifts every later message up by one) and buffer the fragment for (re)transmission. @p epoch is 0
87// (DTLSPlaintext) or 2 (DTLSCiphertext). Records are not built here - that happens in flight_transmit,
88// so a retransmission can use fresh record sequence numbers.
89bool flight_add(DtlsConn *c, uint16_t epoch, const uint8_t *tls_msg, size_t tls_len)
90{
91 if (tls_len < 4 || c->flight_count >= DTLS_FLIGHT_MSGS)
92 return false;
93 uint8_t msg_type = tls_msg[0];
94 uint32_t body_len = (uint32_t)(tls_len - 4);
95 uint16_t msg_seq = c->tx_msg_seq++;
96 size_t flen = dtls_hs_frag_build(msg_type, msg_seq, body_len, 0, tls_msg + 4, body_len,
97 c->flight_buf + c->flight_len, sizeof(c->flight_buf) - c->flight_len);
98 if (!flen)
99 return false;
100 c->flight_msgs[c->flight_count].off = c->flight_len;
101 c->flight_msgs[c->flight_count].len = (uint16_t)flen;
102 c->flight_msgs[c->flight_count].epoch = (uint8_t)epoch;
103 c->flight_count++;
104 c->flight_len = (uint16_t)(c->flight_len + flen);
105 return true;
106}
107
108// Protect the buffered flight into @p out with FRESH record sequence numbers (RFC 9147 §5.8: a
109// retransmission MUST use new sequence numbers - reusing one would repeat an AEAD nonce and be dropped
110// by the peer's replay window). Records the record number of each message's transmission for ACK
111// matching. Used for both the initial send and every retransmission.
112bool flight_transmit(DtlsConn *c, uint8_t *out, size_t out_cap, size_t *out_len)
113{
114 for (uint8_t i = 0; i < c->flight_count; i++)
115 {
116 const uint8_t *frag = c->flight_buf + c->flight_msgs[i].off;
117 size_t flen = c->flight_msgs[i].len;
118 uint8_t epoch = c->flight_msgs[i].epoch;
119 uint64_t seq;
120 size_t rn;
121 if (epoch == 0)
122 {
123 seq = c->tx_seq_ep0++;
124 rn = dtls_plaintext_build(DTLS_CT_HANDSHAKE, 0, seq, frag, flen, out + *out_len, out_cap - *out_len);
125 }
126 else
127 {
128 seq = c->tx_seq_ep2++;
129 rn = dtls_ciphertext_protect(&c->ep2_srv, seq, DTLS_CT_HANDSHAKE, frag, flen, out + *out_len,
130 out_cap - *out_len, c->cid_negotiated ? c->peer_cid : nullptr,
131 c->cid_negotiated ? c->peer_cid_len : 0);
132 }
133 if (!rn)
134 return false;
135 *out_len += rn;
136 c->flight_rec[i].epoch = epoch;
137 c->flight_rec[i].seq = seq;
138 }
139 return true;
140}
141
142// Arm the retransmission timer after (re)sending a flight that expects a peer reply (RFC 9147 §5.8).
143void flight_arm(DtlsConn *c)
144{
145 c->awaiting_reply = true;
146 c->retransmits = 0;
147 c->pto_ms = DTLS_PTO_INITIAL_MS;
148 c->flight_sent_ms = detws_millis();
149}
150
151// Stop the retransmission timer: the expected reply arrived, or the flight was acknowledged.
152void flight_disarm(DtlsConn *c)
153{
154 c->awaiting_reply = false;
155}
156
157// Emit a HelloRetryRequest (RFC 9147 §5.1, RFC 8446 §4.1.4) asking the client to retry with an
158// X25519 key_share, binding a stateless return-routability cookie to the peer address. Per RFC 8446
159// §4.4.1 the transcript is restarted as the synthetic message_hash(ClientHello1) before the HRR is
160// folded in, so the eventual transcript is message_hash || HRR || ClientHello2 || ServerHello || ...
161int send_hello_retry(DtlsConn *c, const Tls13ClientHello *ch, const uint8_t *ch1, size_t ch1_len, uint8_t *out,
162 size_t out_cap, size_t *out_len)
163{
164 uint8_t ch1_hash[SSH_SHA256_DIGEST_LEN];
165 SshSha256Ctx h;
166 ssh_sha256_init(&h);
167 ssh_sha256_update(&h, ch1, ch1_len);
168 ssh_sha256_final(&h, ch1_hash);
169
170 ssh_sha256_init(&c->transcript); // restart: message_hash(Hash(CH1)) replaces ClientHello1
171 size_t n = tls13_build_message_hash(c->msgbuf, sizeof(c->msgbuf), ch1_hash);
172 if (!n)
173 return fail(c, ALERT_INTERNAL_ERROR);
174 ssh_sha256_update(&c->transcript, c->msgbuf, n); // transcript only; message_hash is never sent
175
176 // Stateless cookie with an empty payload: this connection keeps its own transcript across the
177 // retry, so the cookie only has to prove return-routability and bind the client address.
178 uint8_t cookie[DTLS_COOKIE_MAX];
179 size_t clen = dtls_cookie_make(c->cfg.cookie_key, detws_millis(), nullptr, 0, c->peer_addr, c->peer_addr_len,
180 cookie, sizeof(cookie));
181 if (!clen)
182 return fail(c, ALERT_INTERNAL_ERROR);
183
184 n = tls13_build_hello_retry_request(c->msgbuf, sizeof(c->msgbuf), ch->session_id, ch->session_id_len,
185 TLS_GROUP_X25519, cookie, clen, /*dtls=*/true);
186 if (!n)
187 return fail(c, ALERT_INTERNAL_ERROR);
188 ssh_sha256_update(&c->transcript, c->msgbuf, n);
189 flight_reset(c);
190 if (!flight_add(c, 0, c->msgbuf, n) || !flight_transmit(c, out, out_cap, out_len))
191 return fail(c, ALERT_INTERNAL_ERROR);
192 flight_arm(c); // await ClientHello2
193 c->hrr_sent = true;
194 return 0;
195}
196
197// After a HelloRetryRequest, the retry ClientHello must echo a valid cookie (proving the client's
198// address) before we spend the handshake's asymmetric crypto (RFC 9147 §5.1). No HRR -> nothing to check.
199bool dtls_hrr_cookie_ok(const DtlsConn *c, const Tls13ClientHello *ch)
200{
201 if (!c->hrr_sent)
202 return true;
203 uint8_t payload[1];
204 size_t plen = 0;
205 return ch->cookie &&
206 dtls_cookie_verify(c->cfg.cookie_key, detws_millis(), DTLS_HRR_COOKIE_MAX_AGE_MS, c->peer_addr,
207 c->peer_addr_len, ch->cookie, ch->cookie_len, payload, sizeof(payload), &plen);
208}
209
210// Connection-id negotiation (RFC 9146 / RFC 9147 §9): if the client offered a CID we can hold, store it
211// (placed in records we send it) and choose our own CID from the fresh ServerHello random (unique per
212// connection) for the records the client sends us.
213void dtls_negotiate_conn_id(DtlsConn *c, const Tls13ClientHello *ch)
214{
215 if (!ch->has_conn_id || ch->conn_id_len > DTLS_CID_MAX)
216 return;
217 c->cid_negotiated = true;
218 c->peer_cid_len = (uint8_t)ch->conn_id_len;
219 if (ch->conn_id_len)
220 memcpy(c->peer_cid, ch->conn_id, ch->conn_id_len);
221 c->local_cid_len = DTLS_CONN_LOCAL_CID_LEN;
222 memcpy(c->local_cid, c->cfg.server_random, DTLS_CONN_LOCAL_CID_LEN);
223}
224
225// Consume a ClientHello and emit the whole server flight (ServerHello + the epoch-2 encrypted
226// messages), installing handshake and application keys. Mirrors quic_tls process_client_hello. If the
227// client did not offer an X25519 key_share, this instead sends a HelloRetryRequest and returns to wait
228// for the client's second ClientHello (RFC 9147 §5.1).
229int handle_client_hello(DtlsConn *c, const uint8_t *msg, size_t msg_len, uint8_t *out, size_t out_cap, size_t *out_len)
230{
231 Tls13ClientHello ch;
232 if (!tls13_parse_client_hello(msg, msg_len, &ch, /*dtls=*/true))
233 return fail(c, ALERT_DECODE_ERROR);
234 if (!ch.offers_tls13)
235 return fail(c, ALERT_PROTOCOL_VERSION);
236 if (!ch.offers_ed25519 || !ch.offers_x25519)
237 return fail(c, ALERT_HANDSHAKE_FAILURE);
238
239 uint16_t ch_seq = c->reasm.msg_seq; // the message_seq this ClientHello arrived as
240
241 // Group negotiation (RFC 8446 §4.1.4): the client offered X25519 but sent no X25519 key_share.
242 // Answer with a HelloRetryRequest and await the retry - but only once (a retry that still lacks
243 // the share is fatal, so a malicious client cannot loop us).
244 if (!ch.has_key_share)
245 {
246 if (c->hrr_sent)
247 return fail(c, ALERT_HANDSHAKE_FAILURE);
248 if (send_hello_retry(c, &ch, msg, msg_len, out, out_cap, out_len) < 0)
249 return -1;
250 c->next_recv_msg_seq = (uint16_t)(ch_seq + 1);
251 dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4, DTLS_CONN_REASM_CAP);
252 return 0;
253 }
254
255 // A key_share is present. If it followed our HelloRetryRequest, the client must echo the cookie,
256 // authenticating its address before we spend the handshake's asymmetric crypto (§5.1).
257 if (!dtls_hrr_cookie_ok(c, &ch))
258 return fail(c, ALERT_HANDSHAKE_FAILURE);
259
260 dtls_negotiate_conn_id(c, &ch);
261
262 // X25519 shared secret and the server's key_share.
263 uint8_t ecdhe[32];
264 uint8_t server_share[32];
265 ssh_x25519(ecdhe, c->cfg.ephemeral_priv, ch.client_x25519);
266 ssh_x25519_base(server_share, c->cfg.ephemeral_priv);
267
268 ssh_sha256_update(&c->transcript, msg, msg_len); // transcript: ClientHello (CH2 when an HRR preceded it)
269
270 flight_reset(c); // this ClientHello starts a fresh server flight (ServerHello..Finished)
271
272 // ServerHello (epoch 0, plaintext).
273 size_t n =
274 tls13_build_server_hello(c->msgbuf, sizeof(c->msgbuf), c->cfg.server_random, ch.session_id, ch.session_id_len,
275 server_share, 32, TLS_GROUP_X25519, /*dtls=*/true,
276 c->cid_negotiated ? c->local_cid : nullptr, c->cid_negotiated ? c->local_cid_len : 0);
277 if (!n)
278 return fail(c, ALERT_INTERNAL_ERROR);
279 ssh_sha256_update(&c->transcript, c->msgbuf, n);
280 if (!flight_add(c, 0, c->msgbuf, n))
281 return fail(c, ALERT_INTERNAL_ERROR);
282
283 // Handshake-traffic keys from Transcript-Hash(..ServerHello).
284 uint8_t hash[SSH_SHA256_DIGEST_LEN];
285 snapshot(&c->transcript, hash);
286 tls13_ks_early(&DTLS13_KDF, &c->ks);
287 tls13_ks_handshake(&c->ks, ecdhe, hash, 32);
288 dtls_record_keys_derive(&c->ep2_srv, DtlsCipher::AES_128_GCM_SHA256, 2, c->ks.server_hs_traffic);
289 dtls_record_keys_derive(&c->ep2_cli, DtlsCipher::AES_128_GCM_SHA256, 2, c->ks.client_hs_traffic);
290 c->ep2_ready = true;
291
292 // EncryptedExtensions.
293 n = tls13_build_encrypted_extensions_empty(c->msgbuf, sizeof(c->msgbuf));
294 ssh_sha256_update(&c->transcript, c->msgbuf, n);
295 if (!flight_add(c, 2, c->msgbuf, n))
296 return fail(c, ALERT_INTERNAL_ERROR);
297
298 // Certificate.
299 n = tls13_build_certificate(c->msgbuf, sizeof(c->msgbuf), c->cfg.cert_der, c->cfg.cert_len);
300 if (!n)
301 return fail(c, ALERT_INTERNAL_ERROR);
302 ssh_sha256_update(&c->transcript, c->msgbuf, n);
303 if (!flight_add(c, 2, c->msgbuf, n))
304 return fail(c, ALERT_INTERNAL_ERROR);
305
306 // CertificateVerify signs Transcript-Hash(..Certificate).
307 snapshot(&c->transcript, hash);
308 n = tls13_build_cert_verify(c->msgbuf, sizeof(c->msgbuf), hash, c->cfg.ed25519_seed);
309 if (!n)
310 return fail(c, ALERT_INTERNAL_ERROR);
311 ssh_sha256_update(&c->transcript, c->msgbuf, n);
312 if (!flight_add(c, 2, c->msgbuf, n))
313 return fail(c, ALERT_INTERNAL_ERROR);
314
315 // Server Finished over Transcript-Hash(..CertificateVerify).
316 snapshot(&c->transcript, hash);
317 uint8_t verify[SSH_SHA256_DIGEST_LEN];
318 tls13_finished_mac(&DTLS13_KDF, c->ks.server_hs_traffic, hash, verify);
319 n = tls13_build_finished(c->msgbuf, sizeof(c->msgbuf), verify);
320 ssh_sha256_update(&c->transcript, c->msgbuf, n);
321 if (!flight_add(c, 2, c->msgbuf, n))
322 return fail(c, ALERT_INTERNAL_ERROR);
323
324 // Application-traffic keys from Transcript-Hash(..server Finished); this hash also verifies the
325 // client's Finished.
326 snapshot(&c->transcript, c->hs_finished_hash);
327 tls13_ks_master(&c->ks, c->hs_finished_hash);
328 dtls_record_keys_derive(&c->ep3_srv, DtlsCipher::AES_128_GCM_SHA256, 3, c->ks.server_ap_traffic);
329 dtls_record_keys_derive(&c->ep3_cli, DtlsCipher::AES_128_GCM_SHA256, 3, c->ks.client_ap_traffic);
330 c->ep3_ready = true;
331
332 if (!flight_transmit(c, out, out_cap, out_len)) // protect the whole flight now that ep2 keys exist
333 return fail(c, ALERT_INTERNAL_ERROR);
334 flight_arm(c); // await the client Finished
335 c->state = DtlsConnState::WAIT_FINISHED;
336 c->next_recv_msg_seq = (uint16_t)(ch_seq + 1);
337 dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4, DTLS_CONN_REASM_CAP);
338 return 0;
339}
340
341// Verify the client's Finished and complete the handshake.
342int handle_client_finished(DtlsConn *c, const uint8_t *msg, size_t msg_len)
343{
344 if (msg[0] != TlsHs::TLS_HS_FINISHED || msg_len != 4 + SSH_SHA256_DIGEST_LEN)
345 return fail(c, ALERT_DECODE_ERROR);
346 uint8_t expected[SSH_SHA256_DIGEST_LEN];
347 tls13_finished_mac(&DTLS13_KDF, c->ks.client_hs_traffic, c->hs_finished_hash, expected);
348 uint8_t diff = 0;
349 for (int i = 0; i < SSH_SHA256_DIGEST_LEN; i++)
350 diff |= (uint8_t)(expected[i] ^ msg[4 + i]);
351 if (diff)
352 return fail(c, ALERT_DECRYPT_ERROR);
353 ssh_sha256_update(&c->transcript, msg, msg_len);
354 c->state = DtlsConnState::DONE;
355 flight_disarm(c); // the reply arrived; stop retransmitting the server flight
356 // Re-arm the reassembler for the same message_seq so a retransmitted Finished (its ACK was lost)
357 // completes again and we re-acknowledge it, instead of being rejected as unexpected (RFC 9147 §5.8.3).
358 dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4, DTLS_CONN_REASM_CAP);
359 return 0;
360}
361
362int dispatch_message(DtlsConn *c, const uint8_t *tls_msg, size_t tls_len, uint8_t *out, size_t out_cap, size_t *out_len)
363{
364 if (c->state == DtlsConnState::START && tls_msg[0] == TlsHs::TLS_HS_CLIENT_HELLO)
365 return handle_client_hello(c, tls_msg, tls_len, out, out_cap, out_len);
366 if (c->state == DtlsConnState::WAIT_FINISHED && tls_msg[0] == TlsHs::TLS_HS_FINISHED)
367 return handle_client_finished(c, tls_msg, tls_len);
368 if (c->state == DtlsConnState::DONE && tls_msg[0] == TlsHs::TLS_HS_FINISHED)
369 {
370 c->hs_ack_sent = false; // a retransmitted client Finished (our ACK was lost): re-acknowledge it
371 dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4,
372 DTLS_CONN_REASM_CAP); // accept the next one too
373 return 0;
374 }
375 return fail(c, ALERT_UNEXPECTED_MESSAGE);
376}
377
378// Parse and reassemble the DTLS handshake fragments carried in one record's payload, dispatching each
379// complete TLS message.
380int drive_handshake(DtlsConn *c, const uint8_t *payload, size_t plen, uint8_t *out, size_t out_cap, size_t *out_len)
381{
382 size_t p = 0;
383 while (p < plen)
384 {
385 DtlsHsHeader hh;
386 size_t used = dtls_hs_header_parse(payload + p, plen - p, &hh);
387 if (!used)
388 break;
389 p += used;
390 int r = dtls_hs_reasm_add(&c->reasm, &hh); // ignores fragments for other message_seqs
391 if (r < 0)
392 return fail(c, ALERT_DECODE_ERROR);
393 if (r == 1)
394 {
395 // Rebuild the TLS handshake structure (4-byte header + reassembled body) for the transcript.
396 c->reasm_buf[0] = c->reasm.msg_type;
397 c->reasm_buf[1] = (uint8_t)(c->reasm.length >> 16);
398 c->reasm_buf[2] = (uint8_t)(c->reasm.length >> 8);
399 c->reasm_buf[3] = (uint8_t)c->reasm.length;
400 if (dispatch_message(c, c->reasm_buf, 4 + c->reasm.length, out, out_cap, out_len) < 0)
401 return -1;
402 }
403 }
404 return 0;
405}
406
407// A client ACK (RFC 9147 §7) for the outstanding flight: if it acknowledges every message of the last
408// transmission, the peer has the whole flight, so stop retransmitting (§5.8.3). A partial ACK is
409// ignored here - the timer simply retransmits the whole flight, which is always correct.
410void process_ack(DtlsConn *c, const uint8_t *body, size_t len)
411{
412 if (!c->awaiting_reply)
413 return;
414 DtlsRecordNumber acked[16];
415 size_t count = 0;
416 if (!dtls_ack_parse(body, len, acked, 16, &count))
417 return;
418 for (uint8_t i = 0; i < c->flight_count; i++)
419 {
420 bool found = false;
421 for (size_t j = 0; j < count; j++)
422 if (acked[j].epoch == c->flight_rec[i].epoch && acked[j].seq == c->flight_rec[i].seq)
423 {
424 found = true;
425 break;
426 }
427 if (!found)
428 return; // a flight record is still unacknowledged; keep the timer running
429 }
430 flight_disarm(c);
431}
432
433// One-record outcome for the dtls_conn_process datagram walk.
434enum class DtlsRecStep
435{
436 NEXT, // record consumed; keep walking the datagram
437 STOP, // malformed/short record; stop walking (leave the rest)
438 FATAL, // fatal error already recorded via fail(); caller returns -1
439};
440
441// Process one ciphertext (epoch-2) record at dgram[*off], advancing *off past a well-formed record.
442DtlsRecStep process_ciphertext_record(DtlsConn *c, const uint8_t *dgram, size_t len, size_t *off, uint8_t *out,
443 size_t out_cap, size_t *out_len)
444{
445 size_t rlen = ciphertext_record_len(dgram + *off, len - *off, c->cid_negotiated ? c->local_cid_len : 0);
446 if (!rlen)
447 return DtlsRecStep::STOP; // malformed header; stop walking the datagram
448 if (!c->ep2_ready)
449 {
450 fail(c, ALERT_UNEXPECTED_MESSAGE);
451 return DtlsRecStep::FATAL;
452 }
453 uint8_t inner[DTLS_CONN_REASM_CAP + DTLS_TAG_LEN];
454 DtlsCiphertext info;
455 uint64_t next = c->replay_ep2.seeded ? c->replay_ep2.highest + 1 : 0;
456 if (!dtls_ciphertext_unprotect(&c->ep2_cli, next, dgram + *off, rlen, inner, sizeof(inner), &info,
457 c->cid_negotiated ? c->local_cid : nullptr,
458 c->cid_negotiated ? c->local_cid_len : 0))
459 {
460 fail(c, ALERT_DECRYPT_ERROR);
461 return DtlsRecStep::FATAL;
462 }
463 *off += rlen;
464 if (!dtls_replay_check(&c->replay_ep2, info.seq))
465 return DtlsRecStep::NEXT; // replay: drop, but keep processing the datagram
466 dtls_replay_mark(&c->replay_ep2, info.seq);
467 bool is_hs = (info.content_type == DTLS_CT_HANDSHAKE);
468 if (is_hs)
469 c->rx_ep2_seq = info.seq; // the client Finished's record number, for the completion ACK
470 if (info.content_type == DTLS_CT_ACK)
471 process_ack(c, inner, info.pt_len); // the client acknowledged our flight
472 else if (is_hs && drive_handshake(c, inner, info.pt_len, out, out_cap, out_len) < 0)
473 return DtlsRecStep::FATAL;
474 return DtlsRecStep::NEXT;
475}
476
477// Process one plaintext (epoch-0) record at dgram[*off], advancing *off past a well-formed record.
478DtlsRecStep process_plaintext_record(DtlsConn *c, const uint8_t *dgram, size_t len, size_t *off, uint8_t *out,
479 size_t out_cap, size_t *out_len)
480{
481 DtlsPlaintext pt;
482 size_t rlen = dtls_plaintext_parse(dgram + *off, len - *off, &pt);
483 if (!rlen)
484 return DtlsRecStep::STOP;
485 *off += rlen;
486 if (pt.content_type == DTLS_CT_HANDSHAKE && drive_handshake(c, pt.fragment, pt.frag_len, out, out_cap, out_len) < 0)
487 return DtlsRecStep::FATAL;
488 return DtlsRecStep::NEXT;
489}
490
491// Once the client Finished completes the handshake, acknowledge it so the client stops retransmitting
492// its final flight (RFC 9147 §5.8.3). The ACK is a content-type-26 record in the highest available epoch
493// (3, application), covering the epoch-2 Finished record (§7). Sent at most once.
494void maybe_send_completion_ack(DtlsConn *c, uint8_t *out, size_t out_cap, size_t *out_len)
495{
496 if (!dtls_conn_established(c) || c->hs_ack_sent)
497 return;
498 DtlsRecordNumber rn = {2, c->rx_ep2_seq};
499 uint8_t ack_body[2 + 16];
500 size_t bl = dtls_ack_build(&rn, 1, ack_body, sizeof(ack_body));
501 size_t rec = dtls_ciphertext_protect(&c->ep3_srv, c->tx_seq_ep3++, DTLS_CT_ACK, ack_body, bl, out + *out_len,
502 out_cap - *out_len, c->cid_negotiated ? c->peer_cid : nullptr,
503 c->cid_negotiated ? c->peer_cid_len : 0);
504 if (rec)
505 {
506 *out_len += rec;
507 c->hs_ack_sent = true;
508 }
509}
510} // namespace
511
512void dtls_conn_init(DtlsConn *c, const DtlsServerConfig *cfg, const uint8_t *peer_addr, size_t peer_addr_len)
513{
514 memset(c, 0, sizeof(*c));
515 c->cfg = *cfg;
516 c->state = DtlsConnState::START;
517 if (peer_addr && peer_addr_len)
518 {
519 if (peer_addr_len > DTLS_PEER_ADDR_MAX)
520 peer_addr_len = DTLS_PEER_ADDR_MAX;
521 memcpy(c->peer_addr, peer_addr, peer_addr_len);
522 c->peer_addr_len = (uint8_t)peer_addr_len;
523 }
524 ssh_sha256_init(&c->transcript);
525 dtls_replay_init(&c->replay_ep2);
526 dtls_replay_init(&c->replay_ep3);
527 c->next_recv_msg_seq = 0;
528 dtls_hs_reasm_init(&c->reasm, 0, c->reasm_buf + 4, DTLS_CONN_REASM_CAP);
529}
530
531int dtls_conn_process(DtlsConn *c, const uint8_t *dgram, size_t len, uint8_t *out, size_t out_cap)
532{
533 if (c->state == DtlsConnState::FAILED)
534 return -1;
535 size_t out_len = 0;
536 size_t off = 0;
537 while (off < len)
538 {
539 DtlsRecStep step = is_ciphertext(dgram[off])
540 ? process_ciphertext_record(c, dgram, len, &off, out, out_cap, &out_len)
541 : process_plaintext_record(c, dgram, len, &off, out, out_cap, &out_len);
542 if (step == DtlsRecStep::FATAL)
543 return -1;
544 if (step == DtlsRecStep::STOP)
545 break;
546 }
547
548 maybe_send_completion_ack(c, out, out_cap, &out_len);
549 return (int)out_len;
550}
551
552int dtls_conn_timeout_ms(const DtlsConn *c)
553{
554 if (!c->awaiting_reply || c->state == DtlsConnState::FAILED || c->state == DtlsConnState::DONE)
555 return -1;
556 // Wrap-safe remaining time: (deadline - now) as a signed delta, clamped at 0 (already due).
557 int32_t remaining = (int32_t)(c->flight_sent_ms + c->pto_ms - detws_millis());
558 return remaining > 0 ? remaining : 0;
559}
560
561int dtls_conn_on_timeout(DtlsConn *c, uint8_t *out, size_t out_cap)
562{
563 if (!c->awaiting_reply || c->state == DtlsConnState::FAILED || c->state == DtlsConnState::DONE)
564 return 0;
565 if ((int32_t)(detws_millis() - (c->flight_sent_ms + c->pto_ms)) < 0)
566 return 0; // not yet due (spurious / early wake-up)
567 if (c->retransmits >= DTLS_MAX_RETRANSMITS)
568 {
569 // Peer is gone; abandon the handshake. No alert - there is nobody to receive it.
570 c->state = DtlsConnState::FAILED;
571 c->awaiting_reply = false;
572 return -1;
573 }
574 size_t out_len = 0;
575 if (!flight_transmit(c, out, out_cap, &out_len))
576 return -1;
577 c->retransmits++;
578 c->pto_ms = c->pto_ms >= DTLS_PTO_MAX_MS / 2 ? DTLS_PTO_MAX_MS : c->pto_ms * 2; // §5.8.1 backoff, capped
579 c->flight_sent_ms = detws_millis();
580 return (int)out_len;
581}
582
583bool dtls_conn_established(const DtlsConn *c)
584{
585 return c->state == DtlsConnState::DONE && c->ep3_ready;
586}
587
588uint8_t dtls_conn_alert(const DtlsConn *c)
589{
590 return c->alert;
591}
592
593const DtlsRecordKeys *dtls_conn_app_write_keys(const DtlsConn *c)
594{
595 return c->ep3_ready ? &c->ep3_srv : nullptr;
596}
597
598const DtlsRecordKeys *dtls_conn_app_read_keys(const DtlsConn *c)
599{
600 return c->ep3_ready ? &c->ep3_cli : nullptr;
601}
602
603size_t dtls_conn_local_cid(const DtlsConn *c, uint8_t *out)
604{
605 if (!c->cid_negotiated || c->local_cid_len == 0)
606 return 0;
607 memcpy(out, c->local_cid, c->local_cid_len);
608 return c->local_cid_len;
609}
610
611bool 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)
612{
613 if (!dtls_conn_established(c))
614 return false;
615 DtlsCiphertext info;
616 uint64_t next = c->replay_ep3.seeded ? c->replay_ep3.highest + 1 : 0;
617 if (!dtls_ciphertext_unprotect(&c->ep3_cli, next, rec, rec_len, out, out_cap, &info,
618 c->cid_negotiated ? c->local_cid : nullptr,
619 c->cid_negotiated ? c->local_cid_len : 0))
620 return false;
621 if (!dtls_replay_check(&c->replay_ep3, info.seq))
622 return false; // replay or too old
623 dtls_replay_mark(&c->replay_ep3, info.seq);
624 if (info.content_type != DTLS_CT_APPLICATION_DATA)
625 return false;
626 *out_len = info.pt_len;
627 return true;
628}
629
630size_t dtls_conn_seal_app(DtlsConn *c, const uint8_t *data, size_t len, uint8_t *out, size_t out_cap)
631{
632 if (!dtls_conn_established(c))
633 return 0;
634 // tx_seq_ep3 is shared with the completion ACK, so app records never reuse its sequence number.
635 return dtls_ciphertext_protect(&c->ep3_srv, c->tx_seq_ep3++, DTLS_CT_APPLICATION_DATA, data, len, out, out_cap,
636 c->cid_negotiated ? c->peer_cid : nullptr, c->cid_negotiated ? c->peer_cid_len : 0);
637}
638
639#endif // DETWS_ENABLE_DTLS
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
DTLS 1.3 server handshake state machine (RFC 9147 §5-6).
void ssh_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
void ssh_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
void ssh_sha256_init(SshSha256Ctx *ctx)
Initialize a streaming SHA-256 context.
void ssh_sha256_update(SshSha256Ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
void ssh_sha256_final(SshSha256Ctx *ctx, uint8_t digest[SSH_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest.
#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 handshake messages for the QUIC handshake (RFC 8446 sec 4).