ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 pc_quic_tls.cpp
6 * @brief TLS 1.3 server handshake state machine for QUIC (see pc_quic_tls.h).
7 */
8
10
11#if PC_ENABLE_HTTP3
12
15#if PC_ENABLE_PQC_KEX
16#include "crypto/pqc/mlkem.h" // pc_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{
36/// Capacity of the encoded-transport-parameters scratch the EncryptedExtensions builder is fed.
37#define PC_QUIC_TLS_TP_ENC_CAP 512
38
39/// Largest EncryptedExtensions this module can emit: handshake header (4) + extensions length (2)
40/// + the ALPN "h3" extension (9) + the transport-parameters extension header (4) + the parameters.
41#define PC_QUIC_TLS_EE_MAX (4 + 2 + 9 + 4 + PC_QUIC_TLS_TP_ENC_CAP)
42
43// EncryptedExtensions is the FIRST message written into flight_hs (flight_hs_len is reset to 0
44// immediately before it), so its emit() cannot overflow - which is why that failure path carries a
45// coverage exclusion. That only holds while flight_hs is at least one whole EncryptedExtensions,
46// and PC_H3_CRYPTO_BUF is an overridable macro (protocore_config.h), so pin the relationship here:
47// a build that shrank it would silently make the excluded path reachable.
48static_assert(PC_H3_CRYPTO_BUF >= PC_QUIC_TLS_EE_MAX,
49 "PC_H3_CRYPTO_BUF (QuicTls::flight_hs) must hold a whole EncryptedExtensions: the fixed "
50 "512-byte transport-parameter buffer plus the ALPN and extension framing");
51
52void fail(QuicTls *qt, uint8_t alert)
53{
54 qt->state = QtlsState::QTLS_FAILED;
55 qt->alert = alert;
56}
57
58// Snapshot the running Transcript-Hash without disturbing it (pc_sha256_ctx is copyable state).
59void snapshot_hash(const pc_sha256_ctx *ctx, uint8_t out[32])
60{
61 pc_sha256_ctx tmp = *ctx;
62 pc_sha256_final(&tmp, out);
63}
64
65// Append a handshake message to both the outbound flight buffer and the transcript.
66bool emit(QuicTls *qt, uint8_t *flight, size_t cap, size_t *plen, size_t written)
67{
68 // *plen <= cap is the invariant this append maintains, so cap - *plen never underflows; refuse a
69 // write that would run past the flight buffer (each builder already caps to what it was told, so
70 // this only fires if that contract is ever broken - it keeps flight+*plen in bounds regardless).
71 if (!written || written > cap - *plen) // GCOVR_EXCL_LINE every caller passes the builder exactly cap - *plen
72 { // as its own capacity, so a non-zero return can never exceed it
73 fail(qt, TlsAlert::TLS_ALERT_INTERNAL_ERROR);
74 return false;
75 }
76 pc_sha256_update(&qt->transcript, flight + *plen, written);
77 *plen += written;
78 return true;
79}
80
81#if PC_ENABLE_PQC_KEX
82// Emit a HelloRetryRequest (RFC 8446 §4.1.4) asking the client to retry with an X25519MLKEM768
83// key_share, and restart the transcript per §4.4.1: message_hash(Hash(ClientHello1)) || HRR, so the
84// eventual transcript is message_hash || HRR || ClientHello2 || ServerHello || ... QUIC does its own
85// return-routability (Retry tokens), so the HRR carries no cookie. @p msg is ClientHello1.
86bool send_hello_retry(QuicTls *qt, const uint8_t *msg, size_t msg_len, const Tls13ClientHello *ch)
87{
88 uint8_t ch1_hash[32];
89 {
92 pc_sha256_update(&t, msg, msg_len);
93 pc_sha256_final(&t, ch1_hash);
94 }
95 pc_sha256_init(&qt->transcript);
96 uint8_t mh[40];
97 size_t mhn = pc_tls13_build_message_hash(mh, sizeof(mh), ch1_hash);
98 if (!mhn) // GCOVR_EXCL_LINE mh[40] always fits the 36-byte hash
99 {
100 fail(qt, TlsAlert::TLS_ALERT_INTERNAL_ERROR); // GCOVR_EXCL_LINE
101 return false; // GCOVR_EXCL_LINE
102 }
103 pc_sha256_update(&qt->transcript, mh, mhn); // message_hash is transcript-only, never sent
104
105 qt->flight_initial_len = 0;
106 size_t n = pc_tls13_build_hello_retry_request(qt->flight_initial, sizeof(qt->flight_initial), ch->session_id,
107 ch->session_id_len, TLS_GROUP_X25519MLKEM768, nullptr, 0,
108 /*dtls=*/false);
109 if (!emit(qt, qt->flight_initial, sizeof(qt->flight_initial), &qt->flight_initial_len, n)) // GCOVR_EXCL_LINE
110 {
111 return false; // GCOVR_EXCL_LINE the HRR (~50B) always fits flight_initial (>=256B)
112 }
113 qt->hrr_sent = true;
114 return true; // stay in QTLS_START, awaiting ClientHello2 at the Initial level
115}
116#endif
117
118bool process_client_hello(QuicTls *qt, const uint8_t *msg, size_t msg_len)
119{
120 Tls13ClientHello ch;
121 if (!pc_tls13_parse_client_hello(msg, msg_len, &ch))
122 {
123 fail(qt, TlsAlert::TLS_ALERT_DECODE_ERROR);
124 return false;
125 }
126 if (!ch.offers_tls13)
127 {
128 fail(qt, TlsAlert::TLS_ALERT_PROTOCOL_VERSION);
129 return false;
130 }
131 bool use_hybrid = false;
132#if PC_ENABLE_PQC_KEX
133 // Prefer the PQ/T hybrid whenever the client sent a usable X25519MLKEM768 key_share.
134 use_hybrid = ch.has_hybrid_share && ch.offers_x25519mlkem768;
135 // The client offered X25519MLKEM768 but sent only a classical key_share: ask it (once) to retry with
136 // the hybrid share rather than silently downgrading to X25519 (RFC 8446 §4.1.4).
137 // GCOVR_EXCL_LINE below: !ch.has_hybrid_share is implied by the two operands before it - use_hybrid is
138 // has_hybrid_share && offers_x25519mlkem768, so reaching it with the share present is impossible.
139 if (!use_hybrid && ch.offers_x25519mlkem768 && !ch.has_hybrid_share && !qt->hrr_sent) // GCOVR_EXCL_LINE
140 {
141 return send_hello_retry(qt, msg, msg_len, &ch);
142 }
143 // A retry that still lacks the hybrid share is fatal - one HRR only, so a client cannot loop us.
144 if (qt->hrr_sent && !use_hybrid)
145 {
146 fail(qt, TlsAlert::TLS_ALERT_HANDSHAKE_FAILURE);
147 return false;
148 }
149#endif
150 if (!ch.offers_ed25519 || (!use_hybrid && (!ch.has_key_share || !ch.offers_x25519)))
151 {
152 fail(qt, TlsAlert::TLS_ALERT_HANDSHAKE_FAILURE);
153 return false;
154 }
155 if (!ch.offers_h3_alpn)
156 {
157 fail(qt, TlsAlert::TLS_ALERT_NO_APPLICATION_PROTOCOL);
158 return false;
159 }
160 if (!ch.pc_quic_tp)
161 {
162 fail(qt, TlsAlert::TLS_ALERT_MISSING_EXTENSION);
163 return false;
164 }
165 if (!pc_quic_tp_parse(ch.pc_quic_tp, ch.pc_quic_tp_len, &qt->peer))
166 {
167 fail(qt, TlsAlert::TLS_ALERT_ILLEGAL_PARAMETER);
168 return false;
169 }
170 qt->have_peer = true;
171
172 // (EC)DHE shared secret + the server's key_share, per negotiated group. The hybrid secret is the
173 // 64-byte ML-KEM_secret || X25519_secret (ML-KEM first, per draft-ietf-tls-ecdhe-mlkem).
174 uint8_t ecdhe[64];
175 size_t ecdhe_len;
176 uint16_t group;
177 size_t share_len;
178#if PC_ENABLE_PQC_KEX
179 uint8_t server_share[MLKEM768_CT_BYTES + 32]; // S_CT2(1088) || Q_S(32) for the hybrid
180 if (use_hybrid)
181 {
182 uint8_t ml_ss[32];
183 if (!pc_mlkem768_encaps(ch.client_mlkem_ek, qt->cfg.mlkem_m, server_share, ml_ss))
184 {
185 fail(qt, TlsAlert::TLS_ALERT_HANDSHAKE_FAILURE); // malformed ML-KEM key
186 return false;
187 }
188 uint8_t x_ss[32];
189 uint8_t server_pub[32];
190 pc_x25519(x_ss, qt->cfg.ephemeral_priv, ch.client_x25519);
191 pc_x25519_base(server_pub, qt->cfg.ephemeral_priv);
192 memcpy(server_share + MLKEM768_CT_BYTES, server_pub, 32);
193 memcpy(ecdhe, ml_ss, 32);
194 memcpy(ecdhe + 32, x_ss, 32);
195 ecdhe_len = 64;
196 share_len = MLKEM768_CT_BYTES + 32;
197 group = TLS_GROUP_X25519MLKEM768;
198 }
199 else
200#else
201 uint8_t server_share[32];
202#endif
203 {
204 pc_x25519(ecdhe, qt->cfg.ephemeral_priv, ch.client_x25519);
205 pc_x25519_base(server_share, qt->cfg.ephemeral_priv);
206 ecdhe_len = 32;
207 share_len = 32;
208 group = TLS_GROUP_X25519;
209 }
210
211 // Fold the ClientHello into the transcript. On the happy path it is the first message; after a
212 // HelloRetryRequest the transcript already holds message_hash || HRR, so this is ClientHello2.
213 pc_sha256_update(&qt->transcript, msg, msg_len);
214
215 // ServerHello (Initial-level flight). The Initial CRYPTO is one contiguous byte stream, so after a
216 // HelloRetryRequest the ServerHello is appended after the HRR already in flight_initial - build at the
217 // current offset (0 on the happy path, the HRR's end on a retry) and do not reset the length.
218 size_t n = pc_tls13_build_server_hello(qt->flight_initial + qt->flight_initial_len,
219 sizeof(qt->flight_initial) - qt->flight_initial_len, qt->cfg.random,
220 ch.session_id, ch.session_id_len, server_share, share_len, group);
221 if (!emit(qt, qt->flight_initial, sizeof(qt->flight_initial), &qt->flight_initial_len, n)) // GCOVR_EXCL_LINE
222 {
223 return false; // GCOVR_EXCL_LINE ServerHello always fits flight_initial (classical <=~160B; the
224 // hybrid's ~1.2 KB share fits the PQC-sized 1400B buffer)
225 }
226
227 // Handshake keys from Transcript-Hash(ClientHello..ServerHello).
228 uint8_t hash[32];
229 snapshot_hash(&qt->transcript, hash);
230 pc_tls13_ks_early(&TLS13_KDF, &qt->ks);
231 pc_tls13_ks_handshake(&qt->ks, ecdhe, hash, ecdhe_len);
232 pc_quic_keys_from_secret(qt->ks.client_hs_traffic, &qt->hs_client);
233 pc_quic_keys_from_secret(qt->ks.server_hs_traffic, &qt->hs_server);
234 qt->hs_keys_ready = true;
235
236 // Handshake-level flight: EncryptedExtensions, Certificate, CertificateVerify, Finished.
237 qt->flight_hs_len = 0;
238 uint8_t tp_enc[PC_QUIC_TLS_TP_ENC_CAP];
239 size_t tp_len = pc_quic_tp_encode(&qt->cfg.params, tp_enc, sizeof(tp_enc));
240
241 n = pc_tls13_build_encrypted_extensions(qt->flight_hs + qt->flight_hs_len,
242 sizeof(qt->flight_hs) - qt->flight_hs_len, tp_enc, tp_len);
243 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n)) // GCOVR_EXCL_LINE
244 {
245 return false; // GCOVR_EXCL_LINE EncryptedExtensions is the first message written into flight_hs and
246 // PC_H3_CRYPTO_BUF >= PC_QUIC_TLS_EE_MAX is pinned by the static_assert above
247 }
248
249 n = pc_tls13_build_certificate(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len,
250 qt->cfg.cert_der, qt->cfg.cert_len);
251 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
252 {
253 return false;
254 }
255
256 // CertificateVerify signs Transcript-Hash(ClientHello..Certificate).
257 snapshot_hash(&qt->transcript, hash);
258 n = pc_tls13_build_cert_verify(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len, hash,
259 qt->cfg.ed25519_seed);
260 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
261 {
262 return false;
263 }
264
265 // Server Finished over Transcript-Hash(ClientHello..CertificateVerify).
266 snapshot_hash(&qt->transcript, hash);
267 uint8_t verify[32];
268 pc_tls13_finished_mac(&TLS13_KDF, qt->ks.server_hs_traffic, hash, verify);
269 n = pc_tls13_build_finished(qt->flight_hs + qt->flight_hs_len, sizeof(qt->flight_hs) - qt->flight_hs_len, verify);
270 if (!emit(qt, qt->flight_hs, sizeof(qt->flight_hs), &qt->flight_hs_len, n))
271 {
272 return false;
273 }
274
275 // 1-RTT keys from Transcript-Hash(ClientHello..server Finished); also the hash we verify the
276 // client Finished against.
277 snapshot_hash(&qt->transcript, qt->hs_finished_hash);
278 pc_tls13_ks_master(&qt->ks, qt->hs_finished_hash);
279 pc_quic_keys_from_secret(qt->ks.client_ap_traffic, &qt->ap_client);
280 pc_quic_keys_from_secret(qt->ks.server_ap_traffic, &qt->ap_server);
281 qt->ap_keys_ready = true;
282
283 qt->state = QtlsState::QTLS_WAIT_FINISHED;
284 return true;
285}
286
287bool process_client_finished(QuicTls *qt, const uint8_t *msg, size_t msg_len)
288{
289 if (msg[0] != TlsHs::TLS_HS_FINISHED || msg_len != 4 + 32) // GCOVR_EXCL_LINE process_message only routes a
290 { // Finished here, so the type arm cannot be taken
291 fail(qt, TlsAlert::TLS_ALERT_DECODE_ERROR);
292 return false;
293 }
294 uint8_t expected[32];
295 pc_tls13_finished_mac(&TLS13_KDF, qt->ks.client_hs_traffic, qt->hs_finished_hash, expected);
296 uint8_t diff = 0;
297 for (int i = 0; i < 32; i++)
298 {
299 diff |= (uint8_t)(expected[i] ^ msg[4 + i]);
300 }
301 if (diff)
302 {
303 fail(qt, TlsAlert::TLS_ALERT_DECRYPT_ERROR);
304 return false;
305 }
306 pc_sha256_update(&qt->transcript, msg, msg_len);
307 qt->complete = true;
308 qt->state = QtlsState::QTLS_DONE;
309 return true;
310}
311
312bool process_message(QuicTls *qt, int level, const uint8_t *msg, size_t msg_len)
313{
314 if (level == QuicEnc::QUIC_ENC_INITIAL && qt->state == QtlsState::QTLS_START &&
315 msg[0] == TlsHs::TLS_HS_CLIENT_HELLO)
316 {
317 return process_client_hello(qt, msg, msg_len);
318 }
319 if (level == QuicEnc::QUIC_ENC_HANDSHAKE && qt->state == QtlsState::QTLS_WAIT_FINISHED &&
320 msg[0] == TlsHs::TLS_HS_FINISHED)
321 {
322 return process_client_finished(qt, msg, msg_len);
323 }
324 fail(qt, TlsAlert::TLS_ALERT_UNEXPECTED_MESSAGE);
325 return false;
326}
327} // namespace
328
329void pc_quic_tls_server_init(QuicTls *qt, const QuicTlsConfig *cfg)
330{
331 memset(qt, 0, sizeof(*qt));
332 qt->cfg = *cfg;
333 pc_sha256_init(&qt->transcript);
334 qt->state = QtlsState::QTLS_START;
335}
336
337size_t pc_quic_tls_recv_crypto(QuicTls *qt, int level, const uint8_t *data, size_t len)
338{
339 if (qt->state == QtlsState::QTLS_FAILED)
340 {
341 return len; // drain; the connection is closing
342 }
343 size_t off = 0;
344 while (off + 4 <= len)
345 {
346 uint32_t mlen = (uint32_t)((data[off + 1] << 16) | (data[off + 2] << 8) | data[off + 3]);
347 size_t total = 4 + mlen;
348 if (off + total > len)
349 {
350 break; // an incomplete trailing message; wait for more bytes
351 }
352 if (!process_message(qt, level, data + off, total))
353 {
354 return off + total; // consumed through the offending message; state is FAILED/handled
355 }
356 off += total;
357 if (qt->state == QtlsState::QTLS_DONE)
358 {
359 break;
360 }
361 }
362 return off;
363}
364
365const uint8_t *pc_quic_tls_flight(const QuicTls *qt, int level, size_t *len)
366{
367 if (level == QuicEnc::QUIC_ENC_INITIAL)
368 {
369 *len = qt->flight_initial_len;
370 return qt->flight_initial;
371 }
372 if (level == QuicEnc::QUIC_ENC_HANDSHAKE)
373 {
374 *len = qt->flight_hs_len;
375 return qt->flight_hs;
376 }
377 *len = 0;
378 return nullptr;
379}
380
381QuicPacketKeys *pc_quic_tls_keys(QuicTls *qt, int level, bool is_server)
382{
383 if (level == QuicEnc::QUIC_ENC_HANDSHAKE && qt->hs_keys_ready)
384 {
385 return is_server ? &qt->hs_server : &qt->hs_client;
386 }
387 if (level == QuicEnc::QUIC_ENC_APP && qt->ap_keys_ready)
388 {
389 return is_server ? &qt->ap_server : &qt->ap_client;
390 }
391 return nullptr;
392}
393
394const QuicTransportParams *pc_quic_tls_peer_params(const QuicTls *qt)
395{
396 return qt->have_peer ? &qt->peer : nullptr;
397}
398
399#endif // PC_ENABLE_HTTP3
void pc_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32])
X25519 scalar multiplication: out = scalar * point (RFC 7748 §5).
void pc_x25519_base(uint8_t out[32], const uint8_t scalar[32])
X25519 with the standard base point u=9: out = scalar * G.
Curve25519 field arithmetic + X25519 (RFC 7748) for the curve25519-sha256 KEX.
ML-KEM-768 (FIPS 203): Encaps (responder) + KeyGen and Decaps (initiator).
#define PC_H3_CRYPTO_BUF
Maximum bytes of one QUIC/TLS handshake CRYPTO flight (RFC 9001).
PC_CRYPTO_HOT void pc_sha256_init(pc_sha256_ctx *ctx)
Initialize a streaming SHA-256 context (ctx must not be NULL).
Definition sha256.cpp:29
void pc_sha256_final(pc_sha256_ctx *ctx, uint8_t digest[PC_SHA256_DIGEST_LEN])
Finalize the hash and write the 32-byte digest. The context is undefined afterwards; call init() agai...
Definition sha256.cpp:48
void pc_sha256_update(pc_sha256_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
Definition sha256.cpp:39
Streaming SHA-256 context.
Definition sha256.h:40