ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dtls_conn.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 pc_dtls_conn.h
6 * @brief DTLS 1.3 server handshake state machine (RFC 9147 §5-6).
7 *
8 * The transport-neutral core that drives one DTLS 1.3 server handshake: it consumes inbound
9 * datagrams and produces the outbound flight, wiring the reused TLS 1.3 message builders and key
10 * schedule (pc_tls13_msg, pc_tls13_kdf) through the DTLS record layer (pc_dtls_record) and handshake framing
11 * (pc_dtls_handshake). Like pc_coap_server_process it has no sockets - the UDP glue (a later CoAPs
12 * front-end) feeds it datagrams and sends whatever it emits.
13 *
14 * Profile: the single spec-valid suite the whole hand-rolled TLS 1.3 stack uses -
15 * TLS_AES_128_GCM_SHA256, X25519 key exchange, an Ed25519 server certificate. The handshake is the
16 * one-round-trip full handshake (no PSK, no 0-RTT, no client auth):
17 *
18 * epoch 0 ClientHello ->
19 * <- ServerHello (epoch 0, DTLSPlaintext)
20 * epoch 2 <- EncryptedExtensions, Certificate, CertificateVerify, Finished (DTLSCiphertext)
21 * epoch 2 Finished ->
22 * epoch 3 application data (CoAP) protected with the app-traffic keys
23 *
24 * Each handshake message fits one record in this profile. When the client does not offer an X25519
25 * key_share up front, the server answers the first ClientHello with a HelloRetryRequest carrying a
26 * stateless, address-bound cookie and renegotiates the group to X25519 (RFC 9147 §5.1); the second
27 * ClientHello must echo the cookie before any asymmetric crypto is spent. Full ACK/timeout
28 * retransmission (§5.8, §7) beyond the Finished acknowledgement is a follow-on increment; the framing
29 * it needs already exists in pc_dtls_handshake.
30 *
31 * @author Douglas Quigg (dstroy0)
32 * @date 2026
33 */
34
35#ifndef PROTOCORE_DTLS_CONN_H
36#define PROTOCORE_DTLS_CONN_H
37
38#include "protocore_config.h"
39
40#if PC_ENABLE_DTLS
41
42#include "crypto/hash/sha256.h"
46#include <stddef.h>
47#include <stdint.h>
48
49/** @brief Largest inbound handshake message body reassembled (ClientHello / client Finished). */
50#define PC_DTLS_CONN_REASM_CAP 1024
51
52/** @brief Largest single outbound handshake message (Certificate-dominated; one record per message
53 * in this phase, so the certificate plus framing must fit one record). */
54#define PC_DTLS_CONN_MSG_CAP 1024
55
56/** @brief Largest serialized peer address the HelloRetryRequest cookie binds (IPv6 16 + port 2). */
57#define PC_DTLS_PEER_ADDR_MAX 18
58
59/** @brief Length of the connection id the server chooses for itself (RFC 9146 / RFC 9147 §9): the id the
60 * client must place in every record it sends, so the server can route by it across an address
61 * change. 4 bytes is ample per-connection entropy; must be <= @ref PC_DTLS_CID_MAX. */
62#define PC_DTLS_CONN_LOCAL_CID_LEN 4
63
64/** @brief Most handshake messages in one outbound flight (ServerHello + EE + Cert + CV + Finished). */
65#define PC_DTLS_FLIGHT_MSGS 6
66
67/** @brief Buffer for the current flight's DTLS handshake fragments, so it can be retransmitted with
68 * fresh record sequence numbers. Sized for the Certificate-dominated server flight. */
69#define PC_DTLS_FLIGHT_CAP (PC_DTLS_CONN_MSG_CAP + 512)
70
71/** @brief Retransmission timer (RFC 9147 §5.8.1): initial PTO, its cap, and the retransmission ceiling
72 * after which the handshake is abandoned. Times are in the units of @ref pc_millis (ms). */
73#define PC_DTLS_PTO_INITIAL_MS 1000u
74#define PC_DTLS_PTO_MAX_MS 60000u
75#define PC_DTLS_MAX_RETRANSMITS 8
76
77/** @brief One buffered outbound handshake message: where its DTLS fragment sits in @ref DtlsConn.flight_buf
78 * and which epoch protects it. */
79struct DtlsFlightMsg
80{
81 uint16_t off; ///< byte offset of the fragment in flight_buf
82 uint16_t len; ///< fragment length
83 uint8_t epoch; ///< 0 (DTLSPlaintext) or 2 (DTLSCiphertext)
84};
85
86/** @brief Handshake progress. */
87enum class DtlsConnState : uint8_t
88{
89 START, ///< awaiting ClientHello
90 WAIT_FINISHED, ///< server flight sent; awaiting client Finished
91 DONE, ///< handshake complete; application keys installed
92 FAILED ///< fatal error (see @ref pc_dtls_conn_alert)
93};
94
95/**
96 * @brief The server's long-lived identity plus this handshake's fresh randomness.
97 *
98 * @c cert_der / @c ed25519_seed are the server's certificate and matching signing key (long-lived).
99 * @c ephemeral_priv and @c server_random must be freshly generated per connection by the caller
100 * (from a CSPRNG); they are the X25519 ephemeral private key and the ServerHello random.
101 */
102struct DtlsServerConfig
103{
104 const uint8_t *cert_der; ///< Ed25519 leaf certificate, DER
105 size_t cert_len;
106 const uint8_t *ed25519_seed; ///< 32-byte Ed25519 signing seed (matches @c cert_der)
107 const uint8_t *ephemeral_priv; ///< 32-byte X25519 server ephemeral private key (fresh per handshake)
108 const uint8_t *server_random; ///< 32-byte ServerHello random (fresh per handshake)
109 const uint8_t *cookie_key; ///< 32-byte server-wide secret keying the HelloRetryRequest cookie MAC (§5.1)
110};
111
112/** @brief One DTLS 1.3 server handshake. Owns all per-connection state; no heap. */
113struct DtlsConn
114{
115 DtlsServerConfig cfg;
116 DtlsConnState state;
117 uint8_t alert; ///< RFC 8446 §6 alert code when @c state is FAILED (0 otherwise)
118
119 pc_sha256_ctx transcript; ///< running Transcript-Hash over the TLS handshake messages
120 Tls13KeySchedule ks; ///< TLS 1.3 key schedule
121 DtlsRecordKeys ep2_srv; ///< epoch 2 server write keys (handshake traffic)
122 DtlsRecordKeys ep2_cli; ///< epoch 2 client read keys
123 DtlsRecordKeys ep3_srv; ///< epoch 3 server write keys (application traffic)
124 DtlsRecordKeys ep3_cli; ///< epoch 3 client read keys
125 bool ep2_ready; ///< epoch 2 keys installed
126 bool ep3_ready; ///< epoch 3 keys installed
127 uint8_t hs_finished_hash[PC_SHA256_DIGEST_LEN]; ///< Transcript-Hash(CH..server Finished)
128
129 uint64_t tx_seq_ep0; ///< next outbound record sequence number, epoch 0
130 uint64_t tx_seq_ep2; ///< next outbound record sequence number, epoch 2
131 uint64_t tx_seq_ep3; ///< next outbound record sequence number, epoch 3
132 uint16_t tx_msg_seq; ///< next outbound handshake message_seq (advances across an optional HRR)
133 bool hrr_sent; ///< a HelloRetryRequest was sent; the next ClientHello is the retry (§5.1)
134 uint16_t next_recv_msg_seq; ///< handshake message_seq expected next from the client
135 DtlsReplayWindow replay_ep2; ///< anti-replay window for inbound epoch-2 records
136 DtlsReplayWindow replay_ep3; ///< anti-replay window for inbound epoch-3 (application) records
137 uint64_t rx_ep2_seq; ///< sequence number of the last inbound epoch-2 record (the client Finished)
138 bool hs_ack_sent; ///< the client Finished has been acknowledged (RFC 9147 §5.8.3 / §7)
139 uint8_t peer_addr[PC_DTLS_PEER_ADDR_MAX]; ///< serialized peer address the HRR cookie is bound to (§5.1)
140 uint8_t peer_addr_len; ///< bytes of @ref peer_addr in use (0 = no address bound)
141
142 // Connection ids (RFC 9146 / RFC 9147 §9), negotiated by the connection_id extension.
143 bool cid_negotiated; ///< the client offered connection_id and we accepted it
144 uint8_t
145 peer_cid[PC_DTLS_CID_MAX]; ///< the client's CID: placed in every record we send to the client (may be empty)
146 uint8_t peer_cid_len; ///< bytes of @ref peer_cid in use
147 uint8_t local_cid[PC_DTLS_CID_MAX]; ///< the CID we chose: the client places it in every record it sends to us
148 uint8_t local_cid_len; ///< bytes of @ref local_cid in use
149
150 // Retransmission (RFC 9147 §5.8): the current outbound flight, buffered as fragments so it can be
151 // re-sent with fresh record sequence numbers, plus the exponential-backoff timer state.
152 DtlsFlightMsg flight_msgs[PC_DTLS_FLIGHT_MSGS]; ///< the flight's messages (index into @ref flight_buf)
153 DtlsRecordNumber flight_rec[PC_DTLS_FLIGHT_MSGS]; ///< record numbers of each message's last transmission (for ACKs)
154 uint8_t flight_count; ///< messages in the current flight
155 uint16_t flight_len; ///< bytes used in @ref flight_buf
156 bool awaiting_reply; ///< a flight is outstanding and a peer reply is expected (timer runs)
157 uint8_t retransmits; ///< times the current flight has been retransmitted
158 uint32_t pto_ms; ///< current retransmission timeout (doubles each retransmit)
159 uint32_t flight_sent_ms; ///< pc_millis() when the flight was last (re)transmitted
160
161 DtlsHsReasm reasm; ///< inbound handshake reassembler
162 uint8_t reasm_buf[4 + PC_DTLS_CONN_REASM_CAP]; ///< TLS message = 4-byte header [0..3] + body [4..]
163 uint8_t msgbuf[PC_DTLS_CONN_MSG_CAP]; ///< scratch for one outbound TLS message
164 uint8_t flight_buf[PC_DTLS_FLIGHT_CAP]; ///< the current flight's DTLS handshake fragments, for retransmission
165};
166
167/**
168 * @brief Initialize a connection for a new handshake. @p cfg is copied (its pointers must outlive @p c).
169 *
170 * @param peer_addr serialized peer address (e.g. IP || port) the HelloRetryRequest cookie binds,
171 * so a cookie minted for one peer is worthless to another (RFC 9147 §5.1). May
172 * be NULL / 0 when no HRR is expected (the one-round-trip happy path).
173 * @param peer_addr_len length of @p peer_addr; clamped to @ref PC_DTLS_PEER_ADDR_MAX.
174 */
175void pc_dtls_conn_init(DtlsConn *c, const DtlsServerConfig *cfg, const uint8_t *peer_addr, size_t peer_addr_len);
176
177/**
178 * @brief Feed one inbound datagram; append any response records to @p out.
179 *
180 * Demultiplexes the datagram's records (DTLSPlaintext for epoch 0, DTLSCiphertext for epoch 2),
181 * unprotects and reassembles the handshake, and drives the state machine, writing the server's
182 * response flight to @p out.
183 *
184 * @return the number of bytes written to @p out (0 if nothing to send), or -1 on a fatal error
185 * (then @c state is FAILED and @ref pc_dtls_conn_alert gives the reason).
186 */
187int pc_dtls_conn_process(DtlsConn *c, const uint8_t *dgram, size_t len, uint8_t *out, size_t out_cap);
188
189/**
190 * @brief Milliseconds (in @ref pc_millis units) until the retransmission timer fires, or -1 if no
191 * timer is running (no flight is outstanding, or the handshake is done or failed).
192 *
193 * The transport-neutral core keeps no timer of its own; the caller polls this to schedule a wake-up and
194 * calls @ref pc_dtls_conn_on_timeout when it expires (RFC 9147 §5.8). 0 means the timer is already due.
195 */
196int pc_dtls_conn_timeout_ms(const DtlsConn *c);
197
198/**
199 * @brief Fire the retransmission timer: re-send the outstanding flight into @p out with fresh record
200 * sequence numbers and double the timeout (RFC 9147 §5.8.1 exponential backoff).
201 *
202 * Retransmits only if the PTO has actually elapsed, so a spurious/early call is a no-op (returns 0).
203 * After @ref PC_DTLS_MAX_RETRANSMITS attempts the handshake is abandoned (state becomes FAILED, returns -1).
204 *
205 * @return bytes written to @p out (0 if nothing was due), or -1 if the retransmission ceiling was hit.
206 */
207int pc_dtls_conn_on_timeout(DtlsConn *c, uint8_t *out, size_t out_cap);
208
209/** @brief True once the handshake has completed and the application-traffic keys are installed. */
210bool pc_dtls_conn_established(const DtlsConn *c);
211
212/** @brief The alert code (RFC 8446 §6) set when the handshake failed, or 0. */
213uint8_t pc_dtls_conn_alert(const DtlsConn *c);
214
215/** @brief Application-epoch (epoch 3) server write keys - protect outbound application records. */
216DtlsRecordKeys *pc_dtls_conn_app_write_keys(DtlsConn *c);
217
218/** @brief Application-epoch (epoch 3) client read keys - unprotect inbound application records. */
219DtlsRecordKeys *pc_dtls_conn_app_read_keys(DtlsConn *c);
220
221/**
222 * @brief The server's connection id (RFC 9146 / RFC 9147 §9) for this connection, if one was negotiated.
223 *
224 * The client places this id in every record it sends, so a UDP front-end can route inbound records by it
225 * and follow the peer across an address change (NAT rebinding). Copies the id to @p out (which must hold
226 * at least @ref PC_DTLS_CID_MAX bytes).
227 *
228 * @return the connection-id length, or 0 if no connection id was negotiated.
229 */
230size_t pc_dtls_conn_local_cid(const DtlsConn *c, uint8_t *out);
231
232/**
233 * @brief Decrypt one inbound epoch-3 application record into @p out (RFC 9147 §4).
234 *
235 * Only valid once established. Enforces the epoch-3 anti-replay window (§4.5.1) and that the record's
236 * true content type is application_data. A replayed, too-old, or non-application record yields false.
237 *
238 * @return true and sets @p *out_len on success; false if the connection is not established, the record
239 * fails to open, it is a replay, or it is not application data.
240 */
241bool pc_dtls_conn_open_app(DtlsConn *c, const uint8_t *rec, size_t rec_len, uint8_t *out, size_t out_cap,
242 size_t *out_len);
243
244/**
245 * @brief Seal @p data as one outbound epoch-3 application record (RFC 9147 §4), advancing the shared
246 * epoch-3 send sequence so it never collides with the handshake-completion ACK.
247 * @return record bytes written to @p out, or 0 if not established or on overflow.
248 */
249size_t pc_dtls_conn_seal_app(DtlsConn *c, const uint8_t *data, size_t len, uint8_t *out, size_t out_cap);
250
251#endif // PC_ENABLE_DTLS
252#endif // PROTOCORE_DTLS_CONN_H
User-facing configuration for ProtoCore.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
#define PC_SHA256_DIGEST_LEN
SHA-256 digest length in bytes.
Definition sha256.h:25
Streaming SHA-256 context.
Definition sha256.h:40
TLS 1.3 key schedule (RFC 8446 sec 7.1) for the QUIC handshake.