ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
key_schedule.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file key_schedule.h
6 * @brief TLS 1.3 key schedule (RFC 8446 sec 7.1) for the QUIC handshake.
7 *
8 * QUIC runs TLS 1.3 as its handshake protocol (RFC 9001), and mbedTLS exposes no QUIC-TLS callback
9 * API, so the handshake is hand-rolled here. This module is the key schedule: the chain of
10 * HKDF-Extract and Derive-Secret steps (RFC 8446 sec 7.1) that turns the (EC)DHE shared secret and
11 * the running handshake transcript hash into the traffic secrets for each encryption level, plus the
12 * per-message Finished MAC (sec 4.4.4).
13 *
14 * RFC 8446 sec 7.1 keys the whole schedule off the negotiated cipher suite's hash, so a schedule
15 * binds SHA-256 or SHA-384 at @ref Tls13KsNs::early and every secret is 32 or 48 bytes from there on.
16 * The term layout is stated at the wider of the two (@ref TLS13_SECRET_MAX) so a connection's storage
17 * does not depend on what it negotiates, and the length actually in force is read back off @ref
18 * Tls13KsNs::len rather than assumed.
19 *
20 * The schedule is transcript-hash-driven: each step takes a Transcript-Hash over the handshake
21 * messages so far, so this module has no dependency on the message wire formats and is host-testable
22 * in isolation against the RFC 8448 sec 3 worked trace (which lists every intermediate secret and the
23 * (EC)DHE input directly). RFC 8446 sec 4.4.1 runs that hash under the same suite hash as the
24 * schedule, so @ref Tls13KsNs::transcript_init and its two companions keep it here, in a borrow the
25 * caller owns, and a driver never names a hash to keep a transcript. The QUIC packet-protection keys ({key, iv, hp})
26 * are then derived from these traffic secrets by QuicCrypto.keys_from_secret (RFC 9001 sec 5.1).
27 *
28 * Pure, zero heap, host-tested against RFC 8448 sec 3.
29 *
30 * @author Douglas Quigg (dstroy0)
31 * @date 2026
32 */
33
34#ifndef PROTOCORE_TLS_KEY_SCHEDULE_H
35#define PROTOCORE_TLS_KEY_SCHEDULE_H
36#include "protocore_config.h" // the entry point: the enable gate below, and the widths
37
38#if (PROTOCORE_ENABLE_HTTP3 || PROTOCORE_ENABLE_DTLS || PROTOCORE_ENABLE_TLS)
39
41
42// Shared by the HTTP/3 (QUIC) handshake and the DTLS 1.3 handshake - both run the same TLS 1.3 key
43// schedule (see protocore_tls13_msg.h for the matching guard on the message layer).
44
45/** @brief Longest secret the two schedule hashes produce (SHA-384). The term layout is stated at this
46 * width; the length a schedule actually derives is @ref Tls13KsNs::len. */
47#define TLS13_SECRET_MAX PROTOCORE_TLS13_SECRET_MAX
48
49/** @brief SHA-256 secret length: what a TLS_AES_128_GCM_SHA256 schedule binds. */
50#define TLS13_SECRET_SHA256 32
51
52/** @brief SHA-384 secret length: what a TLS_AES_256_GCM_SHA384 schedule binds. */
53#define TLS13_SECRET_SHA384 48
54
55/**
56 * @brief The one thing that differs between the TLS 1.3 and DTLS 1.3 key schedules: the
57 * HKDF-Expand-Label prefix ("tls13 " for TLS/QUIC per RFC 8446 sec 7.1, "dtls13" for DTLS 1.3 per
58 * RFC 9147 sec 5.9). A caller picks the variant once (@ref TLS13_KDF or @ref DTLS13_KDF) and the
59 * key schedule carries it, so no per-call flag is threaded through the derivation steps.
60 */
61typedef struct
62{
63 const char *label_prefix;
64} Tls13Kdf;
65
66/** @brief TLS 1.3 / QUIC variant ("tls13 " prefix, RFC 8446). */
67extern const Tls13Kdf TLS13_KDF;
68/** @brief DTLS 1.3 variant ("dtls13" prefix, RFC 9147 sec 5.9). */
69extern const Tls13Kdf DTLS13_KDF;
70
71/**
72 * @brief The running key-schedule state for one handshake (server side).
73 *
74 * Filled in three steps as the handshake progresses: @ref Tls13KsNs::early (which also binds the @ref
75 * Tls13Kdf variant) before any (EC)DHE, @ref Tls13KsNs::handshake once ClientHello..ServerHello is hashed
76 * and the shared secret is known, and @ref Tls13KsNs::master once ClientHello..server Finished is hashed.
77 * Each step also derives that level's client and server traffic secrets, from which the record/packet
78 * keys are made.
79 */
80// Offsets into Tls13KeySchedule::s. Each term is one TLS13_SECRET_MAX slot, so a term sits at the
81// same address whichever hash the schedule bound and only the bytes written inside it differ.
82#define TLS13_KS_EARLY 0 ///< HKDF-Extract(0, PSK|0) - no-PSK: Extract(0, 0^L)
83#define TLS13_KS_HANDSHAKE TLS13_SECRET_MAX ///< HKDF-Extract(Derive(early,"derived"), (EC)DHE)
84#define TLS13_KS_MASTER (2 * TLS13_SECRET_MAX) ///< HKDF-Extract(Derive(handshake,"derived"), 0^L)
85#define TLS13_KS_CLIENT_HS (3 * TLS13_SECRET_MAX) ///< Derive-Secret(handshake, "c hs traffic", CH..SH)
86#define TLS13_KS_SERVER_HS (4 * TLS13_SECRET_MAX) ///< Derive-Secret(handshake, "s hs traffic", CH..SH)
87#define TLS13_KS_CLIENT_AP (5 * TLS13_SECRET_MAX) ///< Derive-Secret(master, "c ap traffic", CH..SFIN)
88#define TLS13_KS_SERVER_AP (6 * TLS13_SECRET_MAX) ///< Derive-Secret(master, "s ap traffic", CH..SFIN)
89#define TLS13_KS_EMPTY_HASH (7 * TLS13_SECRET_MAX) ///< Transcript-Hash("")
90#define TLS13_KS_DERIVED (8 * TLS13_SECRET_MAX) ///< Derive-Secret(X, "derived", "")
91#define TLS13_KS_FINISHED_KEY (9 * TLS13_SECRET_MAX) ///< HKDF-Expand-Label(traffic, "finished", "", L)
92#define TLS13_KS_ZEROS (10 * TLS13_SECRET_MAX) ///< 0^Hash.length, never written: the borrow arrives zeroed
93#define TLS13_KS_VERIFY (11 * TLS13_SECRET_MAX) ///< the Finished verify_data this end built or expects
94#define PROTOCORE_TLS13_KS_CAP ((size_t)PROTOCORE_TLS13_KS_TERMS * TLS13_SECRET_MAX)
95#define TLS13_KS_WORK PROTOCORE_TLS13_KS_CAP ///< past the terms: the bytes this schedule's HKDF works out of
96
97typedef struct
98{
99 const Tls13Kdf *kdf; ///< variant (label prefix) bound by @ref Tls13KsNs::early
100 uint8_t *s; ///< PROTOCORE_TLS13_KS_BORROW secure bytes: the terms, then the HKDF's own
101 size_t len; ///< the bound hash's secret length, 32 or 48
102 proto_bool is384; ///< true when the suite's hash is SHA-384
103} Tls13KeySchedule;
104
105/** @brief The variant a call runs under, and the schedule it advances. */
106typedef struct
107{
108 const Tls13Kdf *kdf; ///< variant (label prefix) an early step binds
109 Tls13KeySchedule *ks; ///< the schedule a step advances
110 uint8_t *s; ///< PROTOCORE_TLS13_KS_BORROW secure bytes the schedule runs out of
111 proto_bool is384; ///< the negotiated suite's hash: true for SHA-384, false for SHA-256
112} Tls13KsBind;
113
114/** @brief RFC 8446 sec 7.1 HKDF-Expand-Label / Derive-Secret: one derivation's terms. */
115typedef struct
116{
117 uint8_t *work; ///< PROTOCORE_HKDF_BORROW bytes of caller storage
118 const uint8_t *secret; ///< a 32-byte PRK or traffic secret
119 const char *label; ///< short label without the prefix, e.g. "c hs traffic", "derived"
120 const uint8_t *transcript_hash; ///< Transcript-Hash of the relevant messages; H("") for "derived"
121 uint8_t *out; ///< where the derived bytes land
122 size_t out_len; ///< how many
123} Tls13KsDeriveArgs;
124
125/** @brief RFC 8446 sec 7.1 steps 2 and 3: the (EC)DHE input, and the transcript each level is keyed off. */
126typedef struct
127{
128 const uint8_t *ecdhe; ///< the (EC)DHE shared secret
129 size_t ecdhe_len; ///< 32 for X25519, 64 for the X25519MLKEM768 hybrid
130 const uint8_t *ch_sh_hash; ///< Transcript-Hash of ClientHello..ServerHello
131 const uint8_t *ch_sfin_hash; ///< Transcript-Hash of ClientHello..server Finished
132} Tls13KsStepArgs;
133
134/** @brief RFC 8446 sec 4.4.4: what the Finished verify_data is taken over, and where it lands. */
135typedef struct
136{
137 const uint8_t *base_secret; ///< the Finished sender's handshake traffic secret
138 const uint8_t *transcript_hash; ///< the handshake up to but excluding this Finished
139 uint8_t *out; ///< @ref Tls13KsNs::len bytes of verify_data
140} Tls13FinishedArgs;
141
142/** @brief RFC 8446 sec 4.4.1 Transcript-Hash: the bytes one update absorbs, and where a peek lands. */
143typedef struct
144{
145 const uint8_t *data; ///< the handshake message bytes absorbed, header included
146 size_t len; ///< how many
147 uint8_t *out; ///< where a peek writes @ref Tls13KsNs::len octets of digest
148} Tls13TranscriptArgs;
149
150/**
151 * @brief The TLS 1.3 key schedule (RFC 8446 sec 7.1).
152 *
153 * A caller sets the members a call takes, invokes it through ::Tls13Ks, and reads the outcome off the
154 * same handle. The schedule itself is the caller's ::Tls13KeySchedule, named in @ref Tls13KsNs::bind.
155 *
156 * @var Tls13KsNs::bind the variant a call runs under, and the schedule it advances
157 * @var Tls13KsNs::derive_args one derivation's terms
158 * @var Tls13KsNs::step the (EC)DHE input and the transcript each level is keyed off
159 * @var Tls13KsNs::finished_args what the Finished verify_data is taken over
160 * @var Tls13KsNs::ok a call's true/false outcome
161 * @var Tls13KsNs::len the bound hash's secret length, 32 or 48, from @ref Tls13KsNs::early
162 * @var Tls13KsNs::expand_label HKDF-Expand-Label under the bound variant's prefix and hash
163 * @var Tls13KsNs::derive_secret Derive-Secret: Expand-Label over a transcript hash, @c len bytes out
164 * @var Tls13KsNs::early step 1: bind the variant, the hash and the borrow, then early_secret
165 * @var Tls13KsNs::handshake step 2: handshake_secret and the handshake traffic secrets
166 * @var Tls13KsNs::master step 3: master_secret and the application traffic secrets
167 * @var Tls13KsNs::finished_mac the Finished verify_data (sec 4.4.4)
168 * @var Tls13KsNs::transcript_args the bytes one update absorbs, and where a peek lands
169 * @var Tls13KsNs::transcript_init start a Transcript-Hash under the bound hash, in @c work
170 * @var Tls13KsNs::transcript_update absorb one handshake message into it
171 * @var Tls13KsNs::transcript_peek the digest so far, without disturbing the running context
172 *
173 * @ref Tls13KsBind::s is bytes the CONNECTION owns and holds for exactly as long as it lives, so the
174 * schedule dies with it. It must arrive zeroed: TLS13_KS_ZEROS is the first extract's IKM and nothing
175 * ever writes it. A null @c s leaves @ref Tls13KsNs::early false and every later step a no-op.
176 *
177 * No storage member: the steps read their operands and the schedule's own borrow, and hold nothing.
178 */
179typedef struct
180{
181 Tls13KsBind bind;
182 Tls13KsDeriveArgs derive_args;
183 Tls13KsStepArgs step;
184 Tls13FinishedArgs finished_args;
185 Tls13TranscriptArgs transcript_args;
186 proto_bool ok;
187 size_t len;
188} Tls13KsVars;
189
190/** @brief The operands and the outcome. */
191extern Tls13KsVars Tls13KsV;
192
193/** @brief The entries. */
194typedef struct
195{
196 void (*const expand_label)(uint8_t *work);
197 void (*const derive_secret)(uint8_t *work);
198 void (*const early)(uint8_t *work);
199 void (*const handshake)(uint8_t *work);
200 void (*const master)(uint8_t *work);
201 void (*const finished_mac)(uint8_t *work);
202 void (*const transcript_init)(uint8_t *work);
203 void (*const transcript_update)(uint8_t *work);
204 void (*const transcript_peek)(uint8_t *work);
205} Tls13KsNs;
206
207// What the table binds, defined once in the .c and taking one parameter each: everything
208// else an entry needs is an operand in Tls13KsV or a region of the borrow at a fixed offset.
209void protocore_tls13_ks_expand_label(uint8_t *work);
210void protocore_tls13_ks_derive_secret(uint8_t *work);
211void protocore_tls13_ks_early(uint8_t *work);
212void protocore_tls13_ks_handshake(uint8_t *work);
213void protocore_tls13_ks_master(uint8_t *work);
214void protocore_tls13_ks_finished_mac(uint8_t *work);
215void protocore_tls13_ks_transcript_init(uint8_t *work);
216void protocore_tls13_ks_transcript_update(uint8_t *work);
217void protocore_tls13_ks_transcript_peek(uint8_t *work);
218
219// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
220// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
221// `Tls13Ks.expand_label(work)` resolves to a named function and becomes a DIRECT call. An extern table
222// leaves the call indirect and the symbol live at every level, -O2 -flto included.
223static const Tls13KsNs Tls13Ks __attribute__((unused)) = {
224 .expand_label = protocore_tls13_ks_expand_label,
225 .derive_secret = protocore_tls13_ks_derive_secret,
226 .early = protocore_tls13_ks_early,
227 .handshake = protocore_tls13_ks_handshake,
228 .master = protocore_tls13_ks_master,
229 .finished_mac = protocore_tls13_ks_finished_mac,
230 .transcript_init = protocore_tls13_ks_transcript_init,
231 .transcript_update = protocore_tls13_ks_transcript_update,
232 .transcript_peek = protocore_tls13_ks_transcript_peek,
233};
234
235#endif // PROTOCORE_ENABLE_HTTP3 || PROTOCORE_ENABLE_DTLS || PROTOCORE_ENABLE_TLS
236
238
239#endif // PROTOCORE_TLS_KEY_SCHEDULE_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97