ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
quic_crypto.h
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_crypto.h
6 * @brief QUIC packet protection: Initial secrets, AEAD payload protection, header protection,
7 * and the Retry integrity tag (RFC 9001).
8 *
9 * This ties the HKDF key schedule (pc_hkdf) and AEAD_AES_128_GCM (aes128gcm) into the two QUIC
10 * packet-protection operations of RFC 9001 sec 5:
11 *
12 * - pc_quic_derive_initial_secrets() runs the sec 5.2 Initial key derivation: a fixed salt and the
13 * client's Destination Connection ID produce the client and server {key, iv, hp} triples that
14 * protect Initial packets (the only keys available before the TLS handshake yields more).
15 * - pc_quic_packet_protect() / pc_quic_packet_unprotect() perform sec 5.3 AEAD payload protection and
16 * sec 5.4 header protection together, on a whole packet in a buffer. They take a {key, iv, hp}
17 * triple and a header form, so the same code protects Initial, Handshake, and 1-RTT packets -
18 * only the secrets differ. AES-128-GCM header protection samples a 16-byte AES-ECB block.
19 * - pc_quic_retry_integrity_tag() computes the sec 5.8 Retry Integrity Tag (a fixed-key AEAD over the
20 * Retry Pseudo-Packet).
21 *
22 * Pure, zero heap, host-tested against RFC 9001 Appendix A (client Initial A.2, server Initial A.3,
23 * Retry A.4).
24 *
25 * @author Douglas Quigg (dstroy0)
26 * @date 2026
27 */
28
29#ifndef PROTOCORE_QUIC_CRYPTO_H
30#define PROTOCORE_QUIC_CRYPTO_H
31
32#include "protocore_config.h"
33
34#if PC_ENABLE_HTTP3
35
36#include "crypto/aead/aes128gcm.h" // pc_aes128gcm_key, PC_WORK_AES128GCM
37#include "crypto/kdf/hkdf.h" // PC_HKDF_HASH_LEN
38#include <stddef.h>
39#include <stdint.h>
40
41/** @brief The client/server packet-protection secrets for one QUIC encryption level. */
42struct QuicPacketKeys
43{
44 alignas(8) uint8_t gcm[PC_WORK_AES128GCM]; ///< keyed AEAD context, built once per key.
45 ///< Replaces the raw key: the schedule is what the
46 ///< AEAD needs, so no raw key stays resident.
47 uint8_t iv[12]; ///< AEAD nonce base (XOR'd with the padded packet number).
48 alignas(8) uint8_t hp[PC_WORK_AES128]; ///< Keyed header-protection context (AES-128-ECB mask).
49 ///< Built once: ~556 cycles per record, plus the pool
50 ///< borrow and wipe it also removes.
51};
52
53/** @brief Both directions' Initial secrets derived from the client's Destination Connection ID. */
54struct QuicInitialSecrets
55{
56 QuicPacketKeys client; ///< Protects client-sent Initial packets (server opens with this).
57 QuicPacketKeys server; ///< Protects server-sent Initial packets (server seals with this).
58};
59
60/**
61 * @brief Derive the Initial packet-protection secrets (RFC 9001 sec 5.2).
62 *
63 * initial_secret = HKDF-Extract(initial_salt, dcid); each direction's traffic secret is an
64 * HKDF-Expand-Label ("client in" / "server in") of that, and key/iv/hp are expanded from the
65 * traffic secret. @p dcid is the Destination Connection ID from the client's first Initial packet.
66 */
67void pc_quic_derive_initial_secrets(const uint8_t *dcid, size_t dcid_len, QuicInitialSecrets *out);
68
69/**
70 * @brief Expand one traffic secret into a {key, iv, hp} triple (RFC 9001 sec 5.1 labels).
71 *
72 * key = HKDF-Expand-Label(secret, "quic key", 16), iv = "quic iv" (12), hp = "quic hp" (16). The
73 * Initial derivation uses this internally; the Handshake and 1-RTT levels call it directly on the
74 * TLS-derived handshake / application traffic secrets so every level shares one code path.
75 */
76void pc_quic_keys_from_secret(const uint8_t secret[PC_HKDF_HASH_LEN], QuicPacketKeys *out);
77
78/**
79 * @brief Protect one QUIC packet in place: AEAD-seal the payload, then apply header protection.
80 *
81 * On entry @p pkt holds the unprotected header (flags with reserved bits 0, the packet number in
82 * the clear) in bytes [0, pn_offset + pn_len), immediately followed by @p payload_len plaintext
83 * frame bytes. On return @p pkt holds the header (now protected) followed by the ciphertext and
84 * 16-byte tag. The header in [0, pn_offset + pn_len) is used as the AEAD associated data before
85 * protection is applied.
86 *
87 * @param pkt Buffer holding header || plaintext payload; rewritten to header || ciphertext.
88 * @param cap Capacity of @p pkt; must be >= pn_offset + pn_len + payload_len + 16.
89 * @param pn_offset Offset of the packet number within the header.
90 * @param pn_len Packet-number length in bytes (1..4).
91 * @param full_pn Full (untruncated) packet number, for the AEAD nonce.
92 * @param payload_len Plaintext payload length in bytes.
93 * @param keys The {key, iv, hp} triple for this encryption level.
94 * @param is_long True for a long header (Initial/Handshake), false for a 1-RTT short header.
95 * @return total protected packet length, or 0 on a capacity/parameter error.
96 */
97size_t pc_quic_packet_protect(uint8_t *pkt, size_t cap, size_t pn_offset, uint8_t pn_len, uint64_t full_pn,
98 size_t payload_len, QuicPacketKeys &keys, bool is_long);
99
100/**
101 * @brief Remove header protection and AEAD-open one QUIC packet in place (RFC 9001 sec 5.3/5.4).
102 *
103 * @p pkt holds the received packet on the wire. @p pn_offset is the offset of the (protected)
104 * packet number, found by parsing the header up to the Length field. @p length is the QUIC Length
105 * field value (packet-number bytes + protected payload + 16-byte tag). Header protection is removed
106 * first (recovering the true packet-number length and value), the packet number is reconstructed
107 * against @p largest_pn (RFC 9000 sec 17.1), and the payload is verified and decrypted.
108 *
109 * @param pkt Buffer holding the protected packet (mutated: header unprotected in place).
110 * @param pn_offset Offset of the protected packet number.
111 * @param length QUIC Length field (packet-number + payload + tag bytes).
112 * @param largest_pn Largest packet number already received at this level (0 if none yet).
113 * @param keys The {key, iv, hp} triple for this encryption level.
114 * @param is_long True for a long header, false for a 1-RTT short header.
115 * @param out Output plaintext frames (>= length - pn_len - 16 bytes); may alias @p pkt payload.
116 * @param out_pn Receives the reconstructed full packet number (may be NULL).
117 * @return plaintext length, or (size_t)-1 on parameter error or AEAD authentication failure.
118 */
119size_t pc_quic_packet_unprotect(uint8_t *pkt, size_t pn_offset, size_t length, uint64_t largest_pn,
120 QuicPacketKeys &keys, bool is_long, uint8_t *out, uint64_t *out_pn);
121
122/**
123 * @brief Compute the Retry Integrity Tag (RFC 9001 sec 5.8).
124 *
125 * AEAD_AES_128_GCM with the version-1 fixed key and nonce over the Retry Pseudo-Packet, whose AAD
126 * is the original Destination Connection ID (length-prefixed) followed by the Retry packet up to
127 * but excluding the tag. Plaintext is empty, so the whole 16-byte output is the tag.
128 *
129 * @param odcid Original Destination Connection ID (from the client's first Initial).
130 * @param odcid_len ODCID length in bytes.
131 * @param retry Retry packet bytes from the first byte up to (not including) the tag.
132 * @param retry_len Length of @p retry.
133 * @param tag Output 16-byte integrity tag.
134 */
135void pc_quic_retry_integrity_tag(const uint8_t *odcid, size_t odcid_len, const uint8_t *retry, size_t retry_len,
136 uint8_t tag[16]);
137
138#endif // PC_ENABLE_HTTP3
139#endif // PROTOCORE_QUIC_CRYPTO_H
AES-128 block cipher + AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
HKDF-SHA256 (RFC 5869) and TLS 1.3 HKDF-Expand-Label (RFC 8446 sec 7.1).
User-facing configuration for ProtoCore.
#define PC_WORK_AES128
#define PC_WORK_AES128GCM