From: Frederic Lecaille Date: Thu, 3 Sep 2026 13:10:47 +0000 (+0200) Subject: MINOR: quic: implement the AEAD integrity limit X-Git-Tag: v3.5-dev7~84 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=f83f6c0d37dfa08e6db6802894ed22dad61e0ef6;p=haproxy.git MINOR: quic: implement the AEAD integrity limit RFC 9001 6.6 requires an endpoint to count the received packets which fail authentication, and to close the connection with AEAD_LIMIT_REACHED once their number exceeds the limit of the AEAD in use. This was not implemented. Add quic_aead_integrity_limit() which returns that limit for each algorithm tls_aead() may return, and count the failures in qc_pkt_decrypt(). This is the only place where a packet is decrypted, whatever its encryption level, so the count covers all the keys of the connection as the RFC asks. An unknown algorithm gets a null limit, which closes the connection on the first failure: QUIC must not use one. With AES-GCM the limit is 2^52 failed packets, which at one million forged packets per second on a single connection would take more than a century. With ChaCha20-Poly1305 it is 2^36, about 19 hours at the same rate. This is not a bug fix, nothing is affected in practice, so there is no hurry. This may be backported to all the stable versions after some exposure in 3.5. --- diff --git a/include/haproxy/quic_conn-t.h b/include/haproxy/quic_conn-t.h index c594fd835..ab28093ee 100644 --- a/include/haproxy/quic_conn-t.h +++ b/include/haproxy/quic_conn-t.h @@ -386,6 +386,9 @@ struct quic_conn { /* first unhandled streams ID, set by MUX after release */ uint64_t stream_max_uni; uint64_t stream_max_bidi; + + /* Number of received packets which failed authentication. */ + uint64_t auth_fails; } rx; struct { struct quic_tls_kp prv_rx; diff --git a/include/haproxy/quic_tls.h b/include/haproxy/quic_tls.h index dac45a3f2..e33437cca 100644 --- a/include/haproxy/quic_tls.h +++ b/include/haproxy/quic_tls.h @@ -182,6 +182,44 @@ static inline const QUIC_AEAD *tls_aead(const SSL_CIPHER *cipher) } } +/* Return the number of packets which may fail authentication with as + * AEAD algorithm, as stated by RFC 9001 6.6. + */ +static inline uint64_t quic_aead_integrity_limit(const QUIC_AEAD *aead) +{ + /* Note that QUIC_AEAD_API is defined only if OPENSSL_IS_AWSLC is defined. + * + * tls_aead() may only return, depending on the library: + * AWS-LC : aes_128_gcm, aes_256_gcm, chacha20_poly1305 + * wolfSSL : aes_128_gcm, aes_256_gcm, chacha20_poly1305 + * LibreSSL < 4.0: aes_128_gcm, aes_256_gcm, aes_128_ccm + * others : the four of them + */ +#ifdef QUIC_AEAD_API + if (aead == EVP_aead_chacha20_poly1305()) + return 1ULL << 36; + if (aead == EVP_aead_aes_128_gcm() || + aead == EVP_aead_aes_256_gcm()) + return 1ULL << 52; +#else /* !QUIC_AEAD_API */ + switch (EVP_CIPHER_nid(aead)) { +#if !defined(USE_OPENSSL_WOLFSSL) + case NID_aes_128_ccm: + /* 2^21.5 */ + return 2965820; +#endif +#if (!defined(LIBRESSL_VERSION_NUMBER) || LIBRESSL_VERSION_NUMBER >= 0x4000000fL) + case NID_chacha20_poly1305: + return 1ULL << 36; +#endif + case NID_aes_128_gcm: + case NID_aes_256_gcm: + return 1ULL << 52; + } +#endif + return 0; +} + static inline const EVP_MD *tls_md(const SSL_CIPHER *cipher) { switch (SSL_CIPHER_get_id(cipher)) { diff --git a/src/quic_conn.c b/src/quic_conn.c index 1f435a44d..a664b9fb1 100644 --- a/src/quic_conn.c +++ b/src/quic_conn.c @@ -1307,6 +1307,7 @@ struct quic_conn *qc_new_conn(void *target, memset(&qc->rx.params, 0, sizeof(qc->rx.params)); qc->rx.buf = b_make(qc->rx.buf.area, QUIC_CONN_RX_BUFSZ, 0, 0); qc->rx.stream_max_uni = qc->rx.stream_max_bidi = 0; + qc->rx.auth_fails = 0; qc->max_udp_payload = l ? l->bind_conf->quic_params.max_udp_payload_size : diff --git a/src/quic_rx.c b/src/quic_rx.c index bff7f1d96..1bfe78fbb 100644 --- a/src/quic_rx.c +++ b/src/quic_rx.c @@ -186,6 +186,21 @@ static int qc_pkt_decrypt(struct quic_conn *qc, struct quic_enc_level *qel, rx_ctx, tls_ctx->rx.aead, rx_key, iv); if (!ret) { TRACE_ERROR("quic_tls_decrypt() failed", QUIC_EV_CONN_RXPKT, qc); + + /* RFC 9001 6.6. Limits on AEAD Usage + * + * If the total number of received packets that fail + * authentication within the connection, across all keys, + * exceeds the integrity limit for the selected AEAD, the + * endpoint MUST immediately close the connection with a + * connection error of type AEAD_LIMIT_REACHED and not process + * any more packets. + */ + if (++qc->rx.auth_fails > quic_aead_integrity_limit(tls_ctx->rx.aead)) { + TRACE_ERROR("AEAD integrity limit reached", QUIC_EV_CONN_RXPKT, qc); + quic_set_connection_close(qc, quic_err_transport(QC_ERR_AEAD_LIMIT_REACHED)); + } + goto leave; }