DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dtls_record.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 dtls_record.cpp
6 * @brief DTLS 1.3 record layer (RFC 9147 §4). See dtls_record.h.
7 */
8
10
11#if DETWS_ENABLE_DTLS
12
15#include <string.h>
16
17namespace
18{
19// Unified-header first-byte fixed pattern and flag bits (RFC 9147 §4, Figure 3): 0 0 1 C S L E E.
20const uint8_t DTLS_UH_FIXED = 0x20; // 001x xxxx
21const uint8_t DTLS_UH_FIXED_MASK = 0xE0;
22const uint8_t DTLS_UH_CID = 0x10; // C: connection id present
23const uint8_t DTLS_UH_SEQ16 = 0x08; // S: 16-bit (vs 8-bit) sequence number
24const uint8_t DTLS_UH_LENGTH = 0x04; // L: length present
25const uint8_t DTLS_UH_EPOCH_MASK = 0x03;
26
27// Build the AEAD nonce: the 64-bit sequence number, right-aligned in the 12-byte IV, XOR the IV
28// (RFC 9147 §4.2.2 / RFC 8446 §5.3; the epoch is NOT mixed in). Same construction as QUIC.
29void build_nonce(const uint8_t iv[12], uint64_t seq, uint8_t nonce[12])
30{
31 memcpy(nonce, iv, 12);
32 for (int i = 0; i < 8; i++)
33 nonce[11 - i] ^= (uint8_t)(seq >> (8 * i));
34}
35
36// Reconstruct the full sequence number from its truncated on-wire bits (RFC 9147 §4.2.2, using the
37// RFC 9000 Appendix A.3 packet-number decoding: the candidate closest to `expected`).
38uint64_t seq_decode(uint64_t expected, uint64_t truncated, unsigned bits)
39{
40 if (bits == 0 || bits >= 64)
41 return truncated;
42 uint64_t win = (uint64_t)1 << bits;
43 uint64_t hwin = win >> 1;
44 uint64_t mask = win - 1;
45 uint64_t candidate = (expected & ~mask) | (truncated & mask);
46 if (candidate + hwin <= expected && candidate + win > candidate)
47 return candidate + win;
48 if (candidate > expected + hwin && candidate >= win)
49 return candidate - win;
50 return candidate;
51}
52} // namespace
53
54void dtls_record_keys_derive(DtlsRecordKeys *out, DtlsCipher cipher, uint16_t epoch, const uint8_t secret[32])
55{
56 out->cipher = cipher;
57 out->epoch = epoch;
58 // AEAD_AES_128_GCM: 16-byte key, 12-byte IV, 16-byte sequence-number key. The DTLS 1.3 variant
59 // carries the "dtls13" HKDF-Expand-Label prefix (RFC 9147 §4.2.3 / §5.9).
60 tls13_kdf_expand_label(&DTLS13_KDF, secret, "key", out->key, sizeof(out->key));
61 tls13_kdf_expand_label(&DTLS13_KDF, secret, "iv", out->iv, sizeof(out->iv));
62 tls13_kdf_expand_label(&DTLS13_KDF, secret, "sn", out->sn_key, sizeof(out->sn_key));
63}
64
65// ---------------------------------------------------------------------------
66// DTLSPlaintext
67// ---------------------------------------------------------------------------
68
69size_t dtls_plaintext_build(uint8_t content_type, uint16_t epoch, uint64_t seq, const uint8_t *fragment,
70 size_t frag_len, uint8_t *out, size_t out_cap)
71{
72 size_t total = DTLS_PLAINTEXT_HDR_LEN + frag_len;
73 if (total > out_cap || frag_len > 0xFFFF)
74 return 0;
75 out[0] = content_type;
76 out[1] = (uint8_t)(DTLS_LEGACY_VERSION >> 8);
77 out[2] = (uint8_t)DTLS_LEGACY_VERSION;
78 out[3] = (uint8_t)(epoch >> 8);
79 out[4] = (uint8_t)epoch;
80 out[5] = (uint8_t)(seq >> 40); // 48-bit sequence number, big-endian
81 out[6] = (uint8_t)(seq >> 32);
82 out[7] = (uint8_t)(seq >> 24);
83 out[8] = (uint8_t)(seq >> 16);
84 out[9] = (uint8_t)(seq >> 8);
85 out[10] = (uint8_t)seq;
86 out[11] = (uint8_t)(frag_len >> 8);
87 out[12] = (uint8_t)frag_len;
88 if (frag_len)
89 memcpy(out + DTLS_PLAINTEXT_HDR_LEN, fragment, frag_len);
90 return total;
91}
92
93size_t dtls_plaintext_parse(const uint8_t *rec, size_t rec_len, DtlsPlaintext *out)
94{
95 if (rec_len < DTLS_PLAINTEXT_HDR_LEN)
96 return 0;
97 if (rec[1] != (uint8_t)(DTLS_LEGACY_VERSION >> 8) || rec[2] != (uint8_t)DTLS_LEGACY_VERSION)
98 return 0; // wrong legacy_version
99 out->content_type = rec[0];
100 out->epoch = (uint16_t)(((uint16_t)rec[3] << 8) | rec[4]);
101 out->seq = ((uint64_t)rec[5] << 40) | ((uint64_t)rec[6] << 32) | ((uint64_t)rec[7] << 24) |
102 ((uint64_t)rec[8] << 16) | ((uint64_t)rec[9] << 8) | (uint64_t)rec[10];
103 size_t length = ((size_t)rec[11] << 8) | rec[12];
104 if (DTLS_PLAINTEXT_HDR_LEN + length > rec_len)
105 return 0;
106 out->fragment = rec + DTLS_PLAINTEXT_HDR_LEN;
107 out->frag_len = length;
108 return DTLS_PLAINTEXT_HDR_LEN + length;
109}
110
111// ---------------------------------------------------------------------------
112// DTLSCiphertext
113// ---------------------------------------------------------------------------
114
115size_t dtls_ciphertext_protect(const DtlsRecordKeys *keys, uint64_t seq, uint8_t content_type, const uint8_t *plaintext,
116 size_t pt_len, uint8_t *out, size_t out_cap, const uint8_t *cid, size_t cid_len)
117{
118 if (keys->cipher != DtlsCipher::AES_128_GCM_SHA256)
119 return 0;
120 if (cid_len > DTLS_CID_MAX || (cid_len && !cid))
121 return 0;
122 // Unified header: [C] connection id, S=1 (16-bit seq), L=1 (length). hdr = byte0 || [cid] || seq16 ||
123 // length16. The CID (RFC 9146 / RFC 9147 §9) sits between the first byte and the sequence number.
124 const size_t hdr_len = 1 + cid_len + 2 + 2;
125 size_t inner_len = pt_len + 1; // DTLSInnerPlaintext = plaintext || content_type
126 size_t enc_len = inner_len + DTLS_TAG_LEN; // AEAD ciphertext || tag
127 size_t total = hdr_len + enc_len;
128 if (total > out_cap)
129 return 0;
130
131 uint8_t flags = (uint8_t)(DTLS_UH_FIXED | DTLS_UH_SEQ16 | DTLS_UH_LENGTH | (keys->epoch & DTLS_UH_EPOCH_MASK));
132 if (cid_len)
133 flags |= DTLS_UH_CID;
134 out[0] = flags;
135 if (cid_len)
136 memcpy(out + 1, cid, cid_len);
137 size_t seq_off = 1 + cid_len;
138 out[seq_off] = (uint8_t)(seq >> 8); // plaintext sequence number (this header form is the AEAD AAD)
139 out[seq_off + 1] = (uint8_t)seq;
140 out[seq_off + 2] = (uint8_t)(enc_len >> 8);
141 out[seq_off + 3] = (uint8_t)enc_len;
142
143 // Assemble the inner plaintext where it will be sealed (seal permits out == pt).
144 memcpy(out + hdr_len, plaintext, pt_len);
145 out[hdr_len + pt_len] = content_type;
146
147 uint8_t nonce[12];
148 build_nonce(keys->iv, seq, nonce);
149 // AAD = the whole unified header (including any connection id) carrying the plaintext sequence
150 // number (before §4.2.3 encryption).
151 quic_aes128_gcm_seal(keys->key, nonce, out, hdr_len, out + hdr_len, inner_len, out + hdr_len);
152
153 // Encrypt the sequence number (RFC 9147 §4.2.3): mask = AES-ECB(sn_key, ciphertext[0..15]).
154 // enc_len = inner_len + 16 >= 17, so the 16-byte sample is always available.
155 QuicAes128 sn;
156 quic_aes128_init(&sn, keys->sn_key);
157 uint8_t mask[16];
158 quic_aes128_encrypt_block(&sn, out + hdr_len, mask);
159 quic_aes128_wipe(&sn);
160 out[seq_off] ^= mask[0];
161 out[seq_off + 1] ^= mask[1];
162 return total;
163}
164
165bool dtls_ciphertext_unprotect(const DtlsRecordKeys *keys, uint64_t next_seq, const uint8_t *rec, size_t rec_len,
166 uint8_t *out, size_t out_cap, DtlsCiphertext *info, const uint8_t *expected_cid,
167 size_t expected_cid_len)
168{
169 if (keys->cipher != DtlsCipher::AES_128_GCM_SHA256 || rec_len < 1)
170 return false;
171 if (expected_cid_len > DTLS_CID_MAX)
172 return false;
173 uint8_t b0 = rec[0];
174 if ((b0 & DTLS_UH_FIXED_MASK) != DTLS_UH_FIXED)
175 return false; // top 3 bits must be 001
176 if ((b0 & DTLS_UH_EPOCH_MASK) != (keys->epoch & DTLS_UH_EPOCH_MASK))
177 return false; // wrong epoch keys for this record
178
179 size_t off = 1;
180 if (b0 & DTLS_UH_CID)
181 {
182 // A connection-id record: a CID must have been negotiated for this direction, and it must be
183 // ours (the CID is not length-prefixed on the wire - its length is known only from negotiation).
184 if (expected_cid_len == 0 || off + expected_cid_len > rec_len ||
185 memcmp(rec + off, expected_cid, expected_cid_len) != 0)
186 return false;
187 off += expected_cid_len;
188 }
189 else if (expected_cid_len != 0)
190 {
191 return false; // a CID was negotiated for this direction but the record carries none
192 }
193
194 size_t seq_len = (b0 & DTLS_UH_SEQ16) ? 2 : 1;
195 if (off + seq_len > rec_len)
196 return false;
197 size_t seq_off = off;
198 off += seq_len;
199
200 size_t enc_len;
201 if (b0 & DTLS_UH_LENGTH)
202 {
203 if (off + 2 > rec_len)
204 return false;
205 enc_len = ((size_t)rec[off] << 8) | rec[off + 1];
206 off += 2;
207 }
208 else
209 {
210 enc_len = rec_len - off; // to end of datagram
211 }
212 if (off + enc_len > rec_len || enc_len < 16 || enc_len < DTLS_TAG_LEN + 1)
213 return false; // need >= 16 bytes for the SN sample and >= tag + one inner byte
214
215 const uint8_t *enc = rec + off;
216 size_t hdr_len = off; // unified header length (including any connection id)
217
218 // Copy the header so we can write the decrypted sequence number into the AEAD AAD form.
219 uint8_t hdr[1 + DTLS_CID_MAX + 4];
220 memcpy(hdr, rec, hdr_len);
221
222 // Decrypt the sequence number (RFC 9147 §4.2.3).
223 QuicAes128 sn;
224 quic_aes128_init(&sn, keys->sn_key);
225 uint8_t mask[16];
226 quic_aes128_encrypt_block(&sn, enc, mask);
227 quic_aes128_wipe(&sn);
228 uint64_t trunc = 0;
229 for (size_t i = 0; i < seq_len; i++)
230 {
231 hdr[seq_off + i] ^= mask[i];
232 trunc = (trunc << 8) | hdr[seq_off + i];
233 }
234 uint64_t full_seq = seq_decode(next_seq, trunc, (unsigned)(seq_len * 8));
235
236 size_t inner_len = enc_len - DTLS_TAG_LEN;
237 if (inner_len > out_cap)
238 return false;
239
240 uint8_t nonce[12];
241 build_nonce(keys->iv, full_seq, nonce);
242 if (!quic_aes128_gcm_open(keys->key, nonce, hdr, hdr_len, enc, enc_len, out))
243 return false;
244
245 // Strip zero padding: the last non-zero byte of the inner plaintext is the content type (RFC 8446 §5.2).
246 size_t n = inner_len;
247 while (n > 0 && out[n - 1] == 0)
248 n--;
249 if (n == 0)
250 return false; // no content type -> invalid record
251 info->content_type = out[n - 1];
252 info->pt_len = n - 1;
253 info->seq = full_seq;
254 info->epoch = keys->epoch;
255 return true;
256}
257
258// ---------------------------------------------------------------------------
259// Anti-replay sliding window (RFC 9147 §4.5.1)
260// ---------------------------------------------------------------------------
261
262void dtls_replay_init(DtlsReplayWindow *w)
263{
264 w->highest = 0;
265 w->bitmap = 0;
266 w->seeded = false;
267}
268
269bool dtls_replay_check(const DtlsReplayWindow *w, uint64_t seq)
270{
271 if (!w->seeded || seq > w->highest)
272 return true; // first record, or ahead of the window
273 uint64_t diff = w->highest - seq;
274 if (diff >= 64)
275 return false; // older than the window
276 return ((w->bitmap >> diff) & 1u) == 0; // set bit => already seen (replay)
277}
278
279void dtls_replay_mark(DtlsReplayWindow *w, uint64_t seq)
280{
281 if (!w->seeded)
282 {
283 w->seeded = true;
284 w->highest = seq;
285 w->bitmap = 1; // bit 0 = highest
286 return;
287 }
288 if (seq > w->highest)
289 {
290 uint64_t shift = seq - w->highest;
291 w->bitmap = (shift >= 64) ? 1u : ((w->bitmap << shift) | 1u);
292 w->highest = seq;
293 return;
294 }
295 uint64_t diff = w->highest - seq;
296 if (diff < 64)
297 w->bitmap |= ((uint64_t)1 << diff);
298}
299
300#endif // DETWS_ENABLE_DTLS
DTLS 1.3 record layer (RFC 9147 §4).
AES-128 block cipher and AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
TLS 1.3 key schedule (RFC 8446 sec 7.1) for the QUIC handshake.