DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dtls_handshake.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_handshake.h
6 * @brief DTLS 1.3 handshake framing and reliability (RFC 9147 §5, §7).
7 *
8 * The datagram-reliability layer that sits between the DTLS record layer (dtls_record) and the
9 * reused TLS 1.3 message builders (tls13_msg). TLS 1.3 assumes an in-order reliable byte stream;
10 * DTLS carries the same handshake messages over lossy, reorderable datagrams, so each message gains
11 * a 12-byte DTLS handshake header (RFC 9147 §5.2) that lets a fragment be placed independently of
12 * the record that carried it, and lost flights are recovered with acknowledgements (§7) rather than
13 * TCP retransmission.
14 *
15 * This file is pure framing - no crypto state, no sockets. It provides:
16 * - the 12-byte handshake header (@ref dtls_hs_header_parse / @ref dtls_hs_frag_build);
17 * - overlap-tolerant message reassembly (@ref DtlsHsReasm), modelled on the QUIC CRYPTO-stream
18 * reassembler - a fragment may arrive split, duplicated, or overlapping (§5.4);
19 * - the ACK message (@ref dtls_ack_build / @ref dtls_ack_parse, content type 26, §7);
20 * - the stateless HelloRetryRequest cookie (@ref dtls_cookie_make / @ref dtls_cookie_verify,
21 * the §5.1 return-routability / anti-amplification defence).
22 *
23 * The handshake state machine that drives these (flights, epochs, PTO) is dtls_conn; the TLS 1.3
24 * message bodies and key schedule are reused verbatim from the HTTP/3 stack (tls13_msg, tls13_kdf).
25 *
26 * @author Douglas Quigg (dstroy0)
27 * @date 2026
28 */
29
30#ifndef DETERMINISTICESPASYNCWEBSERVER_DTLS_HANDSHAKE_H
31#define DETERMINISTICESPASYNCWEBSERVER_DTLS_HANDSHAKE_H
32
33#include "ServerConfig.h"
34
35#if DETWS_ENABLE_DTLS
36
37#include <stddef.h>
38#include <stdint.h>
39
40/** @brief DTLS handshake header length: msg_type(1) + length(3) + message_seq(2) + fragment_offset(3)
41 * + fragment_length(3) = 12 bytes (RFC 9147 §5.2). */
42static constexpr size_t DTLS_HS_HDR_LEN = 12;
43
44/** @brief message_hash synthetic-message type used when wrapping ClientHello1 for a HelloRetryRequest
45 * transcript (RFC 8446 §4.4.1). Framing constant; the transcript itself lives in dtls_conn. */
46static constexpr uint8_t DTLS_HS_TYPE_MESSAGE_HASH = 254;
47
48// ---------------------------------------------------------------------------
49// Handshake message header (RFC 9147 §5.2)
50// ---------------------------------------------------------------------------
51
52/** @brief Parsed view of one DTLS handshake message fragment (fields point into the caller buffer). */
53struct DtlsHsHeader
54{
55 uint8_t msg_type; ///< HandshakeType (client_hello, server_hello, finished, ...)
56 uint32_t length; ///< full reassembled body length (uint24 on the wire)
57 uint16_t msg_seq; ///< handshake message sequence number
58 uint32_t frag_offset; ///< byte offset of this fragment within the body (uint24)
59 uint32_t frag_length; ///< length of this fragment (uint24)
60 const uint8_t *fragment; ///< fragment bytes, into the input buffer
61};
62
63/**
64 * @brief Parse one DTLS handshake message fragment.
65 *
66 * Validates that @p len holds the 12-byte header plus @c fragment_length bytes and that the fragment
67 * lies within the declared message length.
68 *
69 * @return bytes consumed (DTLS_HS_HDR_LEN + fragment_length), or 0 if malformed / truncated.
70 */
71size_t dtls_hs_header_parse(const uint8_t *p, size_t len, DtlsHsHeader *out);
72
73/**
74 * @brief Build one DTLS handshake message fragment: the 12-byte header followed by the fragment body.
75 *
76 * @param msg_type HandshakeType.
77 * @param msg_seq handshake message sequence number.
78 * @param full_len total (reassembled) message body length.
79 * @param frag_offset byte offset of this fragment within the body.
80 * @param frag fragment bytes.
81 * @param frag_len number of fragment bytes.
82 * @return total bytes written (DTLS_HS_HDR_LEN + @p frag_len), or 0 on overflow / range error.
83 */
84size_t dtls_hs_frag_build(uint8_t msg_type, uint16_t msg_seq, uint32_t full_len, uint32_t frag_offset,
85 const uint8_t *frag, uint32_t frag_len, uint8_t *out, size_t out_cap);
86
87// ---------------------------------------------------------------------------
88// Message reassembly (RFC 9147 §5.4): overlap-tolerant, no heap
89// ---------------------------------------------------------------------------
90
91/** @brief Max distinct byte ranges tracked while reassembling one message (bounds the work an
92 * adversary can force by sending maximally fragmented flights). */
93static constexpr size_t DTLS_HS_REASM_MAX_RANGES = 8;
94
95/**
96 * @brief Reassembles the fragments of a single handshake message into a contiguous body.
97 *
98 * Fragments may arrive out of order, duplicated, or with overlapping ranges (RFC 9147 §5.4 requires
99 * an implementation to handle overlap). Received byte ranges are merged into a bounded interval list;
100 * the message is complete when a single interval covers [0, length).
101 */
102struct DtlsHsReasm
103{
104 bool active; ///< false until the first fragment of the target message is seen
105 bool have_len; ///< true once a fragment established the full message length
106 uint8_t msg_type; ///< HandshakeType of the message being reassembled
107 uint16_t msg_seq; ///< the message sequence number this reassembler accepts
108 uint32_t length; ///< full body length (from the first non-empty fragment)
109 uint8_t *buf; ///< caller-provided body buffer (>= length bytes)
110 size_t buf_cap; ///< capacity of @ref buf
111 uint32_t range_lo[DTLS_HS_REASM_MAX_RANGES]; ///< received interval starts
112 uint32_t range_hi[DTLS_HS_REASM_MAX_RANGES]; ///< received interval ends (exclusive)
113 uint8_t range_count; ///< number of active intervals
114};
115
116/**
117 * @brief Prepare a reassembler for the message numbered @p msg_seq into @p buf.
118 * @param buf body buffer the reassembled message is written into.
119 * @param buf_cap capacity of @p buf; a message longer than this is rejected.
120 */
121void dtls_hs_reasm_init(DtlsHsReasm *r, uint16_t msg_seq, uint8_t *buf, size_t buf_cap);
122
123/**
124 * @brief Feed one parsed fragment into the reassembler.
125 *
126 * Fragments whose @c msg_seq differs from the target are ignored (return 0): the state machine, not
127 * the reassembler, decides what to do with past/future messages.
128 *
129 * @return 1 if the message is now complete (@ref DtlsHsReasm::buf holds @ref DtlsHsReasm::length
130 * bytes), 0 if accepted but incomplete (or not this message), -1 on error (length overflow,
131 * inconsistent length, or too many distinct ranges).
132 */
133int dtls_hs_reasm_add(DtlsHsReasm *r, const DtlsHsHeader *frag);
134
135// ---------------------------------------------------------------------------
136// ACK message (RFC 9147 §7): content type 26
137// ---------------------------------------------------------------------------
138
139/** @brief A record identified for acknowledgement: (epoch, sequence_number), 8 bytes each on the
140 * wire (RFC 9147 §7). */
141struct DtlsRecordNumber
142{
143 uint64_t epoch;
144 uint64_t seq;
145};
146
147/**
148 * @brief Build an ACK message body (RFC 9147 §7): uint16 length prefix then @p count 16-byte record
149 * numbers, big-endian epoch then sequence_number.
150 * @return body bytes written (2 + 16*count), or 0 on overflow.
151 */
152size_t dtls_ack_build(const DtlsRecordNumber *nums, size_t count, uint8_t *out, size_t out_cap);
153
154/**
155 * @brief Parse an ACK message body into record numbers.
156 * @param out destination array.
157 * @param out_cap capacity of @p out in entries.
158 * @param out_count receives the number of record numbers parsed.
159 * @return true on a well-formed ACK that fits in @p out; false if malformed or too many entries.
160 */
161bool dtls_ack_parse(const uint8_t *body, size_t len, DtlsRecordNumber *out, size_t out_cap, size_t *out_count);
162
163// ---------------------------------------------------------------------------
164// HelloRetryRequest cookie (RFC 9147 §5.1): stateless return-routability
165// ---------------------------------------------------------------------------
166
167/** @brief Maximum cookie length this implementation emits / accepts. Overhead is 43 bytes
168 * (version + timestamp + payload_len + HMAC); the rest is available for the payload. */
169static constexpr size_t DTLS_COOKIE_MAX = 128;
170
171/**
172 * @brief Build a stateless HRR cookie binding the client address and an opaque server payload.
173 *
174 * Wire layout: version(1)=1 || timestamp_be(8) || payload_len(2) || payload || HMAC-SHA256(32).
175 * The MAC covers version || timestamp || @p client_addr || payload_len || payload, so the client
176 * address is authenticated without being stored (RFC 9147 §5.1: embedding the apparent client
177 * address stops an attacker forging a cookie for another peer). The @p payload carries whatever the
178 * state machine needs to resume statelessly after the retry (e.g. the ClientHello1 transcript hash
179 * plus the selected group).
180 *
181 * @param hmac_key 32-byte server secret (rotate periodically per §5.1).
182 * @param timestamp monotonic value stamped into the cookie for freshness checks.
183 * @param payload opaque server state to carry through the retry.
184 * @param client_addr serialized client address, mixed into the MAC.
185 * @return cookie bytes written, or 0 on overflow / oversized payload.
186 */
187size_t dtls_cookie_make(const uint8_t hmac_key[32], uint64_t timestamp, const uint8_t *payload, size_t payload_len,
188 const uint8_t *client_addr, size_t addr_len, uint8_t *out, size_t out_cap);
189
190/**
191 * @brief Validate a cookie echoed in a second ClientHello and recover its payload.
192 *
193 * Recomputes the MAC over the cookie fields plus @p client_addr and compares in constant time, then
194 * checks the timestamp is within (@p now - @p max_age, @p now]. A cookie minted for a different
195 * client address fails the MAC check.
196 *
197 * @param now current timestamp in the same units as @ref dtls_cookie_make.
198 * @param max_age maximum accepted age; 0 disables the freshness check.
199 * @param payload_out receives the carried payload on success.
200 * @param payload_cap capacity of @p payload_out.
201 * @param payload_len_out receives the payload length on success.
202 * @return true if the cookie is authentic, fresh, and bound to @p client_addr.
203 */
204bool dtls_cookie_verify(const uint8_t hmac_key[32], uint64_t now, uint64_t max_age, const uint8_t *client_addr,
205 size_t addr_len, const uint8_t *cookie, size_t cookie_len, uint8_t *payload_out,
206 size_t payload_cap, size_t *payload_len_out);
207
208#endif // DETWS_ENABLE_DTLS
209#endif // DETERMINISTICESPASYNCWEBSERVER_DTLS_HANDSHAKE_H
User-facing configuration for DeterministicESPAsyncWebServer.