Skip to content

Commit 5a510c4

Browse files
saghullws-team
authored andcommitted
quic: select AEAD and header-protection cipher from negotiated suite
The built-in QUIC/H3 stack derived its packet-protection AEAD and header-protection (HP) cipher from the length of the exported TLS 1.3 traffic secret alone: a 48-byte secret was treated as AES-256-GCM and anything else as AES-128-GCM. ChaCha20-Poly1305 was never selected. Per RFC 9001 (5.3, 5.4) the AEAD and HP algorithms are fixed by the negotiated TLS 1.3 cipher suite, not by the secret length. Because TLS_AES_128_GCM_SHA256 and TLS_CHACHA20_POLY1305_SHA256 both use SHA-256, they produce identical 32-byte secrets, so a ChaCha20 handshake was mis-keyed with AES header protection and failed ("reserved bits" / decryption failure) whenever ChaCha20-Poly1305 was negotiated. Make the choice cipher-suite aware: - add enum lws_tls_quic_aead and wsi->tls.quic_aead, reported by the TLS backend from the negotiated suite; - every QUIC-capable TLS backend now reports it from the negotiated cipher as the traffic secrets are installed: * mbedTLS: maps the negotiated TLS 1.3 ciphersuite id in the traffic-secret export callback (the suite is only available there mid-handshake; ssl->session, which mbedtls_ssl_get_ciphersuite_id_from_ssl() reads, is not yet set); * OpenSSL/BoringSSL/AWS-LC/LibreSSL: from the SSL_CIPHER handed to set_{read,write}_secret(); * wolfSSL: from SSL_get_current_cipher() in set_encryption_secrets(); * GnuTLS: from gnutls_cipher_get(); * SChannel: from the SEC_TRAFFIC_SECRETS symmetric alg / key size; - lws_quic_set_keys() selects the internal cipher_type from the reported AEAD, falling back to the legacy length heuristic only when a backend does not report it (still correct for AES-128 vs AES-256 GCM, blind to ChaCha20 as before). The correct SHA-256/SHA-384 HKDF hash is still chosen from the secret length, which is unambiguous (it mirrors the suite's PRF hash size). Note: to build mbedtls support for quic/h3 after this patch, you need to patch mbedtls with "Epoch 2" patches from https://libwebsockets.org/git/mbedtls/log?h=development
1 parent 74dfbb8 commit 5a510c4

11 files changed

Lines changed: 170 additions & 5 deletions

File tree

CMakeLists-implied-options.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,9 @@ if (LWS_WITHOUT_DAEMONIZE OR WIN32)
451451
set(LWS_NO_DAEMONIZE 1)
452452
endif()
453453

454+
if (LWS_IPV4)
455+
set(LWS_WITH_IPV4 1)
456+
endif()
454457
if (LWS_IPV6)
455458
set(LWS_WITH_IPV6 1)
456459
endif()

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ option(LWS_WITH_HTTP2 "Compile with server support for HTTP/2" ON)
170170
option(LWS_WITH_LS_QPACK "Compile tests against ls-qpack for correctness testing" OFF)
171171
option(LWS_WITH_LWSWS "Libwebsockets Webserver" OFF)
172172
option(LWS_WITH_CGI "Include CGI (spawn process with network-connected stdin/out/err) APIs" OFF)
173+
option(LWS_IPV4 "Compile with support for ipv4" ON)
173174
option(LWS_IPV6 "Compile with support for ipv6" ON)
174175
option(LWS_UNIX_SOCK "Compile with support for UNIX domain socket if OS supports it" ON)
175176
option(LWS_WITH_PLUGINS "Support plugins for protocols and extensions (implies LWS_WITH_PLUGINS_API)" OFF)

READMEs/README.quic.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,9 @@ config string will append the mbedtls version with +LWSQUIC.
148148
```bash
149149
cmake .. \
150150
-DLWS_WITH_MBEDTLS=ON \
151-
-DMBEDTLS_INCLUDE_DIRS="/path/to/mbedtls/include" \
152-
-DMBEDTLS_LIBRARIES="/path/to/mbedtls/library/libmbedcrypto.a;/path/to/mbedtls/library/libmbedx509.a;/path/to/mbedtls/library/libmbedtls.a" \
151+
-DLWS_MBEDTLS_INCLUDE_DIRS="/path/to/mbedtls/include" \
152+
-DLWS_MBEDTLS_LIBRARIES="/path/to/mbedtls/library/libmbedtls.a;/path/to/mbedtls/library/libmbedx509.a;/path/to/mbedtls/library/libmbedcrypto.a" \
153+
-DLWS_MBEDTLS_TF_PSA_PATH="/path/to/mbedtls/tf-psa-crypto" \
153154
-DLWS_ROLE_QUIC=ON
154155
make -j
155156
```

cmake/lws_config.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
#cmakedefine LWS_WITH_HTTP_PROXY
203203
#cmakedefine LWS_WITH_HTTP_STREAM_COMPRESSION
204204
#cmakedefine LWS_WITH_HTTP_UNCOMMON_HEADERS
205+
#cmakedefine LWS_WITH_IPV4
205206
#cmakedefine LWS_WITH_IPV6
206207
#cmakedefine LWS_WITH_JOSE
207208
#cmakedefine LWS_WITH_CBOR

include/libwebsockets/lws-quic.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ enum lws_tls_quic_secret_type {
3838
LWS_TLS_QUIC_SECRET_SERVER_APPLICATION,
3939
};
4040

41+
/*
42+
* RFC 9001 4.1 / 5.3 / 5.4: the QUIC packet-protection AEAD and the header-
43+
* protection cipher are both fixed by the negotiated TLS 1.3 cipher suite, and
44+
* cannot be inferred from the traffic-secret length alone (TLS_AES_128_GCM_
45+
* SHA256 and TLS_CHACHA20_POLY1305_SHA256 both use SHA-256 and so produce a
46+
* 32-byte secret). The TLS backend reports the negotiated AEAD to the QUIC
47+
* role via wsi->tls.quic_aead so the correct algorithms are selected.
48+
*/
49+
enum lws_tls_quic_aead {
50+
LWS_TLS_QUIC_AEAD_UNKNOWN, /**< backend didn't report; caller
51+
* falls back to length heuristic */
52+
LWS_TLS_QUIC_AEAD_AES_128_GCM, /**< TLS_AES_128_GCM_SHA256 */
53+
LWS_TLS_QUIC_AEAD_AES_256_GCM, /**< TLS_AES_256_GCM_SHA384 */
54+
LWS_TLS_QUIC_AEAD_CHACHA20_POLY1305, /**< TLS_CHACHA20_POLY1305_SHA256 */
55+
};
56+
4157
enum lws_0rtt_status {
4258
LWS_0RTT_STATUS_NONE, /**< No 0-RTT attempted */
4359
LWS_0RTT_STATUS_ATTEMPTED, /**< Client sent 0-RTT, awaiting server decision */

lib/roles/quic/crypto-quic.c

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,39 @@ static const uint8_t quic_v2_initial_salt[20] = {
3939
0xf9, 0xbd, 0x2e, 0xd9
4040
};
4141

42+
/*
43+
* RFC 9001 5.3 / 5.4: the QUIC packet-protection AEAD and the header-protection
44+
* cipher are both fixed by the negotiated TLS 1.3 cipher suite:
45+
*
46+
* TLS_AES_128_GCM_SHA256 -> AEAD AES-128-GCM, HP AES-128-ECB
47+
* TLS_AES_256_GCM_SHA384 -> AEAD AES-256-GCM, HP AES-256-ECB
48+
* TLS_CHACHA20_POLY1305_SHA256 -> AEAD ChaCha20-Poly1305, HP ChaCha20
49+
*
50+
* They must NOT be inferred from the traffic-secret length: AES-128-GCM and
51+
* ChaCha20-Poly1305 both use SHA-256, so both yield a 32-byte secret. The TLS
52+
* backend reports the negotiated AEAD in wsi->tls.quic_aead; only when it did
53+
* not (LWS_TLS_QUIC_AEAD_UNKNOWN, e.g. a backend that doesn't plumb it through)
54+
* do we fall back to the length heuristic, which can still tell AES-256-GCM
55+
* (48-byte / SHA-384) from AES-128-GCM but is blind to ChaCha20.
56+
*
57+
* Maps to the internal cipher_type used throughout this file:
58+
* 0 = AES-128-GCM, 1 = ChaCha20-Poly1305, 2 = AES-256-GCM.
59+
*/
60+
static uint8_t
61+
lws_quic_cipher_type(struct lws *wsi, size_t secret_len)
62+
{
63+
switch (wsi->tls.quic_aead) {
64+
case LWS_TLS_QUIC_AEAD_AES_128_GCM:
65+
return 0;
66+
case LWS_TLS_QUIC_AEAD_CHACHA20_POLY1305:
67+
return 1;
68+
case LWS_TLS_QUIC_AEAD_AES_256_GCM:
69+
return 2;
70+
default:
71+
return (secret_len == 48) ? 2 : 0;
72+
}
73+
}
74+
4275
static int
4376
lws_quic_derive_key_iv_hp(uint8_t *secret, size_t secret_len, uint8_t cipher_type,
4477
uint8_t *iv, size_t iv_len,
@@ -265,7 +298,7 @@ lws_quic_set_keys(struct lws *wsi, enum lws_tls_quic_secret_type type, const uin
265298
}
266299
k->secret_len = secret_len > 48 ? 48 : secret_len;
267300
memcpy(k->secret_rx, secret, k->secret_len);
268-
if (k->cipher_type != 1) k->cipher_type = (k->secret_len == 48) ? 2 : 0;
301+
k->cipher_type = lws_quic_cipher_type(wsi, k->secret_len);
269302
if (lws_quic_derive_key_iv_hp(k->secret_rx, k->secret_len, k->cipher_type, k->iv_rx, sizeof(k->iv_rx),
270303
&k->el_aead_rx, k->key_aead_rx, &k->el_hp_rx, k->key_hp_rx))
271304
return -1;
@@ -276,7 +309,7 @@ lws_quic_set_keys(struct lws *wsi, enum lws_tls_quic_secret_type type, const uin
276309
}
277310
k->secret_len = secret_len > 48 ? 48 : secret_len;
278311
memcpy(k->secret_tx, secret, k->secret_len);
279-
if (k->cipher_type != 1) k->cipher_type = (k->secret_len == 48) ? 2 : 0;
312+
k->cipher_type = lws_quic_cipher_type(wsi, k->secret_len);
280313
if (lws_quic_derive_key_iv_hp(k->secret_tx, k->secret_len, k->cipher_type, k->iv_tx, sizeof(k->iv_tx),
281314
&k->el_aead_tx, k->key_aead_tx, &k->el_hp_tx, k->key_hp_tx))
282315
return -1;

lib/tls/gnutls/gnutls-quic.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ gnutls_quic_secret_func(gnutls_session_t session,
7272
if (!wsi || !wsi->tls.quic_secret_cb || secret_size > 48)
7373
return 0;
7474

75+
/*
76+
* RFC 9001: report the negotiated AEAD so the QUIC role selects the
77+
* matching packet-protection and header-protection ciphers instead of
78+
* guessing from the secret length (AES-128-GCM and ChaCha20-Poly1305
79+
* both yield a 32-byte SHA-256 secret).
80+
*/
81+
switch (gnutls_cipher_get(session)) {
82+
case GNUTLS_CIPHER_AES_128_GCM:
83+
wsi->tls.quic_aead = LWS_TLS_QUIC_AEAD_AES_128_GCM;
84+
break;
85+
case GNUTLS_CIPHER_AES_256_GCM:
86+
wsi->tls.quic_aead = LWS_TLS_QUIC_AEAD_AES_256_GCM;
87+
break;
88+
case GNUTLS_CIPHER_CHACHA20_POLY1305:
89+
wsi->tls.quic_aead = LWS_TLS_QUIC_AEAD_CHACHA20_POLY1305;
90+
break;
91+
default:
92+
/* leave any previously-reported suite in force */
93+
break;
94+
}
95+
7596
if (wsi->a.vhost)
7697
is_client = lwsi_role_client(wsi) ? 1 : 0;
7798
else

lib/tls/mbedtls/mbedtls-quic.c

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
#include "private-lib-core.h"
2626
#include "private-lib-tls-mbedtls.h"
27+
#include <mbedtls/ssl_ciphersuites.h>
2728

2829
void
2930
mbedtls_quic_bio_free(struct lws *wsi);
@@ -113,7 +114,8 @@ mbedtls_quic_set_traffic_secrets(mbedtls_ssl_context *ssl,
113114
mbedtls_ssl_secret_type_t type,
114115
const unsigned char *client_secret,
115116
const unsigned char *server_secret,
116-
size_t secret_len)
117+
size_t secret_len,
118+
int ciphersuite_id)
117119
{
118120
struct lws *wsi = (struct lws *)mbedtls_ssl_get_user_data_p(ssl);
119121
enum lws_tls_quic_secret_type ct, st;
@@ -124,6 +126,30 @@ mbedtls_quic_set_traffic_secrets(mbedtls_ssl_context *ssl,
124126
if (wsi->tls.quic_secret_cb == (lws_tls_quic_secret_cb)1)
125127
return 0;
126128

129+
/*
130+
* RFC 9001: the QUIC AEAD and header-protection algorithms are fixed
131+
* by the negotiated TLS 1.3 cipher suite, not by the secret length
132+
* (AES-128-GCM and ChaCha20-Poly1305 both use a 32-byte SHA-256
133+
* secret). Record which one is in force so the QUIC role selects the
134+
* matching AEAD / HP cipher. The negotiated suite is only available
135+
* here via the export callback: mbedtls_ssl_get_ciphersuite_id_from_
136+
* ssl() reads ssl->session, which is not yet populated mid-handshake.
137+
*/
138+
switch (ciphersuite_id) {
139+
case MBEDTLS_TLS1_3_AES_128_GCM_SHA256:
140+
wsi->tls.quic_aead = LWS_TLS_QUIC_AEAD_AES_128_GCM;
141+
break;
142+
case MBEDTLS_TLS1_3_AES_256_GCM_SHA384:
143+
wsi->tls.quic_aead = LWS_TLS_QUIC_AEAD_AES_256_GCM;
144+
break;
145+
case MBEDTLS_TLS1_3_CHACHA20_POLY1305_SHA256:
146+
wsi->tls.quic_aead = LWS_TLS_QUIC_AEAD_CHACHA20_POLY1305;
147+
break;
148+
default:
149+
/* leave any previously-reported suite in force */
150+
break;
151+
}
152+
127153
switch (type) {
128154
case MBEDTLS_SSL_SECRET_TYPE_EARLY:
129155
ct = st = LWS_TLS_QUIC_SECRET_CLIENT_EARLY;

lib/tls/openssl/openssl-quic.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@
3333

3434
#if defined(LWS_HAVE_BORINGSSL_QUIC_API)
3535

36+
/*
37+
* RFC 9001: the QUIC AEAD and header-protection ciphers are fixed by the
38+
* negotiated TLS 1.3 cipher suite, not by the traffic-secret length
39+
* (AES-128-GCM and ChaCha20-Poly1305 both use a 32-byte SHA-256 secret). Map
40+
* the negotiated suite's IANA code point to the enum the QUIC role consumes.
41+
* SSL_CIPHER_get_id() returns 0x0300<id> on OpenSSL/BoringSSL, so mask to the
42+
* low 16 bits; an unrecognised id leaves LWS_TLS_QUIC_AEAD_UNKNOWN and the QUIC
43+
* role falls back to its length heuristic.
44+
*/
45+
static enum lws_tls_quic_aead
46+
lws_openssl_quic_aead_from_id(uint32_t cipher_id)
47+
{
48+
switch (cipher_id & 0xffff) {
49+
case 0x1301: /* TLS_AES_128_GCM_SHA256 */
50+
return LWS_TLS_QUIC_AEAD_AES_128_GCM;
51+
case 0x1302: /* TLS_AES_256_GCM_SHA384 */
52+
return LWS_TLS_QUIC_AEAD_AES_256_GCM;
53+
case 0x1303: /* TLS_CHACHA20_POLY1305_SHA256 */
54+
return LWS_TLS_QUIC_AEAD_CHACHA20_POLY1305;
55+
default:
56+
return LWS_TLS_QUIC_AEAD_UNKNOWN;
57+
}
58+
}
59+
3660
#if defined(USE_WOLFSSL)
3761

3862
static int
@@ -43,10 +67,21 @@ set_encryption_secrets(WOLFSSL *ssl, enum wolfssl_encryption_level_t level,
4367
{
4468
struct lws *wsi = (struct lws *)SSL_get_app_data((SSL *)ssl);
4569
enum lws_tls_quic_secret_type rt, wt;
70+
const SSL_CIPHER *c;
4671

4772
if (!wsi || secret_len > 48)
4873
return 0;
4974

75+
/*
76+
* Report the negotiated AEAD (see lws_openssl_quic_aead_from_id). The
77+
* wolfSSL callback carries no cipher, so query the negotiated one via
78+
* the OpenSSL-compat API this file already uses for wolfSSL.
79+
*/
80+
c = SSL_get_current_cipher((SSL *)ssl);
81+
if (c)
82+
wsi->tls.quic_aead = lws_openssl_quic_aead_from_id(
83+
(uint32_t)SSL_CIPHER_get_id(c));
84+
5085
switch (level) {
5186
case wolfssl_encryption_early_data:
5287
rt = LWS_TLS_QUIC_SECRET_CLIENT_EARLY;
@@ -150,6 +185,11 @@ set_read_secret(SSL *ssl, enum ssl_encryption_level_t level,
150185
if (!wsi || secret_len > 48)
151186
return 0;
152187

188+
/* Report the negotiated AEAD (see lws_openssl_quic_aead_from_id). */
189+
if (cipher)
190+
wsi->tls.quic_aead = lws_openssl_quic_aead_from_id(
191+
(uint32_t)SSL_CIPHER_get_id(cipher));
192+
153193
switch (level) {
154194
case ssl_encryption_early_data:
155195
t = LWS_TLS_QUIC_SECRET_CLIENT_EARLY;
@@ -181,6 +221,11 @@ set_write_secret(SSL *ssl, enum ssl_encryption_level_t level,
181221
if (!wsi || secret_len > 48)
182222
return 0;
183223

224+
/* Report the negotiated AEAD (see lws_openssl_quic_aead_from_id). */
225+
if (cipher)
226+
wsi->tls.quic_aead = lws_openssl_quic_aead_from_id(
227+
(uint32_t)SSL_CIPHER_get_id(cipher));
228+
184229
switch (level) {
185230
case ssl_encryption_early_data:
186231
t = LWS_TLS_QUIC_SECRET_CLIENT_EARLY;

lib/tls/private-network.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ struct lws_lws_tls {
131131
size_t quic_tp_send_len;
132132
lws_tls_quic_secret_cb quic_secret_cb;
133133
int quic_alert;
134+
uint8_t quic_aead; /* enum lws_tls_quic_aead, set by the
135+
* TLS backend from the negotiated
136+
* TLS 1.3 cipher suite */
134137

135138
unsigned int use_ssl;
136139
unsigned int redirect_to_https:1;

0 commit comments

Comments
 (0)