DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dtls_handshake.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_handshake.cpp
6 * @brief DTLS 1.3 handshake framing and reliability (RFC 9147 §5, §7). See dtls_handshake.h.
7 */
8
10
11#if DETWS_ENABLE_DTLS
12
14#include <string.h>
15
16namespace
17{
18void put_u64(uint8_t *p, uint64_t v)
19{
20 for (int i = 0; i < 8; i++)
21 p[i] = (uint8_t)(v >> (8 * (7 - i)));
22}
23
24uint64_t get_u64(const uint8_t *p)
25{
26 uint64_t v = 0;
27 for (int i = 0; i < 8; i++)
28 v = (v << 8) | p[i];
29 return v;
30}
31
32// Constant-time equality: no early exit, so a forged cookie MAC leaks nothing through timing.
33bool ct_equal(const uint8_t *a, const uint8_t *b, size_t len)
34{
35 uint8_t d = 0;
36 for (size_t i = 0; i < len; i++)
37 d |= (uint8_t)(a[i] ^ b[i]);
38 return d == 0;
39}
40
41// Merge the received byte range [lo, hi) into the reassembler's sorted, disjoint interval list,
42// absorbing any overlapping or adjacent intervals. Returns -1 if there is no room to insert a new
43// disjoint interval (the fragmentation is too scattered for the bounded list).
44int reasm_merge(DtlsHsReasm *r, uint32_t lo, uint32_t hi)
45{
46 uint8_t n = r->range_count;
47 uint8_t i = 0;
48 while (i < n)
49 {
50 // Disjoint (a gap on either side): keep and advance. Touching (hi == lo) merges so the
51 // completion check can collapse contiguous fragments into a single [0, length) interval.
52 if (r->range_hi[i] < lo || r->range_lo[i] > hi)
53 {
54 i++;
55 continue;
56 }
57 if (r->range_lo[i] < lo)
58 lo = r->range_lo[i];
59 if (r->range_hi[i] > hi)
60 hi = r->range_hi[i];
61 for (uint8_t j = i; j + 1 < n; j++) // remove interval i; re-check the one shifted into its slot
62 {
63 r->range_lo[j] = r->range_lo[j + 1];
64 r->range_hi[j] = r->range_hi[j + 1];
65 }
66 n--;
67 }
68 if (n >= DTLS_HS_REASM_MAX_RANGES)
69 return -1;
70 uint8_t k = n; // insert [lo, hi) keeping the list sorted by lo
71 while (k > 0 && r->range_lo[k - 1] > lo)
72 {
73 r->range_lo[k] = r->range_lo[k - 1];
74 r->range_hi[k] = r->range_hi[k - 1];
75 k--;
76 }
77 r->range_lo[k] = lo;
78 r->range_hi[k] = hi;
79 r->range_count = (uint8_t)(n + 1);
80 return 0;
81}
82} // namespace
83
84// ---------------------------------------------------------------------------
85// Handshake message header (RFC 9147 §5.2)
86// ---------------------------------------------------------------------------
87
88size_t dtls_hs_header_parse(const uint8_t *p, size_t len, DtlsHsHeader *out)
89{
90 if (len < DTLS_HS_HDR_LEN)
91 return 0;
92 out->msg_type = p[0];
93 out->length = ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
94 out->msg_seq = (uint16_t)(((uint16_t)p[4] << 8) | p[5]);
95 out->frag_offset = ((uint32_t)p[6] << 16) | ((uint32_t)p[7] << 8) | p[8];
96 out->frag_length = ((uint32_t)p[9] << 16) | ((uint32_t)p[10] << 8) | p[11];
97 if (out->frag_offset + out->frag_length > out->length)
98 return 0; // fragment falls outside the declared message
99 if (DTLS_HS_HDR_LEN + out->frag_length > len)
100 return 0; // fragment bytes truncated
101 out->fragment = p + DTLS_HS_HDR_LEN;
102 return DTLS_HS_HDR_LEN + out->frag_length;
103}
104
105size_t dtls_hs_frag_build(uint8_t msg_type, uint16_t msg_seq, uint32_t full_len, uint32_t frag_offset,
106 const uint8_t *frag, uint32_t frag_len, uint8_t *out, size_t out_cap)
107{
108 if (full_len > 0xFFFFFF || frag_offset > 0xFFFFFF || frag_len > 0xFFFFFF)
109 return 0; // uint24 fields
110 if (frag_offset + frag_len > full_len)
111 return 0;
112 size_t total = DTLS_HS_HDR_LEN + frag_len;
113 if (total > out_cap)
114 return 0;
115 out[0] = msg_type;
116 out[1] = (uint8_t)(full_len >> 16);
117 out[2] = (uint8_t)(full_len >> 8);
118 out[3] = (uint8_t)full_len;
119 out[4] = (uint8_t)(msg_seq >> 8);
120 out[5] = (uint8_t)msg_seq;
121 out[6] = (uint8_t)(frag_offset >> 16);
122 out[7] = (uint8_t)(frag_offset >> 8);
123 out[8] = (uint8_t)frag_offset;
124 out[9] = (uint8_t)(frag_len >> 16);
125 out[10] = (uint8_t)(frag_len >> 8);
126 out[11] = (uint8_t)frag_len;
127 if (frag_len)
128 memcpy(out + DTLS_HS_HDR_LEN, frag, frag_len);
129 return total;
130}
131
132// ---------------------------------------------------------------------------
133// Message reassembly (RFC 9147 §5.4)
134// ---------------------------------------------------------------------------
135
136void dtls_hs_reasm_init(DtlsHsReasm *r, uint16_t msg_seq, uint8_t *buf, size_t buf_cap)
137{
138 r->active = false;
139 r->have_len = false;
140 r->msg_type = 0;
141 r->msg_seq = msg_seq;
142 r->length = 0;
143 r->buf = buf;
144 r->buf_cap = buf_cap;
145 r->range_count = 0;
146}
147
148int dtls_hs_reasm_add(DtlsHsReasm *r, const DtlsHsHeader *frag)
149{
150 if (frag->msg_seq != r->msg_seq)
151 return 0; // a different message; the state machine decides what to do with it
152 if (!r->have_len)
153 {
154 if (frag->length > r->buf_cap)
155 return -1; // message will not fit the reassembly buffer
156 r->length = frag->length;
157 r->msg_type = frag->msg_type;
158 r->have_len = true;
159 r->active = true;
160 }
161 else if (frag->length != r->length)
162 {
163 return -1; // fragments of one message must agree on its total length
164 }
165 uint32_t lo = frag->frag_offset;
166 uint32_t hi = frag->frag_offset + frag->frag_length;
167 if (hi > r->length)
168 return -1;
169 if (r->length == 0)
170 return 1; // empty body: complete as soon as the header is seen
171 if (frag->frag_length == 0)
172 return 0; // empty fragment of a non-empty message contributes nothing
173 memcpy(r->buf + lo, frag->fragment, frag->frag_length);
174 if (reasm_merge(r, lo, hi) < 0)
175 return -1;
176 if (r->range_count == 1 && r->range_lo[0] == 0 && r->range_hi[0] >= r->length)
177 return 1;
178 return 0;
179}
180
181// ---------------------------------------------------------------------------
182// ACK message (RFC 9147 §7)
183// ---------------------------------------------------------------------------
184
185size_t dtls_ack_build(const DtlsRecordNumber *nums, size_t count, uint8_t *out, size_t out_cap)
186{
187 size_t list_len = count * 16;
188 if (list_len > 0xFFFF)
189 return 0;
190 size_t total = 2 + list_len;
191 if (total > out_cap)
192 return 0;
193 out[0] = (uint8_t)(list_len >> 8);
194 out[1] = (uint8_t)list_len;
195 size_t o = 2;
196 for (size_t i = 0; i < count; i++)
197 {
198 put_u64(out + o, nums[i].epoch);
199 put_u64(out + o + 8, nums[i].seq);
200 o += 16;
201 }
202 return total;
203}
204
205bool dtls_ack_parse(const uint8_t *body, size_t len, DtlsRecordNumber *out, size_t out_cap, size_t *out_count)
206{
207 if (len < 2)
208 return false;
209 size_t list_len = ((size_t)body[0] << 8) | body[1];
210 if (list_len % 16 != 0 || 2 + list_len != len)
211 return false;
212 size_t n = list_len / 16;
213 if (n > out_cap)
214 return false;
215 size_t o = 2;
216 for (size_t i = 0; i < n; i++)
217 {
218 out[i].epoch = get_u64(body + o);
219 out[i].seq = get_u64(body + o + 8);
220 o += 16;
221 }
222 *out_count = n;
223 return true;
224}
225
226// ---------------------------------------------------------------------------
227// HelloRetryRequest cookie (RFC 9147 §5.1)
228// ---------------------------------------------------------------------------
229
230size_t dtls_cookie_make(const uint8_t hmac_key[32], uint64_t timestamp, const uint8_t *payload, size_t payload_len,
231 const uint8_t *client_addr, size_t addr_len, uint8_t *out, size_t out_cap)
232{
233 if (payload_len > 0xFFFF)
234 return 0;
235 size_t body = 1 + 8 + 2 + payload_len; // version || timestamp || payload_len || payload
236 size_t total = body + SSH_HMAC_SHA256_LEN;
237 if (total > out_cap || total > DTLS_COOKIE_MAX)
238 return 0;
239 out[0] = 1; // cookie format version
240 put_u64(out + 1, timestamp);
241 out[9] = (uint8_t)(payload_len >> 8);
242 out[10] = (uint8_t)payload_len;
243 if (payload_len)
244 memcpy(out + 11, payload, payload_len);
245 // MAC covers version || timestamp || client_addr || payload_len || payload: the address is
246 // authenticated (so a cookie cannot be replayed from another peer) without being stored.
247 SshHmacCtx h;
248 ssh_hmac_sha256_init(&h, hmac_key, 32);
249 ssh_hmac_sha256_update(&h, out, 9);
250 ssh_hmac_sha256_update(&h, client_addr, addr_len);
251 ssh_hmac_sha256_update(&h, out + 9, 2 + payload_len);
252 ssh_hmac_sha256_final(&h, out + body);
253 return total;
254}
255
256bool dtls_cookie_verify(const uint8_t hmac_key[32], uint64_t now, uint64_t max_age, const uint8_t *client_addr,
257 size_t addr_len, const uint8_t *cookie, size_t cookie_len, uint8_t *payload_out,
258 size_t payload_cap, size_t *payload_len_out)
259{
260 if (cookie_len < 1 + 8 + 2 + SSH_HMAC_SHA256_LEN || cookie[0] != 1)
261 return false;
262 size_t payload_len = ((size_t)cookie[9] << 8) | cookie[10];
263 size_t body = 1 + 8 + 2 + payload_len;
264 if (body + SSH_HMAC_SHA256_LEN != cookie_len) // exact-length: bounds payload before it is read
265 return false;
266 if (payload_len > payload_cap)
267 return false;
268 uint8_t mac[SSH_HMAC_SHA256_LEN];
269 SshHmacCtx h;
270 ssh_hmac_sha256_init(&h, hmac_key, 32);
271 ssh_hmac_sha256_update(&h, cookie, 9);
272 ssh_hmac_sha256_update(&h, client_addr, addr_len);
273 ssh_hmac_sha256_update(&h, cookie + 9, 2 + payload_len);
274 ssh_hmac_sha256_final(&h, mac);
275 if (!ct_equal(mac, cookie + body, SSH_HMAC_SHA256_LEN))
276 return false;
277 if (max_age != 0)
278 {
279 uint64_t ts = get_u64(cookie + 1);
280 if (ts > now || now - ts > max_age)
281 return false; // future-dated or stale
282 }
283 if (payload_len)
284 memcpy(payload_out, cookie + 11, payload_len);
285 *payload_len_out = payload_len;
286 return true;
287}
288
289#endif // DETWS_ENABLE_DTLS
DTLS 1.3 handshake framing and reliability (RFC 9147 §5, §7).
void ssh_hmac_sha256_final(SshHmacCtx *ctx, uint8_t mac[SSH_HMAC_SHA256_LEN])
Finalize and write the 32-byte MAC.
void ssh_hmac_sha256_init(SshHmacCtx *ctx, const uint8_t *key, size_t key_len)
Initialize a streaming HMAC-SHA2-256 context.
void ssh_hmac_sha256_update(SshHmacCtx *ctx, const uint8_t *data, size_t len)
Feed len bytes into the running HMAC.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
#define SSH_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.
Streaming HMAC-SHA2-256 context.