From 5075a21fe1ac7da074979af3b9cafa6a0c38a9ed Mon Sep 17 00:00:00 2001 From: Frederic Lecaille Date: Mon, 19 May 2025 17:04:21 +0200 Subject: [PATCH] MINOR: quic: implement all remaining callbacks for OpenSSL 3.5 QUIC API The quic_conn struct is modified for two reasons. The first one is to store the encoded version of the local tranport parameter as this is done for USE_QUIC_OPENSSL_COMPAT. Indeed, the local transport parameter "should remain valid until after the parameters have been sent" as mentionned by SSL_set_quic_tls_cbs(3) manual. In our case, the buffer is a static buffer attached to the quic_conn object. qc_ssl_set_quic_transport_params() function whose role is to call SSL_set_tls_quic_transport_params() (aliased by SSL_set_quic_transport_params() to set these local tranport parameter into the TLS stack from the buffer attached to the quic_conn struct. The second quic_conn struct modification is the addition of the new ->prot_level (SSL protection level) member is added to the quic_conn strut to store "the most recent write encryption level set via the OSSL_FUNC_SSL_QUIC_TLS_yield_secret_fn callback (if it has been called)" as mentionned by SSL_set_quic_tls_cbs(3) manual. This patches finally implements the five remaining callacks to make the haproxy QUIC implementation work. OSSL_FUNC_SSL_QUIC_TLS_crypto_send_fn() (ha_quic_ossl_crypto_send) is easy to implement. It calls ha_quic_add_handshake_data() after having converted qc->prot_level TLS protection level value to the correct ssl_encryption_level_t (boringSSL API/quictls) value. OSSL_FUNC_SSL_QUIC_TLS_crypto_recv_rcd_fn() (ha_quic_ossl_crypto_recv_rcd()) provide the non-contiguous addresses to the TLS stack, without releasing them. OSSL_FUNC_SSL_QUIC_TLS_crypto_release_rcd_fn() (ha_quic_ossl_crypto_release_rcd()) release these non-contiguous buffer relying on the fact that the list of encryption level (qc->qel_list) is correctly ordered by SSL protection level secret establishements order (by the TLS stack). OSSL_FUNC_SSL_QUIC_TLS_yield_secret_fn() (ha_quic_ossl_got_transport_params()) is a simple wrapping function over ha_quic_set_encryption_secrets() which is used by boringSSL/quictls API. OSSL_FUNC_SSL_QUIC_TLS_got_transport_params_fn() (ha_quic_ossl_got_transport_params()) role is to store the peer received transport parameters. It simply calls quic_transport_params_store() and set them into the TLS stack calling qc_ssl_set_quic_transport_params(). Also add some comments for all the OpenSSL 3.5 QUIC API callbacks. This patch have no impact on the other use of QUIC API provided by the others TLS stacks. --- include/haproxy/quic_conn-t.h | 5 +- include/haproxy/quic_tls.h | 23 +++++ src/quic_conn.c | 3 + src/quic_ssl.c | 174 ++++++++++++++++++++++++++++++++-- 4 files changed, 194 insertions(+), 11 deletions(-) diff --git a/include/haproxy/quic_conn-t.h b/include/haproxy/quic_conn-t.h index 92dd60ae6..def543fff 100644 --- a/include/haproxy/quic_conn-t.h +++ b/include/haproxy/quic_conn-t.h @@ -334,7 +334,7 @@ struct quic_conn { int tps_tls_ext; int state; enum qc_mux_state mux_state; /* status of the connection/mux layer */ -#ifdef USE_QUIC_OPENSSL_COMPAT +#if defined(USE_QUIC_OPENSSL_COMPAT) || defined(HAVE_OPENSSL_QUIC) unsigned char enc_params[QUIC_TP_MAX_ENCLEN]; /* encoded QUIC transport parameters */ size_t enc_params_len; #endif @@ -345,6 +345,9 @@ struct quic_conn { */ uint64_t hash64; +#ifdef HAVE_OPENSSL_QUIC + uint32_t prot_level; +#endif /* Initial encryption level */ struct quic_enc_level *iel; /* 0-RTT encryption level */ diff --git a/include/haproxy/quic_tls.h b/include/haproxy/quic_tls.h index d8bdf7134..d2e56a89a 100644 --- a/include/haproxy/quic_tls.h +++ b/include/haproxy/quic_tls.h @@ -291,6 +291,29 @@ static inline struct quic_enc_level **ssl_to_qel_addr(struct quic_conn *qc, } } +#ifdef HAVE_OPENSSL_QUIC +/* Simple helper function which translate an OpenSSL SSL protection level + * to a quictls SSL encryption. This way the code which use the OpenSSL QUIC API + * may use the code which uses the quictls API. + */ +static inline enum ssl_encryption_level_t ssl_prot_level_to_enc_level(struct quic_conn *qc, + uint32_t prot_level) +{ + switch (prot_level) { + case OSSL_RECORD_PROTECTION_LEVEL_NONE: + return ssl_encryption_initial; + case OSSL_RECORD_PROTECTION_LEVEL_EARLY: + return ssl_encryption_early_data; + case OSSL_RECORD_PROTECTION_LEVEL_HANDSHAKE: + return ssl_encryption_handshake; + case OSSL_RECORD_PROTECTION_LEVEL_APPLICATION: + return ssl_encryption_application; + default: + return -1; + } +} +#endif + /* Return the address of the QUIC TLS encryption level associated to internal * encryption level and attached to QUIC connection if succeeded, or * NULL if failed. diff --git a/src/quic_conn.c b/src/quic_conn.c index 5feda3b3e..bed8ac102 100644 --- a/src/quic_conn.c +++ b/src/quic_conn.c @@ -1109,6 +1109,9 @@ struct quic_conn *qc_new_conn(const struct quic_version *qv, int ipv4, quic_tls_ku_reset(&qc->ku.nxt_rx); quic_tls_ku_reset(&qc->ku.nxt_tx); +#ifdef HAVE_OPENSSL_QUIC + qc->prot_level = OSSL_RECORD_PROTECTION_LEVEL_NONE; +#endif /* Encryption levels */ qc->iel = qc->eel = qc->hel = qc->ael = NULL; LIST_INIT(&qc->qel_list); diff --git a/src/quic_ssl.c b/src/quic_ssl.c index 04a13cb1e..20b88122f 100644 --- a/src/quic_ssl.c +++ b/src/quic_ssl.c @@ -21,7 +21,7 @@ static int qc_ssl_set_quic_transport_params(struct quic_conn *qc, const struct quic_version *ver, int server) { int ret = 0; -#ifdef USE_QUIC_OPENSSL_COMPAT +#if defined(USE_QUIC_OPENSSL_COMPAT) || defined(HAVE_OPENSSL_QUIC) unsigned char *in = qc->enc_params; size_t insz = sizeof qc->enc_params; size_t *enclen = &qc->enc_params_len; @@ -371,68 +371,222 @@ static int ha_quic_add_handshake_data(SSL *ssl, enum ssl_encryption_level_t leve #ifdef HAVE_OPENSSL_QUIC /************************** OpenSSL QUIC TLS API (>= 3.5.0) *******************/ +/* Callback called by OpenSSL when it needs to send CRYPTO data to the peer. + * This is done from buffer with as number of bytes to be sent. + * This callback must set <*consumed> to the number of bytes which could be + * consumed (buffered in our case) before being sent to the peer. This is always + * when this callback succeeds, or 0 when it fails. + * Return 1 if succeeded, 0 if not. + */ static int ha_quic_ossl_crypto_send(SSL *ssl, const unsigned char *buf, size_t buf_len, size_t *consumed, void *arg) { int ret = 0; struct quic_conn *qc = SSL_get_ex_data(ssl, ssl_qc_app_data_index); + enum ssl_encryption_level_t level = ssl_prot_level_to_enc_level(qc, qc->prot_level); TRACE_ENTER(QUIC_EV_CONN_ADDDATA, qc); - TRACE_LEAVE(QUIC_EV_CONN_ADDDATA, qc); + if (!ha_quic_add_handshake_data(ssl, level, buf, buf_len)) + goto err; + + *consumed = buf_len; + TRACE_DEVEL("CRYPTO data buffered", QUIC_EV_CONN_ADDDATA, qc, &level, &buf_len); + + ret = 1; + leave: + TRACE_LEAVE(QUIC_EV_CONN_ADDDATA, qc); return ret; + err: + *consumed = 0; + TRACE_DEVEL("leaving on error", QUIC_EV_CONN_ADDDATA, qc); + goto leave; } +/* Callback to provide CRYPTO data from the peer to the TLS stack. It must set + * to the address of the buffer which contains the CRYPTO data. + * <*byte_read> value must be the number of bytes of CRYPTO data received. + * Never fail, always return 1. + */ static int ha_quic_ossl_crypto_recv_rcd(SSL *ssl, - const unsigned char **buf, size_t *bytes_read, + const unsigned char **buf, + size_t *bytes_read, void *arg) { - int ret = 0; struct quic_conn *qc = SSL_get_ex_data(ssl, ssl_qc_app_data_index); + struct quic_enc_level *qel; + struct ncbuf *ncbuf = NULL; + struct quic_cstream *cstream = NULL; + ncb_sz_t data = 0; TRACE_ENTER(QUIC_EV_CONN_SSLDATA, qc); - TRACE_LEAVE(QUIC_EV_CONN_SSLDATA, qc); - return ret; + list_for_each_entry(qel, &qc->qel_list, list) { + cstream = qel->cstream; + if (!cstream) + continue; + + ncbuf = &cstream->rx.ncbuf; + if (ncb_is_null(ncbuf)) + continue; + + data = ncb_data(ncbuf, 0); + if (data) + break; + } + + if (data) { + const unsigned char *cdata; + + BUG_ON(ncb_is_null(ncbuf) || !cstream); + /* must not be released at this time. */ + cdata = (const unsigned char *)ncb_head(ncbuf); + cstream->rx.offset += data; + TRACE_DEVEL("buffered crypto data were provided to TLS stack", + QUIC_EV_CONN_PHPKTS, qc, qel); + *buf = cdata; + *bytes_read = data; + } + else { + *buf = NULL; + *bytes_read = 0; + } + + TRACE_LEAVE(QUIC_EV_CONN_SSLDATA, qc); + return 1; } -static int ha_quic_ossl_crypto_release_rcd(SSL *ssl, size_t bytes_read, void *arg) +/* Callback to release the CRYPT data buffer which have been received + * by ha_quic_ossl_crypto_recv_rcd(). + * Return 0 if failed, this means no buffer could be released, or 1 if + * succeeded. + */ +static int ha_quic_ossl_crypto_release_rcd(SSL *ssl, + size_t bytes_read, void *arg) { int ret = 0; struct quic_conn *qc = SSL_get_ex_data(ssl, ssl_qc_app_data_index); + struct quic_enc_level *qel; TRACE_ENTER(QUIC_EV_CONN_RELEASE_RCD, qc); - TRACE_LEAVE(QUIC_EV_CONN_RELEASE_RCD, qc); + list_for_each_entry(qel, &qc->qel_list, list) { + struct quic_cstream *cstream = qel->cstream; + struct ncbuf *ncbuf; + ncb_sz_t data; + + if (!cstream) + continue; + + ncbuf = &cstream->rx.ncbuf; + if (ncb_is_null(ncbuf)) + continue; + + data = ncb_data(ncbuf, 0); + if (!data) + continue; + + data = data > bytes_read ? bytes_read : data; + ncb_advance(ncbuf, data); + bytes_read -= data; + if (ncb_is_empty(ncbuf)) { + TRACE_DEVEL("freeing crypto buf", QUIC_EV_CONN_PHPKTS, qc, qel); + quic_free_ncbuf(ncbuf); + } + + ret = 1; + if (bytes_read == 0) + break; + } + + if (!ret) + goto err; + leave: + TRACE_LEAVE(QUIC_EV_CONN_RELEASE_RCD, qc); return ret; + err: + TRACE_DEVEL("leaving on error", QUIC_EV_CONN_RELEASE_RCD, qc); + goto leave; } +/* Callback called by OpenSSL when has been established at + * SSL protection level. value is 0 for a read secret, + * 1 for a write secret. + * Return 1 if succeeded, 0 if not. + */ static int ha_quic_ossl_yield_secret(SSL *ssl, uint32_t prot_level, int direction, const unsigned char *secret, size_t secret_len, void *arg) { int ret = 0; struct quic_conn *qc = SSL_get_ex_data(ssl, ssl_qc_app_data_index); + enum ssl_encryption_level_t level = ssl_prot_level_to_enc_level(qc, prot_level); TRACE_ENTER(QUIC_EV_CONN_RWSEC, qc); - TRACE_LEAVE(QUIC_EV_CONN_RWSEC, qc); + BUG_ON(level == -1); + + if (!direction) { + /* read secret */ + if (!ha_quic_set_encryption_secrets(ssl, level, secret, NULL, secret_len)) + goto err; + } + else { + /* write secret */ + if (!ha_quic_set_encryption_secrets(ssl, level, NULL, secret, secret_len)) + goto err; + + qc->prot_level = prot_level; + } + + ret = 1; + leave: + TRACE_LEAVE(QUIC_EV_CONN_RWSEC, qc); return ret; + err: + TRACE_DEVEL("leaving on error", QUIC_EV_CONN_RWSEC, qc); + goto leave; } +/* Callback called by OpenSSL when the peer transport parameters have been + * received. + * Return 1 if succeeded, 0 if not. + */ static int ha_quic_ossl_got_transport_params(SSL *ssl, const unsigned char *params, size_t params_len, void *arg) { int ret = 0; struct quic_conn *qc = SSL_get_ex_data(ssl, ssl_qc_app_data_index); + const struct quic_version *ver = + qc->negotiated_version ? qc->negotiated_version : qc->original_version; TRACE_ENTER(QUIC_EV_TRANSP_PARAMS, qc); - TRACE_LEAVE(QUIC_EV_TRANSP_PARAMS, qc); + if (qc->flags & QUIC_FL_CONN_TX_TP_RECEIVED) { + TRACE_PROTO("peer transport parameters already received", + QUIC_EV_TRANSP_PARAMS, qc); + ret = 1; + } + else { + if (!quic_transport_params_store(qc, 0, params, params + params_len) || + !qc_ssl_set_quic_transport_params(qc, ver, 1)) + goto err; + } + + ret = 1; +leave: + TRACE_LEAVE(QUIC_EV_TRANSP_PARAMS, qc); return ret; + err: + TRACE_DEVEL("leaving on error", QUIC_EV_CONN_RWSEC, qc); + goto leave; } +/* Callback called by OpenSSL when it needs to send a TLS to the peer with + * as value. + * Always succeeds. + */ static int ha_quic_ossl_alert(SSL *ssl, unsigned char alert_code, void *arg) { int ret = 1, alert = alert_code;