DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
quic_crypto.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_crypto.cpp
6 * @brief QUIC packet protection and Initial secrets (see quic_crypto.h).
7 */
8
10
11#if DETWS_ENABLE_HTTP3
12
16#include <string.h>
17
18namespace
19{
20// RFC 9001 sec 5.2: the version-1 Initial salt.
21const uint8_t INITIAL_SALT[20] = {0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17,
22 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a};
23
24// RFC 9001 sec 5.8: the version-1 Retry integrity key and nonce.
25const uint8_t RETRY_KEY[16] = {0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a,
26 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e};
27const uint8_t RETRY_NONCE[12] = {0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb};
28
29// Build the AEAD nonce: the packet number, left-padded to the 12-byte IV width, XOR the IV.
30void build_nonce(const uint8_t iv[12], uint64_t full_pn, uint8_t nonce[12])
31{
32 memcpy(nonce, iv, 12);
33 for (int i = 0; i < 8; i++)
34 nonce[11 - i] ^= (uint8_t)(full_pn >> (8 * i));
35}
36} // namespace
37
38void quic_keys_from_secret(const uint8_t secret[QUIC_HKDF_HASH_LEN], QuicPacketKeys *out)
39{
40 // RFC 9001 sec 5.1: every encryption level's packet keys are these three Expand-Labels of the
41 // level's traffic secret (the Initial secrets below, or the TLS handshake / application secrets).
42 quic_hkdf_expand_label(secret, "quic key", out->key, sizeof(out->key));
43 quic_hkdf_expand_label(secret, "quic iv", out->iv, sizeof(out->iv));
44 quic_hkdf_expand_label(secret, "quic hp", out->hp, sizeof(out->hp));
45}
46
47void quic_derive_initial_secrets(const uint8_t *dcid, size_t dcid_len, QuicInitialSecrets *out)
48{
49 uint8_t initial_secret[QUIC_HKDF_HASH_LEN];
50 quic_hkdf_extract(INITIAL_SALT, sizeof(INITIAL_SALT), dcid, dcid_len, initial_secret);
51
52 uint8_t client_secret[QUIC_HKDF_HASH_LEN];
53 uint8_t server_secret[QUIC_HKDF_HASH_LEN];
54 quic_hkdf_expand_label(initial_secret, "client in", client_secret, sizeof(client_secret));
55 quic_hkdf_expand_label(initial_secret, "server in", server_secret, sizeof(server_secret));
56
57 quic_keys_from_secret(client_secret, &out->client);
58 quic_keys_from_secret(server_secret, &out->server);
59}
60
61size_t quic_packet_protect(uint8_t *pkt, size_t cap, size_t pn_offset, uint8_t pn_len, uint64_t full_pn,
62 size_t payload_len, const QuicPacketKeys *keys, bool is_long)
63{
64 if (pn_len < 1 || pn_len > 4)
65 return 0;
66 size_t hdr_len = pn_offset + pn_len;
67 size_t total = hdr_len + payload_len + QUIC_AEAD_TAG_LEN;
68 if (total > cap)
69 return 0;
70
71 // AEAD-seal the payload in place; associated data is the unprotected header.
72 uint8_t nonce[12];
73 build_nonce(keys->iv, full_pn, nonce);
74 quic_aes128_gcm_seal(keys->key, nonce, pkt, hdr_len, pkt + hdr_len, payload_len, pkt + hdr_len);
75
76 // Header protection (RFC 9001 sec 5.4): sample 16 bytes at pn_offset + 4 (always inside the
77 // ciphertext because pn_len <= 4), AES-ECB it under hp, mask the low first-byte bits and the PN.
78 QuicAes128 hp;
79 quic_aes128_init(&hp, keys->hp);
80 uint8_t mask[16];
81 quic_aes128_encrypt_block(&hp, pkt + pn_offset + 4, mask);
82 quic_aes128_wipe(&hp);
83
84 pkt[0] ^= mask[0] & (is_long ? 0x0f : 0x1f);
85 for (uint8_t i = 0; i < pn_len; i++)
86 pkt[pn_offset + i] ^= mask[1 + i];
87
88 return total;
89}
90
91size_t quic_packet_unprotect(uint8_t *pkt, size_t pn_offset, size_t length, uint64_t largest_pn,
92 const QuicPacketKeys *keys, bool is_long, uint8_t *out, uint64_t *out_pn)
93{
94 // Header protection needs a full 16-byte sample starting at pn_offset + 4, and the AEAD region
95 // must carry at least the 16-byte tag once the (<=4-byte) packet number is removed.
96 if (length < 4 + QUIC_AEAD_TAG_LEN)
97 return (size_t)-1;
98
99 QuicAes128 hp;
100 quic_aes128_init(&hp, keys->hp);
101 uint8_t mask[16];
102 quic_aes128_encrypt_block(&hp, pkt + pn_offset + 4, mask);
103 quic_aes128_wipe(&hp);
104
105 pkt[0] ^= mask[0] & (is_long ? 0x0f : 0x1f);
106 uint8_t pn_len = (uint8_t)((pkt[0] & 0x03) + 1);
107
108 uint64_t truncated_pn = 0;
109 for (uint8_t i = 0; i < pn_len; i++)
110 {
111 pkt[pn_offset + i] ^= mask[1 + i];
112 truncated_pn = (truncated_pn << 8) | pkt[pn_offset + i];
113 }
114 uint64_t full_pn = quic_pn_decode(largest_pn, truncated_pn, (uint8_t)(pn_len * 8));
115 if (out_pn)
116 *out_pn = full_pn;
117
118 size_t hdr_len = pn_offset + pn_len;
119 size_t ct_len = length - pn_len; // ciphertext + tag
120 uint8_t nonce[12];
121 build_nonce(keys->iv, full_pn, nonce);
122 if (!quic_aes128_gcm_open(keys->key, nonce, pkt, hdr_len, pkt + hdr_len, ct_len, out))
123 return (size_t)-1;
124
125 return ct_len - QUIC_AEAD_TAG_LEN;
126}
127
128void quic_retry_integrity_tag(const uint8_t *odcid, size_t odcid_len, const uint8_t *retry, size_t retry_len,
129 uint8_t tag[16])
130{
131 // AAD = Retry Pseudo-Packet: ODCID Length (1 byte) || ODCID || Retry packet (sans tag).
132 // Assemble it into a scratch buffer; a Retry is small (short token), so a fixed cap suffices.
133 uint8_t aad[1 + QUIC_MAX_CID_LEN + 256];
134 size_t p = 0;
135 aad[p++] = (uint8_t)odcid_len;
136 if (odcid_len > QUIC_MAX_CID_LEN || 1 + odcid_len + retry_len > sizeof(aad))
137 {
138 memset(tag, 0, 16);
139 return;
140 }
141 memcpy(aad + p, odcid, odcid_len);
142 p += odcid_len;
143 memcpy(aad + p, retry, retry_len);
144 p += retry_len;
145
146 // Empty plaintext: seal writes only the 16-byte tag.
147 quic_aes128_gcm_seal(RETRY_KEY, RETRY_NONCE, aad, p, nullptr, 0, tag);
148}
149
150#endif // DETWS_ENABLE_HTTP3
AES-128 block cipher and AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
QUIC packet protection: Initial secrets, AEAD payload protection, header protection,...
HKDF-SHA256 (RFC 5869) and TLS 1.3 HKDF-Expand-Label (RFC 8446 sec 7.1).
QUIC packet headers and packet-number coding (RFC 9000 sec 17).