ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 pc_dtls_conn.cpp
6 * @brief DTLS 1.3 server handshake state machine (RFC 9147 §5-6). See pc_dtls_conn.h.
7 */
8
10
11#if PC_ENABLE_DTLS
12
14#include "crypto/asymmetric/ed25519.h" // pc_ed25519_pubkey for the RFC 7250 RawPublicKey
16#include "services/system/clock.h" // pc_millis() stamps / checks the HelloRetryRequest cookie freshness
17#include <string.h>
18
19namespace
20{
21// TLS alert codes used here (RFC 8446 §6).
22const uint8_t ALERT_UNEXPECTED_MESSAGE = 10;
23const uint8_t ALERT_HANDSHAKE_FAILURE = 40;
24const uint8_t ALERT_DECODE_ERROR = 50;
25const uint8_t ALERT_DECRYPT_ERROR = 51;
26const uint8_t ALERT_PROTOCOL_VERSION = 70;
27const uint8_t ALERT_INTERNAL_ERROR = 80;
28
29// HelloRetryRequest cookie freshness window: the client must echo the cookie within this many
30// milliseconds of it being minted (RFC 9147 §5.1). pc_millis() supplies both timestamps.
31const uint64_t DTLS_HRR_COOKIE_MAX_AGE_MS = 60000;
32
33// The record-layer demux (RFC 9147 §4): a first byte 0b001xxxxx is a DTLSCiphertext unified header.
34bool is_ciphertext(uint8_t b0)
35{
36 return (b0 & 0xE0) == 0x20;
37}
38
39// On-wire length of a DTLSCiphertext record from its (plaintext) header, so a datagram carrying more
40// than one record can be walked. Mirrors the header flags pc_dtls_ciphertext_protect writes. @p cid_len is
41// our negotiated connection id length (the CID is not length-prefixed on the wire, RFC 9146), 0 if none.
42size_t ciphertext_record_len(const uint8_t *rec, size_t avail, size_t cid_len)
43{
44 if (avail < 1) // GCOVR_EXCL_LINE the only caller (process_ciphertext_record) passes len - *off with
45 {
46 return 0; // GCOVR_EXCL_LINE *off < len, so avail is always at least 1
47 }
48 uint8_t b0 = rec[0];
49 size_t off = 1;
50 if (b0 & 0x10) // C bit: connection id present, cid_len bytes (known only from negotiation)
51 {
52 off += cid_len;
53 }
54 off += (b0 & 0x08) ? 2 : 1; // S bit: 16- vs 8-bit sequence number
55 if (b0 & 0x04) // L bit: explicit length present
56 {
57 if (off + 2 > avail)
58 {
59 return 0;
60 }
61 size_t enc = ((size_t)rec[off] << 8) | rec[off + 1];
62 off += 2;
63 if (off + enc > avail)
64 {
65 return 0;
66 }
67 return off + enc;
68 }
69 return avail; // no length -> record runs to the end of the datagram
70}
71
72int fail(DtlsConn *c, uint8_t alert)
73{
74 c->state = DtlsConnState::FAILED;
75 c->alert = alert;
76 return -1;
77}
78
79// Finalize a copy of the running transcript without disturbing it (RFC 8446 intermediate hashes).
80void snapshot(const pc_sha256_ctx *ctx, uint8_t out[PC_SHA256_DIGEST_LEN])
81{
82 pc_sha256_ctx copy = *ctx;
83 pc_sha256_final(&copy, out);
84}
85
86// Begin a new outbound flight (RFC 9147 §5.8): drop whatever was buffered for the previous one.
87void flight_reset(DtlsConn *c)
88{
89 c->flight_count = 0;
90 c->flight_len = 0;
91}
92
93// Append one TLS handshake message (@p tls_msg, 4-byte TLS header + body) to the current flight: wrap
94// it in a DTLS handshake header (message_seq from the running counter, so an optional HelloRetryRequest
95// shifts every later message up by one) and buffer the fragment for (re)transmission. @p epoch is 0
96// (DTLSPlaintext) or 2 (DTLSCiphertext). Records are not built here - that happens in flight_transmit,
97// so a retransmission can use fresh record sequence numbers.
98bool flight_add(DtlsConn *c, uint16_t epoch, const uint8_t *tls_msg, size_t tls_len)
99{
100 // The three guards below are defensive and unreachable from any input the server accepts, which is
101 // why they (and every `if (!flight_add(...))` at the call sites) carry coverage exclusions:
102 // - tls_len < 4: every caller passes a builder's output, and a TLS handshake message is never
103 // shorter than its own 4-byte header;
104 // - flight_count >= PC_DTLS_FLIGHT_MSGS: a flight is at most 5 messages (ServerHello, EncryptedExtensions,
105 // Certificate, CertificateVerify, Finished) against PC_DTLS_FLIGHT_MSGS = 6, and a HelloRetryRequest
106 // flight is a single message after flight_reset;
107 // - !flen: only Certificate can approach PC_DTLS_CONN_MSG_CAP, and PC_DTLS_FLIGHT_CAP is defined as
108 // PC_DTLS_CONN_MSG_CAP + 512 - more than the ~300 bytes the other four fragments occupy.
109 if (tls_len < 4 || c->flight_count >= PC_DTLS_FLIGHT_MSGS) // GCOVR_EXCL_LINE
110 {
111 return false; // GCOVR_EXCL_LINE
112 }
113 uint8_t msg_type = tls_msg[0];
114 uint32_t body_len = (uint32_t)(tls_len - 4);
115 uint16_t msg_seq = c->tx_msg_seq++;
116 size_t flen = pc_dtls_hs_frag_build(msg_type, msg_seq, body_len, 0, tls_msg + 4, body_len,
117 c->flight_buf + c->flight_len, sizeof(c->flight_buf) - c->flight_len);
118 if (!flen) // GCOVR_EXCL_LINE
119 {
120 return false; // GCOVR_EXCL_LINE
121 }
122 c->flight_msgs[c->flight_count].off = c->flight_len;
123 c->flight_msgs[c->flight_count].len = (uint16_t)flen;
124 c->flight_msgs[c->flight_count].epoch = (uint8_t)epoch;
125 c->flight_count++;
126 c->flight_len = (uint16_t)(c->flight_len + flen);
127 return true;
128}
129
130// Protect the buffered flight into @p out with FRESH record sequence numbers (RFC 9147 §5.8: a
131// retransmission MUST use new sequence numbers - reusing one would repeat an AEAD nonce and be dropped
132// by the peer's replay window). Records the record number of each message's transmission for ACK
133// matching. Used for both the initial send and every retransmission.
134bool flight_transmit(DtlsConn *c, uint8_t *out, size_t out_cap, size_t *out_len)
135{
136 for (uint8_t i = 0; i < c->flight_count; i++)
137 {
138 const uint8_t *frag = c->flight_buf + c->flight_msgs[i].off;
139 size_t flen = c->flight_msgs[i].len;
140 uint8_t epoch = c->flight_msgs[i].epoch;
141 uint64_t seq;
142 size_t rn;
143 if (epoch == 0)
144 {
145 seq = c->tx_seq_ep0++;
146 rn = pc_dtls_plaintext_build(PC_DTLS_CT_HANDSHAKE, 0, seq, frag, flen, out + *out_len, out_cap - *out_len);
147 }
148 else
149 {
150 seq = c->tx_seq_ep2++;
151 rn = pc_dtls_ciphertext_protect(c->ep2_srv, seq, PC_DTLS_CT_HANDSHAKE, frag, flen, out + *out_len,
152 out_cap - *out_len, c->cid_negotiated ? c->peer_cid : nullptr,
153 c->cid_negotiated ? c->peer_cid_len : 0);
154 }
155 if (!rn)
156 {
157 return false;
158 }
159 *out_len += rn;
160 c->flight_rec[i].epoch = epoch;
161 c->flight_rec[i].seq = seq;
162 }
163 return true;
164}
165
166// Arm the retransmission timer after (re)sending a flight that expects a peer reply (RFC 9147 §5.8).
167void flight_arm(DtlsConn *c)
168{
169 c->awaiting_reply = true;
170 c->retransmits = 0;
171 c->pto_ms = PC_DTLS_PTO_INITIAL_MS;
172 c->flight_sent_ms = pc_millis();
173}
174
175// Stop the retransmission timer: the expected reply arrived, or the flight was acknowledged.
176void flight_disarm(DtlsConn *c)
177{
178 c->awaiting_reply = false;
179}
180
181// Emit a HelloRetryRequest (RFC 9147 §5.1, RFC 8446 §4.1.4) asking the client to retry with an
182// X25519 key_share, binding a stateless return-routability cookie to the peer address. Per RFC 8446
183// §4.4.1 the transcript is restarted as the synthetic message_hash(ClientHello1) before the HRR is
184// folded in, so the eventual transcript is message_hash || HRR || ClientHello2 || ServerHello || ...
185int send_hello_retry(DtlsConn *c, const Tls13ClientHello *ch, const uint8_t *ch1, size_t ch1_len, uint8_t *out,
186 size_t out_cap, size_t *out_len)
187{
188 uint8_t ch1_hash[PC_SHA256_DIGEST_LEN];
190 pc_sha256_init(&h);
191 pc_sha256_update(&h, ch1, ch1_len);
192 pc_sha256_final(&h, ch1_hash);
193
194 pc_sha256_init(&c->transcript); // restart: message_hash(Hash(CH1)) replaces ClientHello1
195 size_t n = pc_tls13_build_message_hash(c->msgbuf, sizeof(c->msgbuf), ch1_hash);
196 if (!n) // GCOVR_EXCL_LINE a message_hash is 36 bytes and msgbuf is
197 {
198 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE PC_DTLS_CONN_MSG_CAP (1024): it always fits
199 }
200 pc_sha256_update(&c->transcript, c->msgbuf, n); // transcript only; message_hash is never sent
201
202 // Stateless cookie with an empty payload: this connection keeps its own transcript across the
203 // retry, so the cookie only has to prove return-routability and bind the client address.
204 uint8_t cookie[PC_DTLS_COOKIE_MAX];
205 size_t clen = pc_dtls_cookie_make(c->cfg.cookie_key, pc_millis(), nullptr, 0, c->peer_addr, c->peer_addr_len,
206 cookie, sizeof(cookie));
207 if (!clen) // GCOVR_EXCL_LINE an empty-payload cookie is 43 bytes against
208 {
209 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE cookie[PC_DTLS_COOKIE_MAX] (128): it always fits
210 }
211
212 n = pc_tls13_build_hello_retry_request(c->msgbuf, sizeof(c->msgbuf), ch->session_id, ch->session_id_len,
213 TLS_GROUP_X25519, cookie, clen, /*dtls=*/true);
214 if (!n) // GCOVR_EXCL_LINE an HRR with a 43-byte cookie is ~140 bytes
215 {
216 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE against msgbuf's 1024: it always fits
217 }
218 pc_sha256_update(&c->transcript, c->msgbuf, n);
219 flight_reset(c);
220 // GCOVR_EXCL_LINE below: flight_add cannot fail here (see its guards) - only the flight_transmit arm
221 // is reachable, and it is covered by a too-small output buffer.
222 if (!flight_add(c, 0, c->msgbuf, n) || !flight_transmit(c, out, out_cap, out_len)) // GCOVR_EXCL_LINE
223 {
224 return fail(c, ALERT_INTERNAL_ERROR);
225 }
226 flight_arm(c); // await ClientHello2
227 c->hrr_sent = true;
228 return 0;
229}
230
231// After a HelloRetryRequest, the retry ClientHello must echo a valid cookie (proving the client's
232// address) before we spend the handshake's asymmetric crypto (RFC 9147 §5.1). No HRR -> nothing to check.
233bool pc_dtls_hrr_cookie_ok(const DtlsConn *c, const Tls13ClientHello *ch)
234{
235 if (!c->hrr_sent)
236 {
237 return true;
238 }
239 uint8_t payload[1];
240 size_t plen = 0;
241 return ch->cookie &&
242 pc_dtls_cookie_verify(c->cfg.cookie_key, pc_millis(), DTLS_HRR_COOKIE_MAX_AGE_MS, c->peer_addr,
243 c->peer_addr_len, ch->cookie, ch->cookie_len, payload, sizeof(payload), &plen);
244}
245
246// Connection-id negotiation (RFC 9146 / RFC 9147 §9): if the client offered a CID we can hold, store it
247// (placed in records we send it) and choose our own CID from the fresh ServerHello random (unique per
248// connection) for the records the client sends us.
249void pc_dtls_negotiate_conn_id(DtlsConn *c, const Tls13ClientHello *ch)
250{
251 if (!ch->has_conn_id || ch->conn_id_len > PC_DTLS_CID_MAX)
252 {
253 return;
254 }
255 c->cid_negotiated = true;
256 c->peer_cid_len = (uint8_t)ch->conn_id_len;
257 if (ch->conn_id_len)
258 {
259 memcpy(c->peer_cid, ch->conn_id, ch->conn_id_len);
260 }
261 c->local_cid_len = PC_DTLS_CONN_LOCAL_CID_LEN;
262 memcpy(c->local_cid, c->cfg.server_random, PC_DTLS_CONN_LOCAL_CID_LEN);
263}
264
265// Consume a ClientHello and emit the whole server flight (ServerHello + the epoch-2 encrypted
266// messages), installing handshake and application keys. Mirrors pc_quic_tls process_client_hello. If the
267// client did not offer an X25519 key_share, this instead sends a HelloRetryRequest and returns to wait
268// for the client's second ClientHello (RFC 9147 §5.1).
269int handle_client_hello(DtlsConn *c, const uint8_t *msg, size_t msg_len, uint8_t *out, size_t out_cap, size_t *out_len)
270{
271 Tls13ClientHello ch;
272 if (!pc_tls13_parse_client_hello(msg, msg_len, &ch, /*dtls=*/true))
273 {
274 return fail(c, ALERT_DECODE_ERROR);
275 }
276 if (!ch.offers_tls13)
277 {
278 return fail(c, ALERT_PROTOCOL_VERSION);
279 }
280 if (!ch.offers_ed25519 || !ch.offers_x25519)
281 {
282 return fail(c, ALERT_HANDSHAKE_FAILURE);
283 }
284
285 uint16_t ch_seq = c->reasm.msg_seq; // the message_seq this ClientHello arrived as
286
287 // Group negotiation (RFC 8446 §4.1.4): the client offered X25519 but sent no X25519 key_share.
288 // Answer with a HelloRetryRequest and await the retry - but only once (a retry that still lacks
289 // the share is fatal, so a malicious client cannot loop us).
290 if (!ch.has_key_share)
291 {
292 if (c->hrr_sent)
293 {
294 return fail(c, ALERT_HANDSHAKE_FAILURE);
295 }
296 if (send_hello_retry(c, &ch, msg, msg_len, out, out_cap, out_len) < 0)
297 {
298 return -1;
299 }
300 c->next_recv_msg_seq = (uint16_t)(ch_seq + 1);
301 pc_dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4, PC_DTLS_CONN_REASM_CAP);
302 return 0;
303 }
304
305 // A key_share is present. If it followed our HelloRetryRequest, the client must echo the cookie,
306 // authenticating its address before we spend the handshake's asymmetric crypto (§5.1).
307 if (!pc_dtls_hrr_cookie_ok(c, &ch))
308 {
309 return fail(c, ALERT_HANDSHAKE_FAILURE);
310 }
311
312 pc_dtls_negotiate_conn_id(c, &ch);
313
314 // X25519 shared secret and the server's key_share.
315 uint8_t ecdhe[32];
316 uint8_t server_share[32];
317 pc_x25519(ecdhe, c->cfg.ephemeral_priv, ch.client_x25519);
318 pc_x25519_base(server_share, c->cfg.ephemeral_priv);
319
320 pc_sha256_update(&c->transcript, msg, msg_len); // transcript: ClientHello (CH2 when an HRR preceded it)
321
322 flight_reset(c); // this ClientHello starts a fresh server flight (ServerHello..Finished)
323
324 // ServerHello (epoch 0, plaintext).
325 size_t n = pc_tls13_build_server_hello(c->msgbuf, sizeof(c->msgbuf), c->cfg.server_random, ch.session_id,
326 ch.session_id_len, server_share, 32, TLS_GROUP_X25519, /*dtls=*/true,
327 c->cid_negotiated ? c->local_cid : nullptr,
328 c->cid_negotiated ? c->local_cid_len : 0);
329 if (!n) // GCOVR_EXCL_LINE a ServerHello is ~130 bytes against msgbuf's
330 {
331 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE 1024: it always fits
332 }
333 pc_sha256_update(&c->transcript, c->msgbuf, n);
334 if (!flight_add(c, 0, c->msgbuf, n)) // GCOVR_EXCL_LINE flight_add cannot fail (see its guards):
335 {
336 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE this is the first message of a reset flight
337 }
338
339 // Handshake-traffic keys from Transcript-Hash(..ServerHello).
340 uint8_t hash[PC_SHA256_DIGEST_LEN];
341 snapshot(&c->transcript, hash);
342 pc_tls13_ks_early(&DTLS13_KDF, &c->ks);
343 pc_tls13_ks_handshake(&c->ks, ecdhe, hash, 32);
344 pc_dtls_record_keys_derive(&c->ep2_srv, DtlsCipher::AES_128_GCM_SHA256, 2, c->ks.server_hs_traffic);
345 pc_dtls_record_keys_derive(&c->ep2_cli, DtlsCipher::AES_128_GCM_SHA256, 2, c->ks.client_hs_traffic);
346 c->ep2_ready = true;
347
348 // Raw Public Key negotiation (RFC 7250): if the client offered server_certificate_type = RawPublicKey,
349 // answer with that type in EncryptedExtensions and send our Ed25519 SubjectPublicKeyInfo as the
350 // Certificate instead of the X.509 chain. Additive - a client that does not ask still gets X.509.
351 bool rpk = false;
352#if PC_ENABLE_TLS_RPK
353 rpk = ch.offers_rpk_server_cert;
354#endif
355
356 // EncryptedExtensions.
357 n = pc_tls13_build_encrypted_extensions_empty(c->msgbuf, sizeof(c->msgbuf), rpk);
358 pc_sha256_update(&c->transcript, c->msgbuf, n);
359 if (!flight_add(c, 2, c->msgbuf, n)) // GCOVR_EXCL_LINE flight_add cannot fail (see its guards):
360 {
361 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE the flight never fills PC_DTLS_FLIGHT_CAP
362 }
363
364 // Certificate (X.509 chain, or the RFC 7250 RawPublicKey when negotiated).
365#if PC_ENABLE_TLS_RPK
366 if (rpk)
367 {
368 uint8_t ed_pub[PC_ED25519_PUBKEY_LEN];
369 pc_ed25519_pubkey(ed_pub, c->cfg.ed25519_seed);
370 n = pc_tls13_build_certificate_rpk(c->msgbuf, sizeof(c->msgbuf), ed_pub);
371 }
372 else
373#endif
374 n = pc_tls13_build_certificate(c->msgbuf, sizeof(c->msgbuf), c->cfg.cert_der, c->cfg.cert_len);
375 if (!n)
376 {
377 return fail(c, ALERT_INTERNAL_ERROR);
378 }
379 pc_sha256_update(&c->transcript, c->msgbuf, n);
380 if (!flight_add(c, 2, c->msgbuf, n)) // GCOVR_EXCL_LINE flight_add cannot fail (see its guards): the
381 {
382 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE Certificate is capped by msgbuf, which fits
383 }
384
385 // CertificateVerify signs Transcript-Hash(..Certificate).
386 snapshot(&c->transcript, hash);
387 n = pc_tls13_build_cert_verify(c->msgbuf, sizeof(c->msgbuf), hash, c->cfg.ed25519_seed);
388 if (!n) // GCOVR_EXCL_LINE a CertificateVerify is 72 bytes against
389 {
390 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE msgbuf's 1024: it always fits
391 }
392 pc_sha256_update(&c->transcript, c->msgbuf, n);
393 if (!flight_add(c, 2, c->msgbuf, n)) // GCOVR_EXCL_LINE flight_add cannot fail (see its guards):
394 {
395 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE the flight never fills PC_DTLS_FLIGHT_CAP
396 }
397
398 // Server Finished over Transcript-Hash(..CertificateVerify).
399 snapshot(&c->transcript, hash);
400 uint8_t verify[PC_SHA256_DIGEST_LEN];
401 pc_tls13_finished_mac(&DTLS13_KDF, c->ks.server_hs_traffic, hash, verify);
402 n = pc_tls13_build_finished(c->msgbuf, sizeof(c->msgbuf), verify);
403 pc_sha256_update(&c->transcript, c->msgbuf, n);
404 if (!flight_add(c, 2, c->msgbuf, n)) // GCOVR_EXCL_LINE flight_add cannot fail (see its guards):
405 {
406 return fail(c, ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE the flight never fills PC_DTLS_FLIGHT_CAP
407 }
408
409 // Application-traffic keys from Transcript-Hash(..server Finished); this hash also verifies the
410 // client's Finished.
411 snapshot(&c->transcript, c->hs_finished_hash);
412 pc_tls13_ks_master(&c->ks, c->hs_finished_hash);
413 pc_dtls_record_keys_derive(&c->ep3_srv, DtlsCipher::AES_128_GCM_SHA256, 3, c->ks.server_ap_traffic);
414 pc_dtls_record_keys_derive(&c->ep3_cli, DtlsCipher::AES_128_GCM_SHA256, 3, c->ks.client_ap_traffic);
415 c->ep3_ready = true;
416
417 if (!flight_transmit(c, out, out_cap, out_len)) // protect the whole flight now that ep2 keys exist
418 {
419 return fail(c, ALERT_INTERNAL_ERROR);
420 }
421 flight_arm(c); // await the client Finished
422 c->state = DtlsConnState::WAIT_FINISHED;
423 c->next_recv_msg_seq = (uint16_t)(ch_seq + 1);
424 pc_dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4, PC_DTLS_CONN_REASM_CAP);
425 return 0;
426}
427
428// Verify the client's Finished and complete the handshake.
429int handle_client_finished(DtlsConn *c, const uint8_t *msg, size_t msg_len)
430{
431 if (msg[0] != TlsHs::TLS_HS_FINISHED || msg_len != 4 + PC_SHA256_DIGEST_LEN) // GCOVR_EXCL_LINE dispatch_message
432 {
433 return fail(c, ALERT_DECODE_ERROR); // only routes a Finished here, so the type arm cannot be taken
434 }
435 uint8_t expected[PC_SHA256_DIGEST_LEN];
436 pc_tls13_finished_mac(&DTLS13_KDF, c->ks.client_hs_traffic, c->hs_finished_hash, expected);
437 uint8_t diff = 0;
438 for (int i = 0; i < PC_SHA256_DIGEST_LEN; i++)
439 {
440 diff |= (uint8_t)(expected[i] ^ msg[4 + i]);
441 }
442 if (diff)
443 {
444 return fail(c, ALERT_DECRYPT_ERROR);
445 }
446 pc_sha256_update(&c->transcript, msg, msg_len);
447 c->state = DtlsConnState::DONE;
448 flight_disarm(c); // the reply arrived; stop retransmitting the server flight
449 // Re-arm the reassembler for the same message_seq so a retransmitted Finished (its ACK was lost)
450 // completes again and we re-acknowledge it, instead of being rejected as unexpected (RFC 9147 §5.8.3).
451 pc_dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4, PC_DTLS_CONN_REASM_CAP);
452 return 0;
453}
454
455int dispatch_message(DtlsConn *c, const uint8_t *tls_msg, size_t tls_len, uint8_t *out, size_t out_cap, size_t *out_len)
456{
457 if (c->state == DtlsConnState::START && tls_msg[0] == TlsHs::TLS_HS_CLIENT_HELLO)
458 {
459 return handle_client_hello(c, tls_msg, tls_len, out, out_cap, out_len);
460 }
461 if (c->state == DtlsConnState::WAIT_FINISHED && tls_msg[0] == TlsHs::TLS_HS_FINISHED)
462 {
463 return handle_client_finished(c, tls_msg, tls_len);
464 }
465 if (c->state == DtlsConnState::DONE && tls_msg[0] == TlsHs::TLS_HS_FINISHED)
466 {
467 c->hs_ack_sent = false; // a retransmitted client Finished (our ACK was lost): re-acknowledge it
468 pc_dtls_hs_reasm_init(&c->reasm, c->next_recv_msg_seq, c->reasm_buf + 4,
469 PC_DTLS_CONN_REASM_CAP); // accept the next one too
470 return 0;
471 }
472 return fail(c, ALERT_UNEXPECTED_MESSAGE);
473}
474
475// Parse and reassemble the DTLS handshake fragments carried in one record's payload, dispatching each
476// complete TLS message.
477int drive_handshake(DtlsConn *c, const uint8_t *payload, size_t plen, uint8_t *out, size_t out_cap, size_t *out_len)
478{
479 size_t p = 0;
480 while (p < plen)
481 {
482 DtlsHsHeader hh;
483 size_t used = pc_dtls_hs_header_parse(payload + p, plen - p, &hh);
484 if (!used)
485 {
486 break;
487 }
488 p += used;
489 int r = pc_dtls_hs_reasm_add(&c->reasm, &hh); // ignores fragments for other message_seqs
490 if (r < 0)
491 {
492 return fail(c, ALERT_DECODE_ERROR);
493 }
494 if (r == 1)
495 {
496 // Rebuild the TLS handshake structure (4-byte header + reassembled body) for the transcript.
497 c->reasm_buf[0] = c->reasm.msg_type;
498 c->reasm_buf[1] = (uint8_t)(c->reasm.length >> 16);
499 c->reasm_buf[2] = (uint8_t)(c->reasm.length >> 8);
500 c->reasm_buf[3] = (uint8_t)c->reasm.length;
501 if (dispatch_message(c, c->reasm_buf, 4 + c->reasm.length, out, out_cap, out_len) < 0)
502 {
503 return -1;
504 }
505 }
506 }
507 return 0;
508}
509
510// A client ACK (RFC 9147 §7) for the outstanding flight: if it acknowledges every message of the last
511// transmission, the peer has the whole flight, so stop retransmitting (§5.8.3). A partial ACK is
512// ignored here - the timer simply retransmits the whole flight, which is always correct.
513void process_ack(DtlsConn *c, const uint8_t *body, size_t len)
514{
515 if (!c->awaiting_reply)
516 {
517 return;
518 }
519 DtlsRecordNumber acked[16];
520 size_t count = 0;
521 if (!pc_dtls_ack_parse(body, len, acked, 16, &count))
522 {
523 return;
524 }
525 for (uint8_t i = 0; i < c->flight_count; i++)
526 {
527 bool found = false;
528 for (size_t j = 0; j < count; j++)
529 {
530 if (acked[j].epoch == c->flight_rec[i].epoch && acked[j].seq == c->flight_rec[i].seq)
531 {
532 found = true;
533 break;
534 }
535 }
536 if (!found)
537 {
538 return; // a flight record is still unacknowledged; keep the timer running
539 }
540 }
541 flight_disarm(c);
542}
543
544// One-record outcome for the pc_dtls_conn_process datagram walk.
545enum class DtlsRecStep
546{
547 NEXT, // record consumed; keep walking the datagram
548 STOP, // malformed/short record; stop walking (leave the rest)
549 FATAL, // fatal error already recorded via fail(); caller returns -1
550};
551
552// Process one ciphertext (epoch-2) record at dgram[*off], advancing *off past a well-formed record.
553DtlsRecStep process_ciphertext_record(DtlsConn *c, const uint8_t *dgram, size_t len, size_t *off, uint8_t *out,
554 size_t out_cap, size_t *out_len)
555{
556 size_t rlen = ciphertext_record_len(dgram + *off, len - *off, c->cid_negotiated ? c->local_cid_len : 0);
557 if (!rlen)
558 {
559 return DtlsRecStep::STOP; // malformed header; stop walking the datagram
560 }
561 if (!c->ep2_ready)
562 {
563 fail(c, ALERT_UNEXPECTED_MESSAGE);
564 return DtlsRecStep::FATAL;
565 }
566 uint8_t inner[PC_DTLS_CONN_REASM_CAP + PC_DTLS_TAG_LEN];
567 DtlsCiphertext info;
568 uint64_t next = c->replay_ep2.seeded ? c->replay_ep2.highest + 1 : 0;
569 if (!pc_dtls_ciphertext_unprotect(c->ep2_cli, next, dgram + *off, rlen, inner, sizeof(inner), &info,
570 c->cid_negotiated ? c->local_cid : nullptr,
571 c->cid_negotiated ? c->local_cid_len : 0))
572 {
573 fail(c, ALERT_DECRYPT_ERROR);
574 return DtlsRecStep::FATAL;
575 }
576 *off += rlen;
577 if (!pc_dtls_replay_check(&c->replay_ep2, info.seq))
578 {
579 return DtlsRecStep::NEXT; // replay: drop, but keep processing the datagram
580 }
581 pc_dtls_replay_mark(&c->replay_ep2, info.seq);
582 bool is_hs = (info.content_type == PC_DTLS_CT_HANDSHAKE);
583 if (is_hs)
584 {
585 c->rx_ep2_seq = info.seq; // the client Finished's record number, for the completion ACK
586 }
587 if (info.content_type == PC_DTLS_CT_ACK)
588 {
589 process_ack(c, inner, info.pt_len); // the client acknowledged our flight
590 }
591 else if (is_hs && drive_handshake(c, inner, info.pt_len, out, out_cap, out_len) < 0)
592 {
593 return DtlsRecStep::FATAL;
594 }
595 return DtlsRecStep::NEXT;
596}
597
598// Process one plaintext (epoch-0) record at dgram[*off], advancing *off past a well-formed record.
599DtlsRecStep process_plaintext_record(DtlsConn *c, const uint8_t *dgram, size_t len, size_t *off, uint8_t *out,
600 size_t out_cap, size_t *out_len)
601{
602 DtlsPlaintext pt;
603 size_t rlen = pc_dtls_plaintext_parse(dgram + *off, len - *off, &pt);
604 if (!rlen)
605 {
606 return DtlsRecStep::STOP;
607 }
608 *off += rlen;
609 if (pt.content_type == PC_DTLS_CT_HANDSHAKE &&
610 drive_handshake(c, pt.fragment, pt.frag_len, out, out_cap, out_len) < 0)
611 {
612 return DtlsRecStep::FATAL;
613 }
614 return DtlsRecStep::NEXT;
615}
616
617// Once the client Finished completes the handshake, acknowledge it so the client stops retransmitting
618// its final flight (RFC 9147 §5.8.3). The ACK is a content-type-26 record in the highest available epoch
619// (3, application), covering the epoch-2 Finished record (§7). Sent at most once.
620void maybe_send_completion_ack(DtlsConn *c, uint8_t *out, size_t out_cap, size_t *out_len)
621{
622 if (!pc_dtls_conn_established(c) || c->hs_ack_sent)
623 {
624 return;
625 }
626 DtlsRecordNumber rn = {2, c->rx_ep2_seq};
627 uint8_t ack_body[2 + 16];
628 size_t bl = pc_dtls_ack_build(&rn, 1, ack_body, sizeof(ack_body));
629 size_t rec = pc_dtls_ciphertext_protect(c->ep3_srv, c->tx_seq_ep3++, PC_DTLS_CT_ACK, ack_body, bl, out + *out_len,
630 out_cap - *out_len, c->cid_negotiated ? c->peer_cid : nullptr,
631 c->cid_negotiated ? c->peer_cid_len : 0);
632 if (rec)
633 {
634 *out_len += rec;
635 c->hs_ack_sent = true;
636 }
637}
638} // namespace
639
640void pc_dtls_conn_init(DtlsConn *c, const DtlsServerConfig *cfg, const uint8_t *peer_addr, size_t peer_addr_len)
641{
642 memset(c, 0, sizeof(*c));
643 c->cfg = *cfg;
644 c->state = DtlsConnState::START;
645 if (peer_addr && peer_addr_len)
646 {
647 if (peer_addr_len > PC_DTLS_PEER_ADDR_MAX)
648 {
649 peer_addr_len = PC_DTLS_PEER_ADDR_MAX;
650 }
651 memcpy(c->peer_addr, peer_addr, peer_addr_len);
652 c->peer_addr_len = (uint8_t)peer_addr_len;
653 }
654 pc_sha256_init(&c->transcript);
655 pc_dtls_replay_init(&c->replay_ep2);
656 pc_dtls_replay_init(&c->replay_ep3);
657 c->next_recv_msg_seq = 0;
658 pc_dtls_hs_reasm_init(&c->reasm, 0, c->reasm_buf + 4, PC_DTLS_CONN_REASM_CAP);
659}
660
661int pc_dtls_conn_process(DtlsConn *c, const uint8_t *dgram, size_t len, uint8_t *out, size_t out_cap)
662{
663 if (c->state == DtlsConnState::FAILED)
664 {
665 return -1;
666 }
667 size_t out_len = 0;
668 size_t off = 0;
669 while (off < len)
670 {
671 DtlsRecStep step = is_ciphertext(dgram[off])
672 ? process_ciphertext_record(c, dgram, len, &off, out, out_cap, &out_len)
673 : process_plaintext_record(c, dgram, len, &off, out, out_cap, &out_len);
674 if (step == DtlsRecStep::FATAL)
675 {
676 return -1;
677 }
678 if (step == DtlsRecStep::STOP)
679 {
680 break;
681 }
682 }
683
684 maybe_send_completion_ack(c, out, out_cap, &out_len);
685 return (int)out_len;
686}
687
688int pc_dtls_conn_timeout_ms(const DtlsConn *c)
689{
690 if (!c->awaiting_reply || c->state == DtlsConnState::FAILED || c->state == DtlsConnState::DONE)
691 {
692 return -1;
693 }
694 // Wrap-safe remaining time: (deadline - now) as a signed delta, clamped at 0 (already due).
695 int32_t remaining = (int32_t)(c->flight_sent_ms + c->pto_ms - pc_millis());
696 return remaining > 0 ? remaining : 0;
697}
698
699int pc_dtls_conn_on_timeout(DtlsConn *c, uint8_t *out, size_t out_cap)
700{
701 if (!c->awaiting_reply || c->state == DtlsConnState::FAILED || c->state == DtlsConnState::DONE)
702 {
703 return 0;
704 }
705 if ((int32_t)(pc_millis() - (c->flight_sent_ms + c->pto_ms)) < 0)
706 {
707 return 0; // not yet due (spurious / early wake-up)
708 }
709 if (c->retransmits >= PC_DTLS_MAX_RETRANSMITS)
710 {
711 // Peer is gone; abandon the handshake. No alert - there is nobody to receive it.
712 c->state = DtlsConnState::FAILED;
713 c->awaiting_reply = false;
714 return -1;
715 }
716 size_t out_len = 0;
717 if (!flight_transmit(c, out, out_cap, &out_len))
718 {
719 return -1;
720 }
721 c->retransmits++;
722 c->pto_ms = c->pto_ms >= PC_DTLS_PTO_MAX_MS / 2 ? PC_DTLS_PTO_MAX_MS : c->pto_ms * 2; // §5.8.1 backoff, capped
723 c->flight_sent_ms = pc_millis();
724 return (int)out_len;
725}
726
727bool pc_dtls_conn_established(const DtlsConn *c)
728{
729 return c->state == DtlsConnState::DONE && c->ep3_ready;
730}
731
732uint8_t pc_dtls_conn_alert(const DtlsConn *c)
733{
734 return c->alert;
735}
736
737DtlsRecordKeys *pc_dtls_conn_app_write_keys(DtlsConn *c)
738{
739 return c->ep3_ready ? &c->ep3_srv : nullptr;
740}
741
742DtlsRecordKeys *pc_dtls_conn_app_read_keys(DtlsConn *c)
743{
744 return c->ep3_ready ? &c->ep3_cli : nullptr;
745}
746
747size_t pc_dtls_conn_local_cid(const DtlsConn *c, uint8_t *out)
748{
749 if (!c->cid_negotiated || c->local_cid_len == 0)
750 {
751 return 0;
752 }
753 memcpy(out, c->local_cid, c->local_cid_len);
754 return c->local_cid_len;
755}
756
757bool pc_dtls_conn_open_app(DtlsConn *c, const uint8_t *rec, size_t rec_len, uint8_t *out, size_t out_cap,
758 size_t *out_len)
759{
760 if (!pc_dtls_conn_established(c))
761 {
762 return false;
763 }
764 DtlsCiphertext info;
765 uint64_t next = c->replay_ep3.seeded ? c->replay_ep3.highest + 1 : 0;
766 if (!pc_dtls_ciphertext_unprotect(c->ep3_cli, next, rec, rec_len, out, out_cap, &info,
767 c->cid_negotiated ? c->local_cid : nullptr,
768 c->cid_negotiated ? c->local_cid_len : 0))
769 {
770 return false;
771 }
772 if (!pc_dtls_replay_check(&c->replay_ep3, info.seq))
773 {
774 return false; // replay or too old
775 }
776 pc_dtls_replay_mark(&c->replay_ep3, info.seq);
777 if (info.content_type != PC_DTLS_CT_APPLICATION_DATA)
778 {
779 return false;
780 }
781 *out_len = info.pt_len;
782 return true;
783}
784
785size_t pc_dtls_conn_seal_app(DtlsConn *c, const uint8_t *data, size_t len, uint8_t *out, size_t out_cap)
786{
787 if (!pc_dtls_conn_established(c))
788 {
789 return 0;
790 }
791 // tx_seq_ep3 is shared with the completion ACK, so app records never reuse its sequence number.
792 return pc_dtls_ciphertext_protect(c->ep3_srv, c->tx_seq_ep3++, PC_DTLS_CT_APPLICATION_DATA, data, len, out, out_cap,
793 c->cid_negotiated ? c->peer_cid : nullptr,
794 c->cid_negotiated ? c->peer_cid_len : 0);
795}
796
797#endif // PC_ENABLE_DTLS
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
void pc_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
void pc_ed25519_pubkey(uint8_t pub[32], const uint8_t seed[32])
Definition ed25519.cpp:584
Ed25519 signatures (RFC 8032) for ssh-ed25519 host keys + client auth.
#define PC_ED25519_PUBKEY_LEN
Ed25519 public key length.
Definition ed25519.h:29
PC_CRYPTO_HOT void pc_sha256_init(pc_sha256_ctx *ctx)
Initialize a streaming SHA-256 context (ctx must not be NULL).
Definition sha256.cpp:29
void pc_sha256_final(pc_sha256_ctx *ctx, uint8_t digest[PC_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest. The context is undefined afterwards; call init() agai...
Definition sha256.cpp:48
void pc_sha256_update(pc_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
Definition sha256.cpp:39
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition sha256.h:25
Streaming SHA-256 context.
Definition sha256.h:40