From: Frederic Lecaille Date: Fri, 18 Sep 2026 15:40:52 +0000 (+0200) Subject: BUG/MEDIUM: ssl: crt-list ciphers ignored with AWS-LC X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=refs%2Fheads%2F20260917-ssl-issue-3500;p=haproxy.git BUG/MEDIUM: ssl: crt-list ciphers ignored with AWS-LC Since AWS-LC 1.47.0, SSL_new() copies the cipher lists of the SSL_CTX into the SSL object, and SSL_set_SSL_CTX() does not update this copy while the server handshake uses it. The "ciphers" and "ciphersuites" options of a crt-list line were thus silently ignored: a client could negotiate a cipher excluded by the certificate as long as the bind line allowed it. The same configuration built against OpenSSL rejects such a connection. ssl_sock_switchctx_set_ciphers() now sets both lists again on the SSL object after the context switch. Below AWS-LC 1.46 both lists of an SSL object share the same field, so setting the ciphersuites would erase the ciphers. AWSLC_API_VERSION cannot tell these versions apart, it stays at 32 from 1.44 to 1.49. ssl_sock_check_awslc_cipher_lists() therefore checks the behaviour once at startup, and the ciphersuites are only set when the two lists are separate. Those older versions do not need the fix anyway, their handshake reads the lists of the SSL_CTX, which the switch already updated. This adds the ssl_crt-list_ciphers reg-test for TLSv1.2 and the ssl_crt-list_ciphersuites ones for TLSv1.3, over TCP and QUIC. Thank you to @ygkat for having reported this issue in GH #3500 This should be backported as far as 3.0. Note the code is in ssl_sock.c before 3.1. --- diff --git a/reg-tests/quic/ssl_crt-list_ciphersuites.vtc b/reg-tests/quic/ssl_crt-list_ciphersuites.vtc new file mode 100644 index 000000000..e69d5390e --- /dev/null +++ b/reg-tests/quic/ssl_crt-list_ciphersuites.vtc @@ -0,0 +1,10 @@ +#REGTEST_TYPE=bug +varnishtest "Test for per-certificate ciphersuites in a crt-list over QUIC" +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(2.8)'" +feature cmd "$HAPROXY_PROGRAM -cc 'feature(QUIC) && !feature(QUIC_OPENSSL_COMPAT) && !feature(OPENSSL_WOLFSSL) && !ssllib_name_startswith(LibreSSL) && !ssllib_name_startswith(BoringSSL)'" +# This test checks that the ciphersuites set on a crt-list line are enforced +# over QUIC, even when the bind line allows more of them. +# See GitHub issue #3500. + +setenv VTC_SOCK_TYPE quic +include ${testdir}/../ssl/ssl_crt-list_ciphersuites.vtci diff --git a/reg-tests/ssl/certs/ciphers.crt-list b/reg-tests/ssl/certs/ciphers.crt-list new file mode 100644 index 000000000..57c214128 --- /dev/null +++ b/reg-tests/ssl/certs/ciphers.crt-list @@ -0,0 +1,2 @@ +common.pem [ciphers ECDHE-RSA-AES128-GCM-SHA256] restricted.bug3500.domain.tld +common.pem open.bug3500.domain.tld diff --git a/reg-tests/ssl/certs/ciphersuites.crt-list b/reg-tests/ssl/certs/ciphersuites.crt-list new file mode 100644 index 000000000..62ec8dcd9 --- /dev/null +++ b/reg-tests/ssl/certs/ciphersuites.crt-list @@ -0,0 +1,2 @@ +common.pem [ciphersuites TLS_AES_128_GCM_SHA256] restricted.bug3500.domain.tld +common.pem open.bug3500.domain.tld diff --git a/reg-tests/ssl/ssl_crt-list_ciphers.vtc b/reg-tests/ssl/ssl_crt-list_ciphers.vtc new file mode 100644 index 000000000..70f65f7f7 --- /dev/null +++ b/reg-tests/ssl/ssl_crt-list_ciphers.vtc @@ -0,0 +1,75 @@ +#REGTEST_TYPE=bug +varnishtest "Test for per-certificate ciphers in a crt-list" +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(2.8)'" +feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL) && !feature(OPENSSL_WOLFSSL)'" +# This test checks that the ciphers set on a crt-list line are enforced, even +# when the bind line allows more of them. It uses TLSv1.2, so there is no QUIC +# variant. +# See GitHub issue #3500. +feature ignore_unknown_macro + +server s1 -repeat 2 { + rxreq + txresp +} -start + +haproxy h1 -conf { + global + .if feature(THREAD) + thread-groups 1 + .endif + crt-base ${testdir}/certs + + defaults + mode http + option httplog + retries 0 + log stderr local0 debug err + option logasap + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + listen clear-lst + bind "fd@${clearlst}" + use_backend allowed if { path /allowed } + use_backend excluded if { path /excluded } + use_backend open if { path /open } + + # cipher allowed by the crt-list line + backend allowed + server s1 ${h1_ssllst_sock} ssl verify none sni str(restricted.bug3500.domain.tld) ssl-min-ver TLSv1.2 ssl-max-ver TLSv1.2 ciphers ECDHE-RSA-AES128-GCM-SHA256 + + # cipher allowed by the bind line but excluded by the crt-list line + backend excluded + server s1 ${h1_ssllst_sock} ssl verify none sni str(restricted.bug3500.domain.tld) ssl-min-ver TLSv1.2 ssl-max-ver TLSv1.2 ciphers ECDHE-RSA-AES256-GCM-SHA384 + + # same excluded cipher, but for a certificate without restriction + backend open + server s1 ${h1_ssllst_sock} ssl verify none sni str(open.bug3500.domain.tld) ssl-min-ver TLSv1.2 ssl-max-ver TLSv1.2 ciphers ECDHE-RSA-AES256-GCM-SHA384 + + listen ssl-lst + http-response add-header x-cipher %[ssl_fc_cipher] + bind "fd@${ssllst}" ssl strict-sni crt-list ${testdir}/certs/ciphers.crt-list ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384 + server s1 ${s1_addr}:${s1_port} +} -start + +client c1 -connect ${h1_clearlst_sock} { + txreq -url "/allowed" + rxresp + expect resp.status == 200 + expect resp.http.x-cipher == ECDHE-RSA-AES128-GCM-SHA256 +} -run + +client c1 -connect ${h1_clearlst_sock} { + txreq -url "/excluded" + rxresp + expect resp.status == 503 +} -run + +client c1 -connect ${h1_clearlst_sock} { + txreq -url "/open" + rxresp + expect resp.status == 200 + expect resp.http.x-cipher == ECDHE-RSA-AES256-GCM-SHA384 +} -run diff --git a/reg-tests/ssl/ssl_crt-list_ciphersuites.vtc b/reg-tests/ssl/ssl_crt-list_ciphersuites.vtc new file mode 100644 index 000000000..f622f630b --- /dev/null +++ b/reg-tests/ssl/ssl_crt-list_ciphersuites.vtc @@ -0,0 +1,10 @@ +#REGTEST_TYPE=bug +varnishtest "Test for per-certificate ciphersuites in a crt-list" +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(2.8)'" +feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL) && !ssllib_name_startswith(LibreSSL) && !ssllib_name_startswith(BoringSSL) && !feature(OPENSSL_WOLFSSL)'" +# This test checks that the ciphersuites set on a crt-list line are enforced, +# even when the bind line allows more of them. +# See GitHub issue #3500. + +setenv VTC_SOCK_TYPE stream +include ${testdir}/ssl_crt-list_ciphersuites.vtci diff --git a/reg-tests/ssl/ssl_crt-list_ciphersuites.vtci b/reg-tests/ssl/ssl_crt-list_ciphersuites.vtci new file mode 100644 index 000000000..ce49d5456 --- /dev/null +++ b/reg-tests/ssl/ssl_crt-list_ciphersuites.vtci @@ -0,0 +1,71 @@ +feature ignore_unknown_macro + +server s1 -repeat 2 { + rxreq + txresp +} -start + +haproxy h1 -conf { + global + .if streq("$VTC_SOCK_TYPE",quic) + # required for backend connections + expose-experimental-directives + .endif + .if feature(THREAD) + thread-groups 1 + .endif + crt-base ${testdir}/certs + + defaults + mode http + option httplog + retries 0 + log stderr local0 debug err + option logasap + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + listen clear-lst + bind "fd@${clearlst}" + use_backend allowed if { path /allowed } + use_backend excluded if { path /excluded } + use_backend open if { path /open } + + # ciphersuite allowed by the crt-list line + backend allowed + server s1 ${VTC_SOCK_TYPE}+${h1_ssllst_sock} ssl verify none sni str(restricted.bug3500.domain.tld) ssl-min-ver TLSv1.3 ciphersuites TLS_AES_128_GCM_SHA256 + + # ciphersuite allowed by the bind line but excluded by the crt-list line + backend excluded + server s1 ${VTC_SOCK_TYPE}+${h1_ssllst_sock} ssl verify none sni str(restricted.bug3500.domain.tld) ssl-min-ver TLSv1.3 ciphersuites TLS_AES_256_GCM_SHA384 + + # same excluded ciphersuite, but for a certificate without restriction + backend open + server s1 ${VTC_SOCK_TYPE}+${h1_ssllst_sock} ssl verify none sni str(open.bug3500.domain.tld) ssl-min-ver TLSv1.3 ciphersuites TLS_AES_256_GCM_SHA384 + + listen ssl-lst + http-response add-header x-cipher %[ssl_fc_cipher] + bind ${VTC_SOCK_TYPE}+fd@${ssllst} ssl strict-sni crt-list ${testdir}/certs/ciphersuites.crt-list ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384 + server s1 ${s1_addr}:${s1_port} +} -start + +client c1 -connect ${h1_clearlst_sock} { + txreq -url "/allowed" + rxresp + expect resp.status == 200 + expect resp.http.x-cipher == TLS_AES_128_GCM_SHA256 +} -run + +client c1 -connect ${h1_clearlst_sock} { + txreq -url "/excluded" + rxresp + expect resp.status == 503 +} -run + +client c1 -connect ${h1_clearlst_sock} { + txreq -url "/open" + rxresp + expect resp.status == 200 + expect resp.http.x-cipher == TLS_AES_256_GCM_SHA384 +} -run diff --git a/src/ssl_clienthello.c b/src/ssl_clienthello.c index b0a940c47..e2a0f6aab 100644 --- a/src/ssl_clienthello.c +++ b/src/ssl_clienthello.c @@ -12,6 +12,8 @@ #include #include +#include +#include #include #include #include @@ -145,11 +147,84 @@ int ssl_sock_switchctx_err_cbk(SSL *ssl, int *al, void *priv) return SSL_TLSEXT_ERR_NOACK; } +#if defined(OPENSSL_IS_AWSLC) +/* Set to 1 when the SSL object holds its own TLSv1.3 ciphersuites, which is + * the case from AWS-LC 1.46 on. Before that SSL_set_ciphersuites() and + * SSL_set_cipher_list() write the same field, and the last call wins. + */ +static int awslc_ssl_owns_cipher_lists; + +/* Checks whether SSL_set_ciphersuites() keeps what SSL_set_cipher_list() set + * on the same SSL object. AWSLC_API_VERSION cannot tell, it stays at 32 from + * 1.44 to 1.49 while the behaviour changed in 1.46. + * Always returns 0, a failed check just leaves the ciphersuites of a crt-list + * line unapplied. + */ +static int ssl_sock_check_awslc_cipher_lists(void) +{ + SSL_CTX *ctx; + SSL *ssl = NULL; + int before = 0, after = 0; + + ctx = SSL_CTX_new(SSLv23_server_method()); + if (ctx) + ssl = SSL_new(ctx); + + if (ssl) { + while (SSL_get_cipher_list(ssl, before)) + before++; + } + + if (before < 2 || !SSL_set_ciphersuites(ssl, "TLS_AES_128_GCM_SHA256")) { + ha_warning("Cannot check how AWS-LC keeps the cipher lists of an SSL object. " + "The 'ciphersuites' option of a crt-list line will be ignored.\n"); + } + else { + while (SSL_get_cipher_list(ssl, after)) + after++; + + /* More than one entry left means the TLSv1.2 ciphers survived + * the call above. The two lists then use separate fields. + */ + if (after > 1) + awslc_ssl_owns_cipher_lists = 1; + } + + SSL_free(ssl); + SSL_CTX_free(ctx); + return 0; +} + +REGISTER_POST_CHECK(ssl_sock_check_awslc_cipher_lists); + +/* Sets the ciphers and the ciphersuites of the crt-list line on . + * AWS-LC copies the cipher lists of the SSL_CTX into the SSL object at + * SSL_new() time. SSL_set_SSL_CTX() does not update this copy. So they must be + * set again on the SSL object after a context switch. An option not set in + * keeps the bind line value copied at SSL_new() time. + * Returns 0 on success, -1 on failure. + */ +static int ssl_sock_switchctx_set_ciphers(SSL *ssl, struct ssl_bind_conf *conf) +{ + if (conf->ciphers && !SSL_set_cipher_list(ssl, conf->ciphers)) + return -1; + + /* On the versions where both cipher lists share the same field, this + * call would erase the ciphers set above. + */ + if (awslc_ssl_owns_cipher_lists && + conf->ciphersuites && !SSL_set_ciphersuites(ssl, conf->ciphersuites)) + return -1; + + return 0; +} +#endif /* OPENSSL_IS_AWSLC */ + #if defined(SSL_CTX_set1_curves_list) /* Sets the curves of the crt-list line on . * The SSL libraries copy the curves of the SSL_CTX into the SSL object at * SSL_new() time. SSL_set_SSL_CTX() does not update this copy. So they must be - * set again on the connection after a context switch. + * set again on the SSL object after a context switch. * The value is picked in the same order as when the SSL_CTX is prepared: * of the crt-list line, else of the bind line, else * of the crt-list line, else of the bind line. @@ -485,10 +560,18 @@ sni_lookup: methodVersions[conf->ssl_methods.max].ssl_set_version(ssl, SET_MAX); if (conf->early_data) allow_early = 1; +#if defined(OPENSSL_IS_AWSLC) + if (ssl_sock_switchctx_set_ciphers(ssl, conf) < 0) { + HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock); + TRACE_ERROR("Cannot set the crt-list ciphers on the SSL object", + SSL_EV_CONN_SWITCHCTX_CB|SSL_EV_CONN_ERR, conn); + goto abort; + } +#endif #if defined(SSL_CTX_set1_curves_list) if (ssl_sock_switchctx_set_curves(ssl, s, conf) < 0) { HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock); - TRACE_ERROR("Cannot set the crt-list curves on the connection", + TRACE_ERROR("Cannot set the crt-list curves on the SSL object", SSL_EV_CONN_SWITCHCTX_CB|SSL_EV_CONN_ERR, conn); goto abort; }