[PATCH] OpenSSL: Configure TLS 1.3 ciphersuites from openssl_ciphers
Karsten Sperling
ksperling at apple.com
Sun Oct 11 16:14:15 PDT 2026
OpenSSL introduced a separate API for configuration of TLS 1.3 cipher
suites, distinct from the list of TLS <=1.2 ciphers. Other
implementations (including wolfSSL, BoringSSL, LibreSSL, and MbedTLS)
accept both generations in their existing cipher list APIs. This change
adds support for configuring TLS 1.3 suites in OpenSSL from a similar
combined list.
This is done by separating the combined list into TLS 1.3 suites (they
all start with "TLS_"), and legacy (pre-1.3) ciphers. If the list
contains no legacy ciphers then that list is explicitly cleared,
effectively resulting in a 1.3-only configuration. If the list contains
no TLS 1.3 suites, the default suites are left unchanged. This preserves
the current behavior of letting such configurations limit the list of
accepted legacy ciphers without disabling TLS 1.3 as an unintended side
effect.
Note that wolfSSL has similar logic driven by its combined cipher list
API internally. BoringSSL implements a more opinionated version of the
same policy by recognizing 1.3 suite names in the cipher list, but
simply keeps all the 1.3 suites it supports enabled at all times.
Signed-off-by: Karsten Sperling <ksperling at apple.com>
---
hostapd/hostapd.conf | 5 ++
src/crypto/tls_openssl.c | 124 +++++++++++++++++++++++++++--
tests/hwsim/test_ap_eap.py | 29 +++++++
wpa_supplicant/wpa_supplicant.conf | 6 ++
4 files changed, 157 insertions(+), 7 deletions(-)
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index 8f902b753..f56d3ed98 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -1508,6 +1508,11 @@ eap_server=0
# See https://www.openssl.org/docs/apps/ciphers.html for OpenSSL documentation
# on cipher suite configuration. This is applicable only if hostapd is built to
# use OpenSSL.
+# TLSv1.3 cipher suite names (e.g. "TLS_AES_256_GCM_SHA384") may also be
+# included in the list; in this case they replace the default set of TLSv1.3
+# suites that OpenSSL would otherwise enable. (There is no corresponding
+# logic for TLSv1.2 and earlier ciphers: if none are listed, none are enabled.)
+# Note that using TLSv1.3 also requires [ENABLE-TLSv1.3] in tls_flags.
#openssl_ciphers=DEFAULT:!EXP:!LOW
# OpenSSL ECDH curves
diff --git a/src/crypto/tls_openssl.c b/src/crypto/tls_openssl.c
index 032bf05f2..e4084dd9c 100644
--- a/src/crypto/tls_openssl.c
+++ b/src/crypto/tls_openssl.c
@@ -127,6 +127,116 @@ static size_t SSL_SESSION_get_master_key(const SSL_SESSION *session,
#endif
+
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L) && \
+ !defined(LIBRESSL_VERSION_NUMBER) && !defined(OPENSSL_IS_BORINGSSL)
+/*
+ * OpenSSL has introduced a separate API for TLS 1.3 cipher suites distinct
+ * from the API that configures ciphers for TLS versions <=1.2, whereas other
+ * implementations (e.g. wolfSSL, LibreSSL, and BoringSSL) kept a single API.
+ *
+ * Provide a shim that separates a combined list into TLS 1.3 cipher suites
+ * (they all start with "TLS_"), and pre-1.3 ciphers. If the list contains no
+ * TLS 1.3 cipher suites, the existing suites are left untouched (matching
+ * current / desired behavior), but if there are no pre-1.3 ciphers that list
+ * is explicitly cleared. This allows for TLS 1.3-only configurations.
+ *
+ * Exactly one of ctx and ssl must be passed; this is handled by the wrapper
+ * macros tls_{ctx,ssl}_set_ciphers defined below.
+ */
+static int tls_set_ciphers(SSL_CTX *ctx, SSL *ssl, const char *combined)
+{
+ int success = 0;
+ char *ciphers = NULL, *ciphers_end;
+ char *suites = NULL, *suites_end;
+ int len = os_strlen(combined);
+
+ ciphers = os_malloc(len + 1);
+ suites = os_malloc(len + 1);
+ if (!ciphers || !suites)
+ goto done;
+
+ ciphers_end = ciphers;
+ suites_end = suites;
+ for (;;) {
+ const char *sep = combined;
+ /* OpenSSL accepts ',' and ' ' in addition to ':' */
+ while (*sep && *sep != ':' && *sep != ',' && *sep != ' ')
+ sep++;
+ len = sep - combined;
+ if (os_strncmp(combined, "TLS_", 4) == 0) {
+ os_memcpy(suites_end, combined, len);
+ suites_end += len;
+ *suites_end++ = ':';
+ } else if (len > 0) {
+ os_memcpy(ciphers_end, combined, len);
+ ciphers_end += len;
+ *ciphers_end++ = ':';
+ }
+ if (!*sep)
+ break;
+ combined = sep + 1;
+ }
+
+ /* Reject an empty combined list outright. */
+ if (ciphers == ciphers_end && suites == suites_end)
+ goto done;
+
+ if (ciphers_end > ciphers) {
+ *--ciphers_end = '\0';
+ wpa_printf(MSG_DEBUG,
+ "OpenSSL: setting cipher_list '%s'",
+ ciphers);
+ } else {
+ *ciphers = '\0';
+ wpa_printf(MSG_DEBUG, "OpenSSL: clearing cipher_list");
+ }
+ success = (ctx != NULL)
+ ? SSL_CTX_set_cipher_list(ctx, ciphers)
+ : SSL_set_cipher_list(ssl, ciphers);
+ if (ciphers == ciphers_end && !success) {
+ /* Passing an empty string clears the pre-1.3 cipher list as
+ * intended, but SSL[_CTX]_set_cipher_list() reports the fact
+ * that no cipher was selected as SSL_R_NO_CIPHER_MATCH. Treat
+ * this specific error as a success. */
+ unsigned long err = ERR_peek_last_error();
+ if (ERR_GET_LIB(err) != ERR_LIB_SSL ||
+ ERR_GET_REASON(err) != SSL_R_NO_CIPHER_MATCH)
+ goto done;
+ ERR_clear_error();
+ success = 1;
+ } else if (!success) {
+ goto done;
+ }
+
+ if (suites_end > suites) {
+ *--suites_end = '\0';
+ wpa_printf(MSG_DEBUG,
+ "OpenSSL: setting ciphersuites '%s'",
+ suites);
+ success = (ctx != NULL)
+ ? SSL_CTX_set_ciphersuites(ctx, suites)
+ : SSL_set_ciphersuites(ssl, suites);
+ }
+
+done:
+ os_free(ciphers);
+ os_free(suites);
+ return success;
+}
+
+#define tls_ctx_set_ciphers(ctx, ciphers) \
+ tls_set_ciphers(ctx, NULL, ciphers)
+#define tls_ssl_set_ciphers(ssl, ciphers) \
+ tls_set_ciphers(NULL, ssl, ciphers)
+#else /* >= 1.1.1 and !LIBRESSL_VERSION_NUMBER and !OPENSSL_IS_BORINGSSL */
+#define tls_ctx_set_ciphers(ctx, ciphers) \
+ SSL_CTX_set_cipher_list(ctx, ciphers)
+#define tls_ssl_set_ciphers(ssl, ciphers) \
+ SSL_set_cipher_list(ssl, ciphers)
+#endif /* >= 1.1.1 and !LIBRESSL_VERSION_NUMBER and !OPENSSL_IS_BORINGSSL */
+
+
#if OPENSSL_VERSION_NUMBER < 0x10100000L
static const unsigned char * ASN1_STRING_get0_data(const ASN1_STRING *x)
{
@@ -1439,7 +1549,7 @@ void * tls_init(const struct tls_config *conf)
ciphers = conf->openssl_ciphers;
else
ciphers = TLS_DEFAULT_CIPHERS;
- if (SSL_CTX_set_cipher_list(ssl, ciphers) != 1) {
+ if (tls_ctx_set_ciphers(ssl, ciphers) != 1) {
wpa_printf(MSG_ERROR,
"OpenSSL: Failed to set cipher string '%s'",
ciphers);
@@ -3599,7 +3709,7 @@ static int tls_set_conn_flags(struct tls_connection *conn, unsigned int flags,
openssl_ciphers);
ciphers = openssl_ciphers;
}
- if (SSL_set_cipher_list(ssl, ciphers) != 1) {
+ if (tls_ssl_set_ciphers(ssl, ciphers) != 1) {
wpa_printf(MSG_INFO,
"OpenSSL: Failed to set Suite B ciphers");
return -1;
@@ -3618,7 +3728,7 @@ static int tls_set_conn_flags(struct tls_connection *conn, unsigned int flags,
openssl_ciphers);
ciphers = openssl_ciphers;
}
- if (SSL_set_cipher_list(ssl, ciphers) != 1) {
+ if (tls_ssl_set_ciphers(ssl, ciphers) != 1) {
wpa_printf(MSG_INFO,
"OpenSSL: Failed to set Suite B ciphers");
return -1;
@@ -3706,7 +3816,7 @@ static int tls_set_conn_flags(struct tls_connection *conn, unsigned int flags,
}
#else /* OPENSSL_IS_BORINGSSL */
if (!(flags & (TLS_CONN_SUITEB | TLS_CONN_SUITEB_NO_ECDH)) &&
- openssl_ciphers && SSL_set_cipher_list(ssl, openssl_ciphers) != 1) {
+ openssl_ciphers && tls_ssl_set_ciphers(ssl, openssl_ciphers) != 1) {
wpa_printf(MSG_INFO,
"OpenSSL: Failed to set openssl_ciphers '%s'",
openssl_ciphers);
@@ -3714,7 +3824,7 @@ static int tls_set_conn_flags(struct tls_connection *conn, unsigned int flags,
}
#endif /* OPENSSL_IS_BORINGSSL */
#else /* CONFIG_SUITEB */
- if (openssl_ciphers && SSL_set_cipher_list(ssl, openssl_ciphers) != 1) {
+ if (openssl_ciphers && tls_ssl_set_ciphers(ssl, openssl_ciphers) != 1) {
wpa_printf(MSG_INFO,
"OpenSSL: Failed to set openssl_ciphers '%s'",
openssl_ciphers);
@@ -5916,7 +6026,7 @@ int tls_connection_set_params(void *tls_ctx, struct tls_connection *conn,
}
#endif /* OPENSSL_IS_BORINGSSL */
#endif /* CONFIG_SUITEB */
- if (ciphers && SSL_set_cipher_list(conn->ssl, ciphers) != 1) {
+ if (ciphers && tls_ssl_set_ciphers(conn->ssl, ciphers) != 1) {
wpa_printf(MSG_INFO,
"OpenSSL: Failed to set cipher string '%s'",
ciphers);
@@ -6159,7 +6269,7 @@ int tls_global_set_params(void *tls_ctx,
data->openssl_ciphers = NULL;
}
if (params->openssl_ciphers &&
- SSL_CTX_set_cipher_list(ssl_ctx, params->openssl_ciphers) != 1) {
+ tls_ctx_set_ciphers(ssl_ctx, params->openssl_ciphers) != 1) {
wpa_printf(MSG_INFO,
"OpenSSL: Failed to set cipher string '%s'",
params->openssl_ciphers);
diff --git a/tests/hwsim/test_ap_eap.py b/tests/hwsim/test_ap_eap.py
index 399968678..0a79e4e3e 100644
--- a/tests/hwsim/test_ap_eap.py
+++ b/tests/hwsim/test_ap_eap.py
@@ -6313,6 +6313,35 @@ def test_ap_wpa2_eap_tls_13_fragmentation(dev, apdev):
phase1="tls_disable_tlsv1_0=1 tls_disable_tlsv1_1=1 tls_disable_tlsv1_2=1 tls_disable_tlsv1_3=0",
fragment_size="100")
+def test_ap_wpa2_eap_tls_13_openssl_ciphers_only_13(dev, apdev):
+ """EAP-TLS with only TLSv1.3 ciphers in openssl_ciphers"""
+ params = int_eap_server_params()
+ params['tls_flags'] = '[ENABLE-TLSv1.2][ENABLE-TLSv1.3]'
+ params['openssl_ciphers'] = 'TLS_AES_128_GCM_SHA256'
+ hapd = hostapd.add_ap(apdev[0], params)
+ check_tls13_support(hapd)
+
+ check_tls13_support(dev[0])
+ # TLSv1.2-only client should fail, only TLSv1.3 ciphers available
+ id = eap_connect(dev[0], hapd, "TLS", "tls user",
+ ca_cert="auth_serv/ca.pem",
+ client_cert="auth_serv/user.pem",
+ private_key="auth_serv/user.key",
+ phase1="tls_disable_tlsv1_0=1 tls_disable_tlsv1_1=1 tls_disable_tlsv1_2=0 tls_disable_tlsv1_3=1",
+ expect_failure=True, local_error_report=True)
+ dev[0].request("REMOVE_NETWORK all")
+ dev[0].dump_monitor()
+ # TLVv1.2 / TLSv1.3 client should succeed
+ id = eap_connect(dev[0], hapd, "TLS", "tls user",
+ ca_cert="auth_serv/ca.pem",
+ client_cert="auth_serv/user.pem",
+ private_key="auth_serv/user.key",
+ phase1="tls_disable_tlsv1_0=1 tls_disable_tlsv1_1=1 tls_disable_tlsv1_2=0 tls_disable_tlsv1_3=0")
+ if dev[0].get_status_field("eap_tls_version") != "TLSv1.3":
+ raise Exception("Unexpected TLS version")
+ if dev[0].get_status_field('EAP TLS cipher') != params['openssl_ciphers']:
+ raise Exception("Unexpected TLS cipher")
+
def test_ap_wpa2_eap_ttls_13(dev, apdev):
"""EAP-TTLS and TLS 1.3"""
params = hostapd.wpa2_eap_params(ssid="test-wpa2-eap")
diff --git a/wpa_supplicant/wpa_supplicant.conf b/wpa_supplicant/wpa_supplicant.conf
index 4fc3cca52..16463b605 100644
--- a/wpa_supplicant/wpa_supplicant.conf
+++ b/wpa_supplicant/wpa_supplicant.conf
@@ -188,6 +188,12 @@ fast_reauth=1
# See https://www.openssl.org/docs/apps/ciphers.html for OpenSSL documentation
# on cipher suite configuration. This is applicable only if wpa_supplicant is
# built to use OpenSSL.
+# TLSv1.3 cipher suite names (e.g. "TLS_AES_256_GCM_SHA384") may also be
+# included in the list; in this case they replace the default set of TLSv1.3
+# suites that OpenSSL would otherwise enable. (There is no corresponding
+# logic for TLSv1.2 and earlier ciphers: if none are listed, none are enabled.)
+# Note that using TLSv1.3 also requires tls_disable_tlsv1_3=0 in the network's
+# phase1 parameter.
#openssl_ciphers=DEFAULT:!EXP:!LOW
# Dynamic EAP methods
--
2.50.1 (Apple Git-155)
More information about the Hostap
mailing list