ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
record.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 record.h
6 * @brief TLS 1.3 record layer over a reliable stream (RFC 8446 sec 5).
7 *
8 * The TCP counterpart to protocore_dtls_record: it protects and unprotects individual records on a byte
9 * stream. TCP delivers them in order, exactly once, so this half of the protocol is the smaller
10 * one - there is no epoch, no sequence number on the wire, no anti-replay window and no fragment
11 * reassembly. What remains is the header, the AEAD, and a record counter each side keeps itself.
12 *
13 * Two record shapes (RFC 8446 sec 5.1, 5.2):
14 * - **TLSPlaintext** - the 5-byte header (type, legacy_record_version, length) followed by the
15 * fragment, sent unencrypted for the first handshake flight and for alerts before keys exist.
16 * - **TLSCiphertext** - the same header with opaque_type = application_data(23) over an AEAD-sealed
17 * body. The sealed plaintext is `content || real_type`, so the true content type travels inside
18 * the encryption; the header's type is a constant that reveals nothing.
19 *
20 * The record sequence number is never transmitted. It starts at zero when a key is installed and
21 * counts records under that key (sec 5.3), so both ends derive the same nonce from their own count.
22 *
23 * -- Reuse --
24 * The AEAD and the key/iv derivation follow the negotiated suite (@ref TlsCipher), and the
25 * derivation runs through ::Tls13Ks under the "tls13 " label prefix (@ref TLS13_KDF). Two suites:
26 * TLS_AES_128_GCM_SHA256 over AEAD_AES_128_GCM with a SHA-256 schedule, and TLS_AES_256_GCM_SHA384
27 * over AEAD_AES_256_GCM with a SHA-384 one. Which AEAD a record uses is read off the key it was
28 * derived into, so nothing below the key derivation branches on the suite.
29 *
30 * Pure, zero heap, host-tested. This is the portable record layer; a build whose vendor ships a TLS
31 * compiles with PROTOCORE_ENABLE_TLS.
32 *
33 * @author Douglas Quigg (dstroy0)
34 * @date 2026
35 */
36
37#ifndef PROTOCORE_TLS_RECORD_H
38#define PROTOCORE_TLS_RECORD_H
39
40#include "crypto/aead/aes128gcm/aes128gcm.h" // Aes128Gcm, the 0x1301 record AEAD
41#include "crypto/aead/aesgcm/aesgcm.h" // AesGcm, the 0x1302 record AEAD
42#include "protocore_config.h" // the entry point: the enable gate below, and the widths
43
44#if PROTOCORE_ENABLE_TLS
45
47
48#include "network_drivers/presentation/http/http3/tls13_msg/tls13_msg.h" // the suite code points TlsCipher takes its values from
49
50/** @name Record content types (RFC 8446 sec 5).
51 * Shared by the TLSPlaintext `type` field and the TLSInnerPlaintext trailing content type. */
52///@{
53#define PROTOCORE_TLS_CT_CHANGE_CIPHER_SPEC 20
54#define PROTOCORE_TLS_CT_ALERT 21
55#define PROTOCORE_TLS_CT_HANDSHAKE 22
56#define PROTOCORE_TLS_CT_APPLICATION_DATA 23
57///@}
58
59/** @brief TLSPlaintext legacy_record_version on the wire: TLS 1.2 (RFC 8446 sec 5.1). */
60#define PROTOCORE_TLS_LEGACY_VERSION 0x0303
61
62/** @brief TLSPlaintext header length: type(1) + legacy_record_version(2) + length(2). */
63#define PROTOCORE_TLS_PLAINTEXT_HDR_LEN 5
64
65/** @brief AEAD tag length (all supported suites: 16 bytes). */
66#define PROTOCORE_TLS_TAG_LEN 16
67
68/** @brief Largest plaintext fragment one record carries: 2^14 (RFC 8446 sec 5.1). */
69#define PROTOCORE_TLS_MAX_PLAINTEXT 16384
70
71/**
72 * @brief Largest protected record body: the fragment, its inner content type, and the tag.
73 *
74 * RFC 8446 sec 5.2 allows up to 256 further bytes of padding. Nothing here pads, so a record this
75 * side builds never reaches that; a peer's may, and @ref TlsRecordNs::unprotect strips it.
76 */
77#define PROTOCORE_TLS_MAX_CIPHERTEXT (PROTOCORE_TLS_MAX_PLAINTEXT + 1 + PROTOCORE_TLS_TAG_LEN)
78
79/** @brief Record-layer AEAD suites, as the two RFC 8446 sec B.4 mandatory-to-implement code points
80 * name them. The suite fixes both the AEAD and the hash the key schedule under it runs. Zero is the
81 * sec 9.1 mandatory-to-implement suite, so a zero-initialised config names it. */
82typedef enum PROTO_ENUM_PACKED
83{
84 TLS_CIPHER_AES_128_GCM_SHA256 = 0, ///< 0x1301: AEAD_AES_128_GCM, 16-byte key, SHA-256 schedule
85 TLS_CIPHER_AES_256_GCM_SHA384 = 1 ///< 0x1302: AEAD_AES_256_GCM, 32-byte key, SHA-384 schedule
86} TlsCipher;
87
88/** @brief Whether @p c runs its key schedule and Transcript-Hash on SHA-384 (RFC 8446 sec 7.1).
89 * The one statement of which hash a suite binds, so a driver reads it here instead of restating it. */
90proto_bool protocore_tls_cipher_is384(TlsCipher c);
91
92/** @brief @p c as its IANA CipherSuite code point, the form a ClientHello or ServerHello carries. */
93uint16_t protocore_tls_cipher_code(TlsCipher c);
94
95/** @brief AEAD write-IV length. Both record suites are 12 (RFC 5116); a static_assert in record.c
96 * holds the two primitives to it. */
97#define PROTOCORE_TLS_RECORD_IV_LEN 12
98
99/**
100 * @brief One direction's record-protection state (RFC 8446 sec 5.3).
101 *
102 * Derived from a TLS 1.3 traffic secret; holds the AEAD key plus write IV and the record counter the
103 * nonce is built from. One instance per (direction, key generation).
104 */
105typedef struct
106{
107 TlsCipher cipher; ///< the AEAD this key is under
108 uint8_t gcm[PROTOCORE_TLS_RECORD_AEAD_BORROW]; ///< the AEAD's borrow, keyed once per key.
109 ///< Replaces the raw key: the schedule is what
110 ///< the AEAD needs, so no raw key stays resident.
111 ///< Sized at the wider of the two suites.
112 uint8_t iv[PROTOCORE_TLS_RECORD_IV_LEN]; ///< AEAD write IV (nonce = iv XOR seq)
113 uint8_t nonce[PROTOCORE_TLS_RECORD_IV_LEN]; ///< this record's nonce, rebuilt from iv and seq
114 uint64_t seq; ///< records sealed/opened under this key, never sent
115 proto_bool ready; ///< the AEAD context holds a key
116} TlsRecordKeys;
117
118/** @brief Parsed view of a TLSPlaintext record (fields point into the caller's buffer). */
119typedef struct
120{
121 uint8_t content_type;
122 const uint8_t *fragment; ///< into the input buffer
123 size_t frag_len;
124} TlsPlaintext;
125
126/** @brief Result of a successful @ref TlsRecordNs::unprotect. */
127typedef struct
128{
129 uint8_t content_type; ///< recovered inner content type (last non-zero byte of the inner plaintext)
130 size_t pt_len; ///< plaintext bytes written to @p out
131} TlsCiphertext;
132
133/** @brief RFC 8446 sec 5.3: the direction a call acts on, and what a derive installs into it. */
134typedef struct
135{
136 TlsRecordKeys *keys; ///< the direction a call acts on
137 TlsCipher cipher; ///< the negotiated AEAD a derive installs
138 const uint8_t *secret; ///< the traffic secret it derives from, 32 or 48 octets by @c cipher
139} TlsKeyArgs;
140
141/** @brief RFC 8446 sec 5.1 TLSPlaintext: the fragment a build carries, and the view a parse fills. */
142typedef struct
143{
144 const uint8_t *fragment; ///< the bytes an unencrypted record carries
145 size_t frag_len; ///< how many
146 TlsPlaintext *view; ///< where a parse lands its view of the record
147} TlsPlaintextArgs;
148
149/** @brief RFC 8446 sec 5.2 TLSCiphertext: the fragment a seal takes, and the record an open takes. */
150typedef struct
151{
152 const uint8_t *pt; ///< the fragment a protect seals
153 size_t pt_len; ///< how many
154 const uint8_t *rec; ///< the received record an unprotect opens
155 size_t rec_len; ///< how many bytes of it there are
156 TlsCiphertext *info; ///< what an unprotect recovered
157} TlsCiphertextArgs;
158
159/** @brief Where a build, a protect or an unprotect writes. */
160typedef struct
161{
162 uint8_t *out; ///< where the record or the recovered plaintext lands
163 size_t out_cap; ///< how much room it has
164} TlsRecordOut;
165
166/**
167 * @brief The record layer (RFC 8446 sec 5): the two record shapes and their keys.
168 *
169 * A caller sets the members a call takes, invokes it through ::TlsRecord, and reads the outcome off
170 * the same handle. The keys are the caller's, named in @ref TlsRecordNs::key.
171 *
172 * @var TlsRecordNs::content_type the record's true content type, on the way out or the way in
173 * @var TlsRecordNs::key the direction a call acts on, and what a derive installs
174 * @var TlsRecordNs::plain sec 5.1 the fragment a build carries and the view a parse fills
175 * @var TlsRecordNs::sealed sec 5.2 the fragment a seal takes and the record an open takes
176 * @var TlsRecordNs::out_args where a build, a protect or an unprotect writes
177 * @var TlsRecordNs::ok a call's true/false outcome
178 * @var TlsRecordNs::n bytes written, or the record length consumed; 0 on refusal
179 * @var TlsRecordNs::keys_derive derive one direction's record keys from a 32-byte TLS 1.3 traffic
180 * secret. RFC 8446 sec 7.3: key and iv are each HKDF-Expand-Label of it under the "tls13 "
181 * prefix. Sets seq to zero, which is what starting a key generation means (sec 5.3).
182 * @var TlsRecordNs::plaintext_build build a TLSPlaintext record; bytes written, or 0 on overflow
183 * @var TlsRecordNs::plaintext_parse parse one back, validating the length field; the record length
184 * consumed, or 0 if malformed or truncated
185 * @var TlsRecordNs::protect seal one record (sec 5.2): the inner plaintext is @c sealed.pt with the
186 * real content type appended, the header carries application_data(23) and the sealed length, and
187 * the nonce is iv XOR seq with the header as the associated data. Advances seq. Bytes written,
188 * or 0 on overflow or an over-long fragment.
189 * @var TlsRecordNs::unprotect open a received record: verify and decrypt into @c out_args.out, then
190 * scan back past the zero padding for the real content type. Advances seq only on success. A
191 * record whose inner plaintext is all zeros has no content type and is refused (sec 5.4).
192 * @var TlsRecordNs::keys_wipe wipe the AEAD schedule and the IV; the storage stays the caller's
193 *
194 * No storage member: every call works in the caller's buffers and in the keys it was handed.
195 */
196typedef struct
197{
198 uint8_t content_type;
199 TlsKeyArgs key;
200 TlsPlaintextArgs plain;
201 TlsCiphertextArgs sealed;
202 TlsRecordOut out_args;
203 proto_bool ok;
204 size_t n;
205} TlsRecordVars;
206
207/** @brief The operands and the outcome. */
208extern TlsRecordVars TlsRecordV;
209
210/** @brief The entries. */
211typedef struct
212{
213 void (*const keys_derive)(uint8_t *work);
214 void (*const plaintext_build)(uint8_t *work);
215 void (*const plaintext_parse)(uint8_t *work);
216 void (*const protect)(uint8_t *work);
217 void (*const unprotect)(uint8_t *work);
218 void (*const keys_wipe)(uint8_t *work);
219} TlsRecordNs;
220
221// What the table binds, defined once in the .c and taking one parameter each: everything
222// else an entry needs is an operand in TlsRecordV or a region of the borrow at a fixed offset.
223void protocore_tls_record_keys_derive(uint8_t *work);
224void protocore_tls_record_plaintext_build(uint8_t *work);
225void protocore_tls_record_plaintext_parse(uint8_t *work);
226void protocore_tls_record_protect(uint8_t *work);
227void protocore_tls_record_unprotect(uint8_t *work);
228void protocore_tls_record_keys_wipe(uint8_t *work);
229
230// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
231// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
232// `TlsRecord.keys_derive(work)` resolves to a named function and becomes a DIRECT call. An extern table
233// leaves the call indirect and the symbol live at every level, -O2 -flto included.
234static const TlsRecordNs TlsRecord __attribute__((unused)) = {
235 .keys_derive = protocore_tls_record_keys_derive,
236 .plaintext_build = protocore_tls_record_plaintext_build,
237 .plaintext_parse = protocore_tls_record_plaintext_parse,
238 .protect = protocore_tls_record_protect,
239 .unprotect = protocore_tls_record_unprotect,
240 .keys_wipe = protocore_tls_record_keys_wipe,
241};
242
243#endif // PROTOCORE_ENABLE_TLS
244
246
247#endif // PROTOCORE_TLS_RECORD_H
AEAD_AES_128_GCM (RFC 5116) - keyed, detached tag - and the AES-128 block it is built on.
AES-256-GCM AEAD (RFC 5116) - keyed, detached tag.
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_TLS_RECORD_AEAD_BORROW
TLS 1.3 handshake messages for the QUIC handshake (RFC 8446 sec 4).
#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