ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dtls_record.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_record.h
6 * @brief DTLS 1.3 record layer (RFC 9147 §4).
7 *
8 * The datagram counterpart to the TLS 1.3 record layer: it protects and unprotects individual
9 * UDP-carried records. This is the transport-specific half of DTLS 1.3; the handshake it carries
10 * reuses the TLS 1.3 crypto that already backs HTTP/3 (pc_tls13_*, pc_hkdf, aes128gcm).
11 *
12 * Two record shapes (RFC 9147 §4):
13 * - **DTLSPlaintext** - the classic 13-byte header (type, legacy_version, epoch, 48-bit sequence
14 * number, length, fragment). Used unencrypted for the first handshake flight and for alerts
15 * sent in epoch 0.
16 * - **DTLSCiphertext** - the compact "unified header" plus an AEAD-sealed body, used once record
17 * keys exist. The record's sequence number is itself encrypted (RFC 9147 §4.2.3), and the AEAD
18 * nonce is the TLS 1.3 construction over the full 64-bit sequence number (§4.2.2, epoch excluded).
19 *
20 * ─ Reuse ─
21 * AEAD (AEAD_AES_128_GCM) and the AES-128 block used for sequence-number encryption come from
22 * aes128gcm; key/iv/sn derivation from pc_hkdf (HKDF-Expand-Label). Phase 1 supports the one
23 * cipher suite the whole hand-rolled TLS 1.3 stack uses: TLS_AES_128_GCM_SHA256.
24 *
25 * Pure, zero heap, host-tested. Not the mbedTLS TCP-TLS engine (network_drivers/tls) - this is the
26 * self-contained datagram record layer.
27 *
28 * @author Douglas Quigg (dstroy0)
29 * @date 2026
30 */
31
32#ifndef PROTOCORE_DTLS_RECORD_H
33#define PROTOCORE_DTLS_RECORD_H
34
35#include "crypto/aead/aes128gcm.h" // pc_aes128gcm_key, PC_WORK_AES128GCM
36#include "protocore_config.h"
37
38#if PC_ENABLE_DTLS
39
40#include <stddef.h>
41#include <stdint.h>
42
43/** @name Record content types (RFC 8446 §5 / RFC 9147 §4).
44 * Shared by the DTLSPlaintext `type` field and the DTLSInnerPlaintext trailing content type. */
45///@{
46#define PC_DTLS_CT_CHANGE_CIPHER_SPEC 20
47#define PC_DTLS_CT_ALERT 21
48#define PC_DTLS_CT_HANDSHAKE 22
49#define PC_DTLS_CT_APPLICATION_DATA 23
50#define PC_DTLS_CT_ACK 26 ///< DTLS 1.3 acknowledgement (RFC 9147 §7)
51///@}
52
53/** @brief DTLSPlaintext legacy_version on the wire: DTLS 1.2 (RFC 9147 §4). */
54#define PC_DTLS_LEGACY_VERSION 0xFEFD
55
56/** @brief DTLSPlaintext header length: type(1) + version(2) + epoch(2) + seq(6) + length(2). */
57#define PC_DTLS_PLAINTEXT_HDR_LEN 13
58
59/** @brief AEAD tag length (all supported suites: 16 bytes). */
60#define PC_DTLS_TAG_LEN 16
61
62/** @brief Largest connection id carried in a DTLSCiphertext header (RFC 9146 / RFC 9147 §9). The CID
63 * is not length-prefixed on the wire, so the receiver must know its length from negotiation; 8
64 * bytes is ample routing entropy and bounds the fixed header-scratch buffers. */
65#define PC_DTLS_CID_MAX 8
66
67/** @brief Record-layer AEAD suites (phase 1: AEAD_AES_128_GCM with SHA-256). */
68enum class DtlsCipher : uint8_t
69{
70 AES_128_GCM_SHA256 = 0
71};
72
73/**
74 * @brief One direction's record-protection keys for one epoch (RFC 9147 §4).
75 *
76 * Derived from a TLS 1.3 traffic secret; holds the AEAD key + write IV plus the separate
77 * sequence-number-encryption key. One instance per (epoch, direction).
78 */
79struct DtlsRecordKeys
80{
81 DtlsCipher cipher; ///< negotiated AEAD (phase 1: AES-128-GCM)
82 uint16_t epoch; ///< this epoch number; its low 2 bits appear in the unified header
83 alignas(8) uint8_t gcm[PC_WORK_AES128GCM]; ///< keyed AEAD context, built once per key.
84 ///< Replaces the raw key: the schedule is what the
85 ///< AEAD needs, so no raw key stays resident.
86 uint8_t iv[12]; ///< AEAD write IV (per-record nonce = iv XOR sequence_number)
87 alignas(8) uint8_t sn_key[PC_WORK_AES128]; ///< Keyed sequence-number-protection context.
88 ///< Built once; see quic_crypto.h for the numbers.
89};
90
91/**
92 * @brief Derive the directional record keys from a 32-byte TLS 1.3 traffic secret.
93 *
94 * RFC 8446 §7.3 + RFC 9147 §4.2.3: key = HKDF-Expand-Label(secret,"key",""), iv = "iv", and the
95 * sequence-number key = HKDF-Expand-Label(secret,"sn","") (all with the "tls13 " prefix).
96 */
97void pc_dtls_record_keys_derive(DtlsRecordKeys *out, DtlsCipher cipher, uint16_t epoch, const uint8_t secret[32]);
98
99// ---------------------------------------------------------------------------
100// DTLSPlaintext (RFC 9147 §4): unencrypted record (initial handshake flight, alerts)
101// ---------------------------------------------------------------------------
102
103/**
104 * @brief Build a DTLSPlaintext record.
105 * @return total bytes written (PC_DTLS_PLAINTEXT_HDR_LEN + @p frag_len), or 0 on overflow.
106 */
107size_t pc_dtls_plaintext_build(uint8_t content_type, uint16_t epoch, uint64_t seq, const uint8_t *fragment,
108 size_t frag_len, uint8_t *out, size_t out_cap);
109
110/** @brief Parsed view of a DTLSPlaintext record (fields point into the caller's buffer). */
111struct DtlsPlaintext
112{
113 uint8_t content_type;
114 uint16_t epoch;
115 uint64_t seq; ///< 48-bit record sequence number
116 const uint8_t *fragment; ///< into the input buffer
117 size_t frag_len;
118};
119
120/**
121 * @brief Parse a DTLSPlaintext record, validating legacy_version and the length field.
122 * @return total record length consumed (13 + length), or 0 if malformed / truncated.
123 */
124size_t pc_dtls_plaintext_parse(const uint8_t *rec, size_t rec_len, DtlsPlaintext *out);
125
126// ---------------------------------------------------------------------------
127// DTLSCiphertext (RFC 9147 §4): AEAD-protected record with the unified header
128// ---------------------------------------------------------------------------
129
130/**
131 * @brief Protect one record (RFC 9147 §4.2).
132 *
133 * Seals @p plaintext (whose inner content type is @p content_type) under @p keys at record sequence
134 * number @p seq, writing a complete DTLSCiphertext to @p out: the unified header (S=1 16-bit sequence
135 * number, L=1 length present, low 2 epoch bits), the AEAD-sealed body, and the encrypted sequence
136 * number. The AEAD nonce is iv XOR seq; the associated data is the unified header carrying the
137 * *plaintext* sequence number (before §4.2.3 encryption).
138 *
139 * When @p cid_len is non-zero the peer's connection id (RFC 9146 / RFC 9147 §9) is placed in the
140 * header (C bit set, @p cid bytes immediately after the first byte) and is covered by the AEAD AAD;
141 * @p cid_len must be <= @ref PC_DTLS_CID_MAX. With @p cid_len 0 the header carries no CID (C=0), the
142 * original behavior.
143 *
144 * @return bytes written, or 0 on overflow / unsupported cipher / an over-long CID.
145 */
146size_t pc_dtls_ciphertext_protect(DtlsRecordKeys &keys, uint64_t seq, uint8_t content_type, const uint8_t *plaintext,
147 size_t pt_len, uint8_t *out, size_t out_cap, const uint8_t *cid = nullptr,
148 size_t cid_len = 0);
149
150/** @brief Result of a successful @ref pc_dtls_ciphertext_unprotect. */
151struct DtlsCiphertext
152{
153 uint8_t content_type; ///< recovered inner content type (last non-zero byte of the inner plaintext)
154 uint16_t epoch; ///< epoch of @p keys (its low 2 bits matched the header)
155 uint64_t seq; ///< reconstructed full sequence number
156 size_t pt_len; ///< plaintext bytes written to @p out
157};
158
159/**
160 * @brief Unprotect one received DTLSCiphertext record (RFC 9147 §4.2).
161 *
162 * Decrypts the sequence number, reconstructs the full sequence number from @p next_seq (the expected
163 * next value for this epoch, RFC 9147 §4.2.2 / RFC 9000 Appendix A.3), opens the AEAD, and strips the
164 * inner content type and any zero padding. @p keys must be the epoch whose low 2 bits match the
165 * record header (verified).
166 *
167 * Connection ids (RFC 9146 / RFC 9147 §9): when @p expected_cid_len is non-zero a CID was negotiated
168 * for this direction, so the record must carry the C bit and a connection id equal to @p expected_cid
169 * (the CID is not length-prefixed on the wire - its length is known only from negotiation); the CID is
170 * covered by the AEAD AAD. When @p expected_cid_len is 0 a record with the C bit set is rejected (a CID
171 * was not negotiated).
172 *
173 * @return true on success (@p out / @p info filled); false on a malformed header, an epoch-bit
174 * mismatch, an unexpected / mismatched connection id, a failed AEAD tag, or an output overflow.
175 */
176bool pc_dtls_ciphertext_unprotect(DtlsRecordKeys &keys, uint64_t next_seq, const uint8_t *rec, size_t rec_len,
177 uint8_t *out, size_t out_cap, DtlsCiphertext *info,
178 const uint8_t *expected_cid = nullptr, size_t expected_cid_len = 0);
179
180// ---------------------------------------------------------------------------
181// Anti-replay sliding window (RFC 9147 §4.5.1)
182// ---------------------------------------------------------------------------
183
184/** @brief 64-record sliding replay window over the highest sequence number accepted in an epoch. */
185struct DtlsReplayWindow
186{
187 uint64_t highest; ///< highest accepted sequence number (bit 0 of @ref bitmap)
188 uint64_t bitmap; ///< bit i set => (highest - i) has been accepted
189 bool seeded; ///< false until the first record is accepted
190};
191
192/** @brief Reset a replay window to empty. */
193void pc_dtls_replay_init(DtlsReplayWindow *w);
194
195/**
196 * @brief Test whether @p seq may be accepted (new and within the window).
197 * @return true if @p seq is new and in-window; false if it is a replay or older than the window.
198 */
199bool pc_dtls_replay_check(const DtlsReplayWindow *w, uint64_t seq);
200
201/** @brief Record @p seq as accepted, advancing the window. Call only after a successful deprotect. */
202void pc_dtls_replay_mark(DtlsReplayWindow *w, uint64_t seq);
203
204#endif // PC_ENABLE_DTLS
205#endif // PROTOCORE_DTLS_RECORD_H
AES-128 block cipher + AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
User-facing configuration for ProtoCore.
#define PC_WORK_AES128
#define PC_WORK_AES128GCM