DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 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 (tls13_*, quic_hkdf, quic_aead).
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 * quic_aead; key/iv/sn derivation from quic_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 DETERMINISTICESPASYNCWEBSERVER_DTLS_RECORD_H
33#define DETERMINISTICESPASYNCWEBSERVER_DTLS_RECORD_H
34
35#include "ServerConfig.h"
36
37#if DETWS_ENABLE_DTLS
38
39#include <stddef.h>
40#include <stdint.h>
41
42/** @name Record content types (RFC 8446 §5 / RFC 9147 §4).
43 * Shared by the DTLSPlaintext `type` field and the DTLSInnerPlaintext trailing content type. */
44///@{
45static constexpr uint8_t DTLS_CT_CHANGE_CIPHER_SPEC = 20;
46static constexpr uint8_t DTLS_CT_ALERT = 21;
47static constexpr uint8_t DTLS_CT_HANDSHAKE = 22;
48static constexpr uint8_t DTLS_CT_APPLICATION_DATA = 23;
49static constexpr uint8_t DTLS_CT_ACK = 26; ///< DTLS 1.3 acknowledgement (RFC 9147 §7)
50///@}
51
52/** @brief DTLSPlaintext legacy_version on the wire: DTLS 1.2 (RFC 9147 §4). */
53static constexpr uint16_t DTLS_LEGACY_VERSION = 0xFEFD;
54
55/** @brief DTLSPlaintext header length: type(1) + version(2) + epoch(2) + seq(6) + length(2). */
56static constexpr size_t DTLS_PLAINTEXT_HDR_LEN = 13;
57
58/** @brief AEAD tag length (all supported suites: 16 bytes). */
59static constexpr size_t DTLS_TAG_LEN = 16;
60
61/** @brief Largest connection id carried in a DTLSCiphertext header (RFC 9146 / RFC 9147 §9). The CID
62 * is not length-prefixed on the wire, so the receiver must know its length from negotiation; 8
63 * bytes is ample routing entropy and bounds the fixed header-scratch buffers. */
64static constexpr size_t DTLS_CID_MAX = 8;
65
66/** @brief Record-layer AEAD suites (phase 1: AEAD_AES_128_GCM with SHA-256). */
67enum class DtlsCipher : uint8_t
68{
69 AES_128_GCM_SHA256 = 0
70};
71
72/**
73 * @brief One direction's record-protection keys for one epoch (RFC 9147 §4).
74 *
75 * Derived from a TLS 1.3 traffic secret; holds the AEAD key + write IV plus the separate
76 * sequence-number-encryption key. One instance per (epoch, direction).
77 */
78struct DtlsRecordKeys
79{
80 DtlsCipher cipher; ///< negotiated AEAD (phase 1: AES-128-GCM)
81 uint16_t epoch; ///< this epoch number; its low 2 bits appear in the unified header
82 uint8_t key[16]; ///< AEAD key
83 uint8_t iv[12]; ///< AEAD write IV (per-record nonce = iv XOR sequence_number)
84 uint8_t sn_key[16]; ///< sequence-number-encryption key
85};
86
87/**
88 * @brief Derive the directional record keys from a 32-byte TLS 1.3 traffic secret.
89 *
90 * RFC 8446 §7.3 + RFC 9147 §4.2.3: key = HKDF-Expand-Label(secret,"key",""), iv = "iv", and the
91 * sequence-number key = HKDF-Expand-Label(secret,"sn","") (all with the "tls13 " prefix).
92 */
93void dtls_record_keys_derive(DtlsRecordKeys *out, DtlsCipher cipher, uint16_t epoch, const uint8_t secret[32]);
94
95// ---------------------------------------------------------------------------
96// DTLSPlaintext (RFC 9147 §4): unencrypted record (initial handshake flight, alerts)
97// ---------------------------------------------------------------------------
98
99/**
100 * @brief Build a DTLSPlaintext record.
101 * @return total bytes written (DTLS_PLAINTEXT_HDR_LEN + @p frag_len), or 0 on overflow.
102 */
103size_t dtls_plaintext_build(uint8_t content_type, uint16_t epoch, uint64_t seq, const uint8_t *fragment,
104 size_t frag_len, uint8_t *out, size_t out_cap);
105
106/** @brief Parsed view of a DTLSPlaintext record (fields point into the caller's buffer). */
107struct DtlsPlaintext
108{
109 uint8_t content_type;
110 uint16_t epoch;
111 uint64_t seq; ///< 48-bit record sequence number
112 const uint8_t *fragment; ///< into the input buffer
113 size_t frag_len;
114};
115
116/**
117 * @brief Parse a DTLSPlaintext record, validating legacy_version and the length field.
118 * @return total record length consumed (13 + length), or 0 if malformed / truncated.
119 */
120size_t dtls_plaintext_parse(const uint8_t *rec, size_t rec_len, DtlsPlaintext *out);
121
122// ---------------------------------------------------------------------------
123// DTLSCiphertext (RFC 9147 §4): AEAD-protected record with the unified header
124// ---------------------------------------------------------------------------
125
126/**
127 * @brief Protect one record (RFC 9147 §4.2).
128 *
129 * Seals @p plaintext (whose inner content type is @p content_type) under @p keys at record sequence
130 * number @p seq, writing a complete DTLSCiphertext to @p out: the unified header (S=1 16-bit sequence
131 * number, L=1 length present, low 2 epoch bits), the AEAD-sealed body, and the encrypted sequence
132 * number. The AEAD nonce is iv XOR seq; the associated data is the unified header carrying the
133 * *plaintext* sequence number (before §4.2.3 encryption).
134 *
135 * When @p cid_len is non-zero the peer's connection id (RFC 9146 / RFC 9147 §9) is placed in the
136 * header (C bit set, @p cid bytes immediately after the first byte) and is covered by the AEAD AAD;
137 * @p cid_len must be <= @ref DTLS_CID_MAX. With @p cid_len 0 the header carries no CID (C=0), the
138 * original behaviour.
139 *
140 * @return bytes written, or 0 on overflow / unsupported cipher / an over-long CID.
141 */
142size_t dtls_ciphertext_protect(const DtlsRecordKeys *keys, uint64_t seq, uint8_t content_type, const uint8_t *plaintext,
143 size_t pt_len, uint8_t *out, size_t out_cap, const uint8_t *cid = nullptr,
144 size_t cid_len = 0);
145
146/** @brief Result of a successful @ref dtls_ciphertext_unprotect. */
147struct DtlsCiphertext
148{
149 uint8_t content_type; ///< recovered inner content type (last non-zero byte of the inner plaintext)
150 uint16_t epoch; ///< epoch of @p keys (its low 2 bits matched the header)
151 uint64_t seq; ///< reconstructed full sequence number
152 size_t pt_len; ///< plaintext bytes written to @p out
153};
154
155/**
156 * @brief Unprotect one received DTLSCiphertext record (RFC 9147 §4.2).
157 *
158 * Decrypts the sequence number, reconstructs the full sequence number from @p next_seq (the expected
159 * next value for this epoch, RFC 9147 §4.2.2 / RFC 9000 Appendix A.3), opens the AEAD, and strips the
160 * inner content type and any zero padding. @p keys must be the epoch whose low 2 bits match the
161 * record header (verified).
162 *
163 * Connection ids (RFC 9146 / RFC 9147 §9): when @p expected_cid_len is non-zero a CID was negotiated
164 * for this direction, so the record must carry the C bit and a connection id equal to @p expected_cid
165 * (the CID is not length-prefixed on the wire - its length is known only from negotiation); the CID is
166 * covered by the AEAD AAD. When @p expected_cid_len is 0 a record with the C bit set is rejected (a CID
167 * was not negotiated).
168 *
169 * @return true on success (@p out / @p info filled); false on a malformed header, an epoch-bit
170 * mismatch, an unexpected / mismatched connection id, a failed AEAD tag, or an output overflow.
171 */
172bool dtls_ciphertext_unprotect(const DtlsRecordKeys *keys, uint64_t next_seq, const uint8_t *rec, size_t rec_len,
173 uint8_t *out, size_t out_cap, DtlsCiphertext *info,
174 const uint8_t *expected_cid = nullptr, size_t expected_cid_len = 0);
175
176// ---------------------------------------------------------------------------
177// Anti-replay sliding window (RFC 9147 §4.5.1)
178// ---------------------------------------------------------------------------
179
180/** @brief 64-record sliding replay window over the highest sequence number accepted in an epoch. */
181struct DtlsReplayWindow
182{
183 uint64_t highest; ///< highest accepted sequence number (bit 0 of @ref bitmap)
184 uint64_t bitmap; ///< bit i set => (highest - i) has been accepted
185 bool seeded; ///< false until the first record is accepted
186};
187
188/** @brief Reset a replay window to empty. */
189void dtls_replay_init(DtlsReplayWindow *w);
190
191/**
192 * @brief Test whether @p seq may be accepted (new and within the window).
193 * @return true if @p seq is new and in-window; false if it is a replay or older than the window.
194 */
195bool dtls_replay_check(const DtlsReplayWindow *w, uint64_t seq);
196
197/** @brief Record @p seq as accepted, advancing the window. Call only after a successful deprotect. */
198void dtls_replay_mark(DtlsReplayWindow *w, uint64_t seq);
199
200#endif // DETWS_ENABLE_DTLS
201#endif // DETERMINISTICESPASYNCWEBSERVER_DTLS_RECORD_H
User-facing configuration for DeterministicESPAsyncWebServer.