ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dtls_record.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4#ifndef PROTOCORE_DTLS_RECORD_H
5#define PROTOCORE_DTLS_RECORD_H
6
7#include "protocore_config.h" // the entry point: protocore_types.h for the widths
8
10
11/**
12 * @file dtls_record.h
13 * @brief DTLS 1.3 record layer (RFC 9147 §4).
14 *
15 * The datagram counterpart to the TLS 1.3 record layer: it protects and unprotects individual
16 * UDP-carried records. This is the transport-specific half of DTLS 1.3; the handshake it carries
17 * reuses the TLS 1.3 crypto that already backs HTTP/3 (protocore_tls13_*, protocore_hkdf, aes128gcm).
18 *
19 * Two record shapes (RFC 9147 §4):
20 * - **DTLSPlaintext** - the classic 13-byte header (type, legacy_version, epoch, 48-bit sequence
21 * number, length, fragment). Used unencrypted for the first handshake flight and for alerts
22 * sent in epoch 0.
23 * - **DTLSCiphertext** - the compact "unified header" plus an AEAD-sealed body, used once record
24 * keys exist. The record's sequence number is itself encrypted (RFC 9147 §4.2.3), and the AEAD
25 * nonce is the TLS 1.3 construction over the full 64-bit sequence number (§4.2.2, epoch excluded).
26 *
27 * ─ Reuse ─
28 * AEAD (AEAD_AES_128_GCM) and the AES-128 block used for sequence-number encryption come from
29 * aes128gcm; key/iv/sn derivation from protocore_hkdf (HKDF-Expand-Label). Phase 1 supports the one
30 * cipher suite the whole hand-rolled TLS 1.3 stack uses: TLS_AES_128_GCM_SHA256.
31 *
32 * Pure, zero heap, host-tested. Not the mbedTLS TCP-TLS engine (network_drivers/tls) - this is the
33 * self-contained datagram record layer.
34 *
35 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
36 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
37 * a caller drives every namespace the same way.
38 *
39 * @author Douglas Quigg (dstroy0)
40 * @date 2026
41 */
42
43// PROTOCORE_DTLS_RECORD_BORROW - the bytes this module runs out of - is stated in protocore_config.h, which sums
44// it into its arena. Its size and its offset are each a static_assert, so a feature
45// combination that does not fit fails to compile rather than overrunning at run time.
46
47/** @name Record content types (RFC 8446 §5 / RFC 9147 §4).
48 * Shared by the DTLSPlaintext `type` field and the DTLSInnerPlaintext trailing content type. */
49///@{
50#define PROTOCORE_DTLS_CT_CHANGE_CIPHER_SPEC 20
51#define PROTOCORE_DTLS_CT_ALERT 21
52#define PROTOCORE_DTLS_CT_HANDSHAKE 22
53#define PROTOCORE_DTLS_CT_APPLICATION_DATA 23
54#define PROTOCORE_DTLS_CT_ACK 26 ///< DTLS 1.3 acknowledgement (RFC 9147 §7)
55///@}
56
57/** @brief DTLSPlaintext legacy_version on the wire: DTLS 1.2 (RFC 9147 §4). */
58#define PROTOCORE_DTLS_LEGACY_VERSION 0xFEFD
59
60/** @brief DTLSPlaintext header length: type(1) + version(2) + epoch(2) + seq(6) + length(2). */
61#define PROTOCORE_DTLS_PLAINTEXT_HDR_LEN 13
62
63/** @brief AEAD tag length (all supported suites: 16 bytes). */
64#define PROTOCORE_DTLS_TAG_LEN 16
65
66/** @brief Largest connection id carried in a DTLSCiphertext header (RFC 9146 / RFC 9147 §9). The CID
67 * is not length-prefixed on the wire, so the receiver must know its length from negotiation; 8
68 * bytes is ample routing entropy and bounds the fixed header-scratch buffers. */
69#define PROTOCORE_DTLS_CID_MAX 8
70
71/** @brief Record-layer AEAD suites (phase 1: AEAD_AES_128_GCM with SHA-256). */
76
77/**
78 * @brief One direction's record-protection keys for one epoch (RFC 9147 §4).
79 *
80 * Derived from a TLS 1.3 traffic secret; holds the AEAD key + write IV plus the separate
81 * sequence-number-encryption key. One instance per (epoch, direction).
82 */
83typedef struct
84{
85 DtlsCipher cipher; ///< negotiated AEAD (phase 1: AES-128-GCM)
86 uint16_t epoch; ///< this epoch number; its low 2 bits appear in the unified header
87 uint8_t gcm[PROTOCORE_AES128GCM_BORROW]; ///< this epoch's AEAD borrow. Carries both keyed
88 ///< contexts: the record AEAD and the
89 ///< sequence-number-protection block. Replaces the raw
90 ///< keys, so neither stays resident.
91 uint8_t iv[12]; ///< AEAD write IV (per-record nonce = iv XOR sequence_number)
93
94/** @brief Parsed view of a DTLSPlaintext record (fields point into the caller's buffer). */
95typedef struct
96{
97 uint8_t content_type;
98 uint16_t epoch;
99 uint64_t seq; ///< 48-bit record sequence number
100 const uint8_t *fragment; ///< into the input buffer
101 size_t frag_len;
103
104/** @brief Result of a successful @ref protocore_dtls_ciphertext_unprotect. */
105typedef struct
106{
107 uint8_t content_type; ///< recovered inner content type (last non-zero byte of the inner plaintext)
108 uint16_t epoch; ///< epoch of @p keys (its low 2 bits matched the header)
109 uint64_t seq; ///< reconstructed full sequence number
110 size_t pt_len; ///< plaintext bytes written to @p out
112
113/** @brief 64-record sliding replay window over the highest sequence number accepted in an epoch. */
114typedef struct
115{
116 uint64_t highest; ///< highest accepted sequence number (bit 0 of @ref bitmap)
117 uint64_t bitmap; ///< bit i set => (highest - i) has been accepted
118 proto_bool seeded; ///< false until the first record is accepted
120
121/** @brief Dispatch table. Addressed by offset, so the layout is asserted below. */
122typedef struct
123{
124 void (*keys_derive)(uint8_t *, DtlsRecordKeys *, DtlsCipher, uint16_t, const uint8_t *);
125 size_t (*plaintext_build)(uint8_t *, uint8_t, uint16_t, uint64_t, const uint8_t *, size_t, uint8_t *, size_t);
126 size_t (*plaintext_parse)(uint8_t *, const uint8_t *, size_t, DtlsPlaintext *);
127 size_t (*protect)(uint8_t *, DtlsRecordKeys *, uint64_t, uint8_t, const uint8_t *, size_t, uint8_t *, size_t,
128 const uint8_t *, size_t);
129 proto_bool (*unprotect)(uint8_t *, DtlsRecordKeys *, uint64_t, const uint8_t *, size_t, uint8_t *, size_t,
130 DtlsCiphertext *, const uint8_t *, size_t);
131 void (*replay_init)(uint8_t *, DtlsReplayWindow *);
132 proto_bool (*replay_check)(uint8_t *, const DtlsReplayWindow *, uint64_t);
133 void (*replay_mark)(uint8_t *, DtlsReplayWindow *, uint64_t);
135PROTOCORE_NS_LAYOUT(DtlsRecordNs, keys_derive, plaintext_build, plaintext_parse, protect, unprotect, replay_init,
136 replay_check, replay_mark);
137
138/**
139 * @brief Derive one direction's record keys from a 32-byte TLS 1.3 traffic .
140 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
141 * @param out Out
142 * @param cipher Cipher
143 * @param epoch Epoch
144 * @param secret 32 bytes
145 */
146void protocore_dtls_record_keys_derive(uint8_t *work, DtlsRecordKeys *out, DtlsCipher cipher, uint16_t epoch,
147 const uint8_t *secret);
148/**
149 * @brief A DTLSPlaintext record; bytes written (13 + frag_len), or 0 on .
150 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
151 * @param content_type Content type
152 * @param epoch Epoch
153 * @param seq Seq
154 * @param fragment Fragment
155 * @param frag_len Frag len
156 * @param out Out
157 * @param out_cap Out cap
158 * @return The size_t.
159 */
160size_t protocore_dtls_record_plaintext_build(uint8_t *work, uint8_t content_type, uint16_t epoch, uint64_t seq,
161 const uint8_t *fragment, size_t frag_len, uint8_t *out, size_t out_cap);
162/**
163 * @brief The same record back, validating legacy_version and the length .
164 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
165 * @param rec Rec
166 * @param rec_len Rec len
167 * @param out Out
168 * @return The size_t.
169 */
170size_t protocore_dtls_record_plaintext_parse(uint8_t *work, const uint8_t *rec, size_t rec_len, DtlsPlaintext *out);
171/**
172 * @brief Seal one record (RFC 9147 sec 4.2): the unified header, the .
173 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
174 * @param keys Keys
175 * @param seq Seq
176 * @param content_type Content type
177 * @param plaintext Plaintext
178 * @param pt_len Pt len
179 * @param out Out
180 * @param out_cap Out cap
181 * @param cid Cid
182 * @param cid_len Cid len
183 * @return The size_t.
184 */
185size_t protocore_dtls_record_protect(uint8_t *work, DtlsRecordKeys *keys, uint64_t seq, uint8_t content_type,
186 const uint8_t *plaintext, size_t pt_len, uint8_t *out, size_t out_cap,
187 const uint8_t *cid, size_t cid_len);
188/**
189 * @brief Open one received record: decrypt the sequence number, rebuild the .
190 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
191 * @param keys Keys
192 * @param next_seq Next seq
193 * @param rec Rec
194 * @param rec_len Rec len
195 * @param out Out
196 * @param out_cap Out cap
197 * @param info Info
198 * @param expected_cid Expected cid
199 * @param expected_cid_len Expected cid len
200 * @return PROTO_TRUE on success.
201 */
202proto_bool protocore_dtls_record_unprotect(uint8_t *work, DtlsRecordKeys *keys, uint64_t next_seq, const uint8_t *rec,
203 size_t rec_len, uint8_t *out, size_t out_cap, DtlsCiphertext *info,
204 const uint8_t *expected_cid, size_t expected_cid_len);
205/**
206 * @brief Reset a replay window to empty.
207 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
208 * @param w W
209 */
211/**
212 * @brief Whether seq is new and inside the window, rather than a replay or .
213 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
214 * @param w W
215 * @param seq Seq
216 * @return PROTO_TRUE on success.
217 */
219/**
220 * @brief Record seq as accepted and advance the window; only after a .
221 * @param work PROTOCORE_DTLS_RECORD_BORROW bytes the caller took. Not held past the call.
222 * @param w W
223 * @param seq Seq
224 */
225void protocore_dtls_record_replay_mark(uint8_t *work, DtlsReplayWindow *w, uint64_t seq);
226
227/** @brief Module namespace. */
236
238
239#endif // PROTOCORE_DTLS_RECORD_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
enum PROTO_ENUM_PACKED DtlsCipher
Record-layer AEAD suites (phase 1: AEAD_AES_128_GCM with SHA-256).
void protocore_dtls_record_replay_mark(uint8_t *work, DtlsReplayWindow *w, uint64_t seq)
Record seq as accepted and advance the window; only after a .
void protocore_dtls_record_replay_init(uint8_t *work, DtlsReplayWindow *w)
Reset a replay window to empty.
PROTOCORE_NS DtlsRecordNs DtlsRecord PROTOCORE_UNUSED
Module namespace.
size_t protocore_dtls_record_protect(uint8_t *work, DtlsRecordKeys *keys, uint64_t seq, uint8_t content_type, const uint8_t *plaintext, size_t pt_len, uint8_t *out, size_t out_cap, const uint8_t *cid, size_t cid_len)
Seal one record (RFC 9147 sec 4.2): the unified header, the .
size_t protocore_dtls_record_plaintext_build(uint8_t *work, uint8_t content_type, uint16_t epoch, uint64_t seq, const uint8_t *fragment, size_t frag_len, uint8_t *out, size_t out_cap)
A DTLSPlaintext record; bytes written (13 + frag_len), or 0 on .
proto_bool protocore_dtls_record_replay_check(uint8_t *work, const DtlsReplayWindow *w, uint64_t seq)
Whether seq is new and inside the window, rather than a replay or .
proto_bool protocore_dtls_record_unprotect(uint8_t *work, DtlsRecordKeys *keys, uint64_t next_seq, const uint8_t *rec, size_t rec_len, uint8_t *out, size_t out_cap, DtlsCiphertext *info, const uint8_t *expected_cid, size_t expected_cid_len)
Open one received record: decrypt the sequence number, rebuild the .
@ DTLS_CIPHER_AES_128_GCM_SHA256
Definition dtls_record.h:74
size_t protocore_dtls_record_plaintext_parse(uint8_t *work, const uint8_t *rec, size_t rec_len, DtlsPlaintext *out)
The same record back, validating legacy_version and the length .
void protocore_dtls_record_keys_derive(uint8_t *work, DtlsRecordKeys *out, DtlsCipher cipher, uint16_t epoch, const uint8_t *secret)
Derive one direction's record keys from a 32-byte TLS 1.3 traffic .
#define PROTOCORE_AES128GCM_BORROW
#define PROTOCORE_NS_LAYOUT(T,...)
Pin every dispatch slot of a table that is nothing but function pointers.
#define PROTOCORE_NS
Storage for a dispatch table. The const is load bearing.
Result of a successful protocore_dtls_ciphertext_unprotect.
uint64_t seq
reconstructed full sequence number
size_t pt_len
plaintext bytes written to out
uint16_t epoch
epoch of keys (its low 2 bits matched the header)
uint8_t content_type
recovered inner content type (last non-zero byte of the inner plaintext)
Parsed view of a DTLSPlaintext record (fields point into the caller's buffer).
Definition dtls_record.h:96
uint64_t seq
48-bit record sequence number
Definition dtls_record.h:99
uint8_t content_type
Definition dtls_record.h:97
uint16_t epoch
Definition dtls_record.h:98
const uint8_t * fragment
into the input buffer
One direction's record-protection keys for one epoch (RFC 9147 §4).
Definition dtls_record.h:84
DtlsCipher cipher
negotiated AEAD (phase 1: AES-128-GCM)
Definition dtls_record.h:85
uint16_t epoch
this epoch number; its low 2 bits appear in the unified header
Definition dtls_record.h:86
Dispatch table. Addressed by offset, so the layout is asserted below.
void(* keys_derive)(uint8_t *, DtlsRecordKeys *, DtlsCipher, uint16_t, const uint8_t *)
64-record sliding replay window over the highest sequence number accepted in an epoch.
uint64_t highest
highest accepted sequence number (bit 0 of bitmap)
uint64_t bitmap
bit i set => (highest - i) has been accepted
proto_bool seeded
false until the first record is accepted
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97