ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 pc_dtls_handshake.cpp
6 * @brief DTLS 1.3 handshake framing and reliability (RFC 9147 §5, §7). See pc_dtls_handshake.h.
7 */
8
10
11#if PC_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 {
22 p[i] = (uint8_t)(v >> (8 * (7 - i)));
23 }
24}
25
26uint64_t get_u64(const uint8_t *p)
27{
28 uint64_t v = 0;
29 for (int i = 0; i < 8; i++)
30 {
31 v = (v << 8) | p[i];
32 }
33 return v;
34}
35
36// Constant-time equality: no early exit, so a forged cookie MAC leaks nothing through timing.
37bool ct_equal(const uint8_t *a, const uint8_t *b, size_t len)
38{
39 uint8_t d = 0;
40 for (size_t i = 0; i < len; i++)
41 {
42 d |= (uint8_t)(a[i] ^ b[i]);
43 }
44 return d == 0;
45}
46
47// Merge the received byte range [lo, hi) into the reassembler's sorted, disjoint interval list,
48// absorbing any overlapping or adjacent intervals. Returns -1 if there is no room to insert a new
49// disjoint interval (the fragmentation is too scattered for the bounded list).
50int reasm_merge(DtlsHsReasm *r, uint32_t lo, uint32_t hi)
51{
52 uint8_t n = r->range_count;
53 uint8_t i = 0;
54 while (i < n)
55 {
56 // Disjoint (a gap on either side): keep and advance. Touching (hi == lo) merges so the
57 // completion check can collapse contiguous fragments into a single [0, length) interval.
58 if (r->range_hi[i] < lo || r->range_lo[i] > hi)
59 {
60 i++;
61 continue;
62 }
63 if (r->range_lo[i] < lo)
64 {
65 lo = r->range_lo[i];
66 }
67 if (r->range_hi[i] > hi)
68 {
69 hi = r->range_hi[i];
70 }
71 for (uint8_t j = i; j + 1 < n; j++) // remove interval i; re-check the one shifted into its slot
72 {
73 r->range_lo[j] = r->range_lo[j + 1];
74 r->range_hi[j] = r->range_hi[j + 1];
75 }
76 n--;
77 }
78 if (n >= PC_DTLS_HS_REASM_MAX_RANGES)
79 {
80 return -1;
81 }
82 uint8_t k = n; // insert [lo, hi) keeping the list sorted by lo
83 while (k > 0 && r->range_lo[k - 1] > lo)
84 {
85 r->range_lo[k] = r->range_lo[k - 1];
86 r->range_hi[k] = r->range_hi[k - 1];
87 k--;
88 }
89 r->range_lo[k] = lo;
90 r->range_hi[k] = hi;
91 r->range_count = (uint8_t)(n + 1);
92 return 0;
93}
94} // namespace
95
96// ---------------------------------------------------------------------------
97// Handshake message header (RFC 9147 §5.2)
98// ---------------------------------------------------------------------------
99
100size_t pc_dtls_hs_header_parse(const uint8_t *p, size_t len, DtlsHsHeader *out)
101{
102 if (len < PC_DTLS_HS_HDR_LEN)
103 {
104 return 0;
105 }
106 out->msg_type = p[0];
107 out->length = ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
108 out->msg_seq = (uint16_t)(((uint16_t)p[4] << 8) | p[5]);
109 out->frag_offset = ((uint32_t)p[6] << 16) | ((uint32_t)p[7] << 8) | p[8];
110 out->frag_length = ((uint32_t)p[9] << 16) | ((uint32_t)p[10] << 8) | p[11];
111 if (out->frag_offset + out->frag_length > out->length)
112 {
113 return 0; // fragment falls outside the declared message
114 }
115 if (PC_DTLS_HS_HDR_LEN + out->frag_length > len)
116 {
117 return 0; // fragment bytes truncated
118 }
119 out->fragment = p + PC_DTLS_HS_HDR_LEN;
120 return PC_DTLS_HS_HDR_LEN + out->frag_length;
121}
122
123size_t pc_dtls_hs_frag_build(uint8_t msg_type, uint16_t msg_seq, uint32_t full_len, uint32_t frag_offset,
124 const uint8_t *frag, uint32_t frag_len, uint8_t *out, size_t out_cap)
125{
126 if (full_len > 0xFFFFFF || frag_offset > 0xFFFFFF || frag_len > 0xFFFFFF)
127 {
128 return 0; // uint24 fields
129 }
130 if (frag_offset + frag_len > full_len)
131 {
132 return 0;
133 }
134 size_t total = PC_DTLS_HS_HDR_LEN + frag_len;
135 if (total > out_cap)
136 {
137 return 0;
138 }
139 out[0] = msg_type;
140 out[1] = (uint8_t)(full_len >> 16);
141 out[2] = (uint8_t)(full_len >> 8);
142 out[3] = (uint8_t)full_len;
143 out[4] = (uint8_t)(msg_seq >> 8);
144 out[5] = (uint8_t)msg_seq;
145 out[6] = (uint8_t)(frag_offset >> 16);
146 out[7] = (uint8_t)(frag_offset >> 8);
147 out[8] = (uint8_t)frag_offset;
148 out[9] = (uint8_t)(frag_len >> 16);
149 out[10] = (uint8_t)(frag_len >> 8);
150 out[11] = (uint8_t)frag_len;
151 if (frag_len)
152 {
153 memcpy(out + PC_DTLS_HS_HDR_LEN, frag, frag_len);
154 }
155 return total;
156}
157
158// ---------------------------------------------------------------------------
159// Message reassembly (RFC 9147 §5.4)
160// ---------------------------------------------------------------------------
161
162void pc_dtls_hs_reasm_init(DtlsHsReasm *r, uint16_t msg_seq, uint8_t *buf, size_t buf_cap)
163{
164 r->active = false;
165 r->have_len = false;
166 r->msg_type = 0;
167 r->msg_seq = msg_seq;
168 r->length = 0;
169 r->buf = buf;
170 r->buf_cap = buf_cap;
171 r->range_count = 0;
172}
173
174int pc_dtls_hs_reasm_add(DtlsHsReasm *r, const DtlsHsHeader *frag)
175{
176 if (frag->msg_seq != r->msg_seq)
177 {
178 return 0; // a different message; the state machine decides what to do with it
179 }
180 if (!r->have_len)
181 {
182 if (frag->length > r->buf_cap)
183 {
184 return -1; // message will not fit the reassembly buffer
185 }
186 r->length = frag->length;
187 r->msg_type = frag->msg_type;
188 r->have_len = true;
189 r->active = true;
190 }
191 else if (frag->length != r->length)
192 {
193 return -1; // fragments of one message must agree on its total length
194 }
195 uint32_t lo = frag->frag_offset;
196 uint32_t hi = frag->frag_offset + frag->frag_length;
197 if (hi > r->length)
198 {
199 return -1;
200 }
201 if (r->length == 0)
202 {
203 return 1; // empty body: complete as soon as the header is seen
204 }
205 if (frag->frag_length == 0)
206 {
207 return 0; // empty fragment of a non-empty message contributes nothing
208 }
209 memcpy(r->buf + lo, frag->fragment, frag->frag_length);
210 if (reasm_merge(r, lo, hi) < 0)
211 {
212 return -1;
213 }
214 if (r->range_count == 1 && r->range_lo[0] == 0 && r->range_hi[0] >= r->length)
215 {
216 return 1;
217 }
218 return 0;
219}
220
221// ---------------------------------------------------------------------------
222// ACK message (RFC 9147 §7)
223// ---------------------------------------------------------------------------
224
225size_t pc_dtls_ack_build(const DtlsRecordNumber *nums, size_t count, uint8_t *out, size_t out_cap)
226{
227 size_t list_len = count * 16;
228 if (list_len > 0xFFFF)
229 {
230 return 0;
231 }
232 size_t total = 2 + list_len;
233 if (total > out_cap)
234 {
235 return 0;
236 }
237 out[0] = (uint8_t)(list_len >> 8);
238 out[1] = (uint8_t)list_len;
239 size_t o = 2;
240 for (size_t i = 0; i < count; i++)
241 {
242 put_u64(out + o, nums[i].epoch);
243 put_u64(out + o + 8, nums[i].seq);
244 o += 16;
245 }
246 return total;
247}
248
249bool pc_dtls_ack_parse(const uint8_t *body, size_t len, DtlsRecordNumber *out, size_t out_cap, size_t *out_count)
250{
251 if (len < 2)
252 {
253 return false;
254 }
255 size_t list_len = ((size_t)body[0] << 8) | body[1];
256 if (list_len % 16 != 0 || 2 + list_len != len)
257 {
258 return false;
259 }
260 size_t n = list_len / 16;
261 if (n > out_cap)
262 {
263 return false;
264 }
265 size_t o = 2;
266 for (size_t i = 0; i < n; i++)
267 {
268 out[i].epoch = get_u64(body + o);
269 out[i].seq = get_u64(body + o + 8);
270 o += 16;
271 }
272 *out_count = n;
273 return true;
274}
275
276// ---------------------------------------------------------------------------
277// HelloRetryRequest cookie (RFC 9147 §5.1)
278// ---------------------------------------------------------------------------
279
280size_t pc_dtls_cookie_make(const uint8_t pc_hmac_key[32], uint64_t timestamp, const uint8_t *payload,
281 size_t payload_len, const uint8_t *client_addr, size_t addr_len, uint8_t *out,
282 size_t out_cap)
283{
284 if (payload_len > 0xFFFF)
285 {
286 return 0;
287 }
288 size_t body = 1 + 8 + 2 + payload_len; // version || timestamp || payload_len || payload
289 size_t total = body + PC_HMAC_SHA256_LEN;
290 if (total > out_cap || total > PC_DTLS_COOKIE_MAX)
291 {
292 return 0;
293 }
294 out[0] = 1; // cookie format version
295 put_u64(out + 1, timestamp);
296 out[9] = (uint8_t)(payload_len >> 8);
297 out[10] = (uint8_t)payload_len;
298 if (payload_len)
299 {
300 memcpy(out + 11, payload, payload_len);
301 }
302 // MAC covers version || timestamp || client_addr || payload_len || payload: the address is
303 // authenticated (so a cookie cannot be replayed from another peer) without being stored.
305 pc_hmac_sha256_init(&h, pc_hmac_key, 32);
306 pc_hmac_sha256_update(&h, out, 9);
307 pc_hmac_sha256_update(&h, client_addr, addr_len);
308 pc_hmac_sha256_update(&h, out + 9, 2 + payload_len);
309 pc_hmac_sha256_final(&h, out + body);
310 return total;
311}
312
313bool pc_dtls_cookie_verify(const uint8_t pc_hmac_key[32], uint64_t now, uint64_t max_age, const uint8_t *client_addr,
314 size_t addr_len, const uint8_t *cookie, size_t cookie_len, uint8_t *payload_out,
315 size_t payload_cap, size_t *payload_len_out)
316{
317 if (cookie_len < 1 + 8 + 2 + PC_HMAC_SHA256_LEN || cookie[0] != 1)
318 {
319 return false;
320 }
321 size_t payload_len = ((size_t)cookie[9] << 8) | cookie[10];
322 size_t body = 1 + 8 + 2 + payload_len;
323 if (body + PC_HMAC_SHA256_LEN != cookie_len) // exact-length: bounds payload before it is read
324 {
325 return false;
326 }
327 if (payload_len > payload_cap)
328 {
329 return false;
330 }
331 uint8_t mac[PC_HMAC_SHA256_LEN];
333 pc_hmac_sha256_init(&h, pc_hmac_key, 32);
334 pc_hmac_sha256_update(&h, cookie, 9);
335 pc_hmac_sha256_update(&h, client_addr, addr_len);
336 pc_hmac_sha256_update(&h, cookie + 9, 2 + payload_len);
337 pc_hmac_sha256_final(&h, mac);
338 if (!ct_equal(mac, cookie + body, PC_HMAC_SHA256_LEN))
339 {
340 return false;
341 }
342 if (max_age != 0)
343 {
344 uint64_t ts = get_u64(cookie + 1);
345 if (ts > now || now - ts > max_age)
346 {
347 return false; // future-dated or stale
348 }
349 }
350 if (payload_len)
351 {
352 memcpy(payload_out, cookie + 11, payload_len);
353 }
354 *payload_len_out = payload_len;
355 return true;
356}
357
358#endif // PC_ENABLE_DTLS
void pc_hmac_sha256_update(pc_hmac_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes into the running HMAC.
void pc_hmac_sha256_init(pc_hmac_sha256_ctx *ctx, const uint8_t *key, size_t key_len)
Initialize a streaming HMAC-SHA2-256 context.
void pc_hmac_sha256_final(pc_hmac_sha256_ctx *ctx, uint8_t mac[PC_HMAC_SHA256_LEN])
Finalize and write the 32-byte MAC.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
#define PC_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.
Definition hmac_sha256.h:30
Streaming HMAC-SHA2-256 context.
Definition hmac_sha256.h:57