DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
quic_tls.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 quic_tls.cpp
6 * @brief TLS 1.3 server handshake state machine for QUIC (see quic_tls.h).
7 */
8
10
11#if DETWS_ENABLE_HTTP3
12
15#if DETWS_ENABLE_PQC_KEX
16#include "network_drivers/presentation/pqc/mlkem.h" // mlkem768_encaps (X25519MLKEM768 hybrid)
17#endif
18#include <string.h>
19
20// TLS alert codes we may raise (RFC 8446 sec 6).
21struct TlsAlert
22{
23 static constexpr uint8_t TLS_ALERT_UNEXPECTED_MESSAGE = 10;
24 static constexpr uint8_t TLS_ALERT_HANDSHAKE_FAILURE = 40;
25 static constexpr uint8_t TLS_ALERT_ILLEGAL_PARAMETER = 47;
26 static constexpr uint8_t TLS_ALERT_DECODE_ERROR = 50;
27 static constexpr uint8_t TLS_ALERT_DECRYPT_ERROR = 51;
28 static constexpr uint8_t TLS_ALERT_PROTOCOL_VERSION = 70;
29 static constexpr uint8_t TLS_ALERT_INTERNAL_ERROR = 80;
30 static constexpr uint8_t TLS_ALERT_MISSING_EXTENSION = 109;
31 static constexpr uint8_t TLS_ALERT_NO_APPLICATION_PROTOCOL = 120;
32};
33
34namespace
35{
36void fail(QuicTls *qt, uint8_t alert)
37{
38 qt->state = QtlsState::QTLS_FAILED;
39 qt->alert = alert;
40}
41
42// Snapshot the running Transcript-Hash without disturbing it (SshSha256Ctx is copyable state).
43void snapshot_hash(const SshSha256Ctx *ctx, uint8_t out[32])
44{
45 SshSha256Ctx tmp = *ctx;
46 ssh_sha256_final(&tmp, out);
47}
48
49// Append a handshake message to both the outbound flight buffer and the transcript.
50bool emit(QuicTls *qt, uint8_t *flight, size_t cap, size_t *plen, size_t written)
51{
52 // *plen <= cap is the invariant this append maintains, so cap - *plen never underflows; refuse a
53 // write that would run past the flight buffer (each builder already caps to what it was told, so
54 // this only fires if that contract is ever broken - it keeps flight+*plen in bounds regardless).
55 if (!written || written > cap - *plen)
56 {
57 fail(qt, TlsAlert::TLS_ALERT_INTERNAL_ERROR);
58 return false;
59 }
60 ssh_sha256_update(&qt->transcript, flight + *plen, written);
61 *plen += written;
62 return true;
63}
64
65bool process_client_hello(QuicTls *qt, const uint8_t *msg, size_t msg_len)
66{
67 Tls13ClientHello ch;
68 if (!tls13_parse_client_hello(msg, msg_len, &ch))
69 {
70 fail(qt, TlsAlert::TLS_ALERT_DECODE_ERROR);
71 return false;
72 }
73 if (!ch.offers_tls13)
74 {
75 fail(qt, TlsAlert::TLS_ALERT_PROTOCOL_VERSION);
76 return false;
77 }
78 bool use_hybrid = false;
79#if DETWS_ENABLE_PQC_KEX
80 // Prefer the PQ/T hybrid whenever the client sent a usable X25519MLKEM768 key_share.
81 use_hybrid = ch.has_hybrid_share && ch.offers_x25519mlkem768;
82#endif
83 if (!ch.offers_ed25519 || (!use_hybrid && (!ch.has_key_share || !ch.offers_x25519)))
84 {
85 fail(qt, TlsAlert::TLS_ALERT_HANDSHAKE_FAILURE);
86 return false;
87 }
88 if (!ch.offers_h3_alpn)
89 {
90 fail(qt, TlsAlert::TLS_ALERT_NO_APPLICATION_PROTOCOL);
91 return false;
92 }
93 if (!ch.quic_tp)
94 {
95 fail(qt, TlsAlert::TLS_ALERT_MISSING_EXTENSION);
96 return false;
97 }
98 if (!quic_tp_parse(ch.quic_tp, ch.quic_tp_len, &qt->peer))
99 {
100 fail(qt, TlsAlert::TLS_ALERT_ILLEGAL_PARAMETER);
101 return false;
102 }
103 qt->have_peer = true;
104
105 // (EC)DHE shared secret + the server's key_share, per negotiated group. The hybrid secret is the
106 // 64-byte ML-KEM_secret || X25519_secret (ML-KEM first, per draft-ietf-tls-ecdhe-mlkem).
107 uint8_t ecdhe[64];
108 size_t ecdhe_len;
109 uint16_t group;
110 size_t share_len;
111#if DETWS_ENABLE_PQC_KEX
112 uint8_t server_share[MLKEM768_CT_BYTES + 32]; // S_CT2(1088) || Q_S(32) for the hybrid
113 if (use_hybrid)
114 {
115 uint8_t ml_ss[32];
116 if (!mlkem768_encaps(ch.client_mlkem_ek, qt->cfg.mlkem_m, server_share, ml_ss))
117 {
118 fail(qt, TlsAlert::TLS_ALERT_HANDSHAKE_FAILURE); // malformed ML-KEM key
119 return false;
120 }
121 uint8_t x_ss[32];
122 uint8_t server_pub[32];
123 ssh_x25519(x_ss, qt->cfg.ephemeral_priv, ch.client_x25519);
124 ssh_x25519_base(server_pub, qt->cfg.ephemeral_priv);
125 memcpy(server_share + MLKEM768_CT_BYTES, server_pub, 32);
126 memcpy(ecdhe, ml_ss, 32);
127 memcpy(ecdhe + 32, x_ss, 32);
128 ecdhe_len = 64;
129 share_len = MLKEM768_CT_BYTES + 32;
130 group = TLS_GROUP_X25519MLKEM768;
131 }
132 else
133#else
134 uint8_t server_share[32];
135#endif
136 {
137 ssh_x25519(ecdhe, qt->cfg.ephemeral_priv, ch.client_x25519);
138 ssh_x25519_base(server_share, qt->cfg.ephemeral_priv);
139 ecdhe_len = 32;
140 share_len = 32;
141 group = TLS_GROUP_X25519;
142 }
143
144 // Transcript starts with the ClientHello.
145 ssh_sha256_update(&qt->transcript, msg, msg_len);
146
147 // ServerHello (Initial-level flight).
148 size_t n = tls13_build_server_hello(qt->flight_initial, sizeof(qt->flight_initial), qt->cfg.random, ch.session_id,
149 ch.session_id_len, server_share, share_len, group);
150 qt->flight_initial_len = 0;
151 if (!emit(qt, qt->flight_initial, sizeof(qt->flight_initial), &qt->flight_initial_len, n))
152 return false; // GCOVR_EXCL_LINE ServerHello always fits flight_initial (classical <=~160B; the
153 // hybrid's ~1.2 KB share fits the PQC-sized 1400B buffer)
154
155 // Handshake keys from Transcript-Hash(ClientHello..ServerHello).
156 uint8_t hash[32];
157 snapshot_hash(&qt->transcript, hash);
158 tls13_ks_early(&TLS13_KDF, &qt->ks);
159 tls13_ks_handshake(&qt->ks, ecdhe, hash, ecdhe_len);
160 quic_keys_from_secret(qt->ks.client_hs_traffic, &qt->hs_client);
161 quic_keys_from_secret(qt->ks.server_hs_traffic, &qt->hs_server);
162 qt->hs_keys_ready = true;
163
164 // Handshake-level flight: EncryptedExtensions, Certificate, CertificateVerify, Finished.
165 qt->flight_hs_len = 0;
166 uint8_t tp_enc[512];
167 size_t tp_len = quic_tp_encode(&qt->cfg.params, tp_enc, sizeof(tp_enc));
168
169 n = tls13_build_encrypted_extensions(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len,
170 tp_enc, tp_len);
171 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
172 return false; // GCOVR_EXCL_LINE EncryptedExtensions (tp <= tp_enc[512]) always fits flight_hs[2048]
173
174 n = tls13_build_certificate(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len,
175 qt->cfg.cert_der, qt->cfg.cert_len);
176 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
177 return false;
178
179 // CertificateVerify signs Transcript-Hash(ClientHello..Certificate).
180 snapshot_hash(&qt->transcript, hash);
181 n = tls13_build_cert_verify(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len, hash,
182 qt->cfg.ed25519_seed);
183 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
184 return false;
185
186 // Server Finished over Transcript-Hash(ClientHello..CertificateVerify).
187 snapshot_hash(&qt->transcript, hash);
188 uint8_t verify[32];
189 tls13_finished_mac(&TLS13_KDF, qt->ks.server_hs_traffic, hash, verify);
190 n = tls13_build_finished(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len, verify);
191 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
192 return false;
193
194 // 1-RTT keys from Transcript-Hash(ClientHello..server Finished); also the hash we verify the
195 // client Finished against.
196 snapshot_hash(&qt->transcript, qt->hs_finished_hash);
197 tls13_ks_master(&qt->ks, qt->hs_finished_hash);
198 quic_keys_from_secret(qt->ks.client_ap_traffic, &qt->ap_client);
199 quic_keys_from_secret(qt->ks.server_ap_traffic, &qt->ap_server);
200 qt->ap_keys_ready = true;
201
202 qt->state = QtlsState::QTLS_WAIT_FINISHED;
203 return true;
204}
205
206bool process_client_finished(QuicTls *qt, const uint8_t *msg, size_t msg_len)
207{
208 if (msg[0] != TlsHs::TLS_HS_FINISHED || msg_len != 4 + 32)
209 {
210 fail(qt, TlsAlert::TLS_ALERT_DECODE_ERROR);
211 return false;
212 }
213 uint8_t expected[32];
214 tls13_finished_mac(&TLS13_KDF, qt->ks.client_hs_traffic, qt->hs_finished_hash, expected);
215 uint8_t diff = 0;
216 for (int i = 0; i < 32; i++)
217 diff |= (uint8_t)(expected[i] ^ msg[4 + i]);
218 if (diff)
219 {
220 fail(qt, TlsAlert::TLS_ALERT_DECRYPT_ERROR);
221 return false;
222 }
223 ssh_sha256_update(&qt->transcript, msg, msg_len);
224 qt->complete = true;
225 qt->state = QtlsState::QTLS_DONE;
226 return true;
227}
228
229bool process_message(QuicTls *qt, int level, const uint8_t *msg, size_t msg_len)
230{
231 if (level == QuicEnc::QUIC_ENC_INITIAL && qt->state == QtlsState::QTLS_START &&
232 msg[0] == TlsHs::TLS_HS_CLIENT_HELLO)
233 return process_client_hello(qt, msg, msg_len);
234 if (level == QuicEnc::QUIC_ENC_HANDSHAKE && qt->state == QtlsState::QTLS_WAIT_FINISHED &&
235 msg[0] == TlsHs::TLS_HS_FINISHED)
236 return process_client_finished(qt, msg, msg_len);
237 fail(qt, TlsAlert::TLS_ALERT_UNEXPECTED_MESSAGE);
238 return false;
239}
240} // namespace
241
242void quic_tls_server_init(QuicTls *qt, const QuicTlsConfig *cfg)
243{
244 memset(qt, 0, sizeof(*qt));
245 qt->cfg = *cfg;
246 ssh_sha256_init(&qt->transcript);
247 qt->state = QtlsState::QTLS_START;
248}
249
250size_t quic_tls_recv_crypto(QuicTls *qt, int level, const uint8_t *data, size_t len)
251{
252 if (qt->state == QtlsState::QTLS_FAILED)
253 return len; // drain; the connection is closing
254 size_t off = 0;
255 while (off + 4 <= len)
256 {
257 uint32_t mlen = (uint32_t)((data[off + 1] << 16) | (data[off + 2] << 8) | data[off + 3]);
258 size_t total = 4 + mlen;
259 if (off + total > len)
260 break; // an incomplete trailing message; wait for more bytes
261 if (!process_message(qt, level, data + off, total))
262 return off + total; // consumed through the offending message; state is FAILED/handled
263 off += total;
264 if (qt->state == QtlsState::QTLS_DONE)
265 break;
266 }
267 return off;
268}
269
270const uint8_t *quic_tls_flight(const QuicTls *qt, int level, size_t *len)
271{
272 if (level == QuicEnc::QUIC_ENC_INITIAL)
273 {
274 *len = qt->flight_initial_len;
275 return qt->flight_initial;
276 }
277 if (level == QuicEnc::QUIC_ENC_HANDSHAKE)
278 {
279 *len = qt->flight_hs_len;
280 return qt->flight_hs;
281 }
282 *len = 0;
283 return nullptr;
284}
285
286const QuicPacketKeys *quic_tls_keys(const QuicTls *qt, int level, bool is_server)
287{
288 if (level == QuicEnc::QUIC_ENC_HANDSHAKE && qt->hs_keys_ready)
289 return is_server ? &qt->hs_server : &qt->hs_client;
290 if (level == QuicEnc::QUIC_ENC_APP && qt->ap_keys_ready)
291 return is_server ? &qt->ap_server : &qt->ap_client;
292 return nullptr;
293}
294
295const QuicTransportParams *quic_tls_peer_params(const QuicTls *qt)
296{
297 return qt->have_peer ? &qt->peer : nullptr;
298}
299
300#endif // DETWS_ENABLE_HTTP3
ML-KEM-768 encapsulation (FIPS 203), responder side only.
TLS 1.3 server handshake state machine for QUIC (RFC 9001 / RFC 8446).
void ssh_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
void ssh_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
void ssh_sha256_init(SshSha256Ctx *ctx)
Initialize a streaming SHA-256 context.
void ssh_sha256_update(SshSha256Ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
void ssh_sha256_final(SshSha256Ctx *ctx, uint8_t digest[SSH_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest.
Streaming SHA-256 context.
Definition ssh_sha256.h:44
TLS 1.3 handshake messages for the QUIC handshake (RFC 8446 sec 4).