RFC: UCI support for legacy/weak TLS in EAP (TTLS/PEAP/FAST/TLS) — seeking feedback before PR

(This is my first post; hopefully, I've understood the guidelines correctly in posting this here.)

I'm planning to submit a PR and would like feedback regarding the approach before I do so.

Problem

When connecting as a client to hotspots that require EAP-TTLS (or PEAP/FAST/TLS) with TLS 1.0 or 1.1, OpenWrt's UCI cannot generate a working wpa_supplicant configuration — even when WPA Supplicant and the underlying TLS library support it.

The failure looks like this:

wpa_supplicant[1775]: SSL: SSL3 alert: read (remote end reported an error):fatal:handshake failure
wpa_supplicant[1775]: OpenSSL: openssl_handshake - SSL_connect error:0A000410:SSL routines::ssl/tls alert handshake failure
wpa_supplicant[1775]: apcli0: CTRL-EVENT-EAP-FAILURE EAP authentication failed

This affects a meaningful number of real-world deployments. Comcast/Xfinity alone claims over 23 million hotspots, and Spectrum, Cox, and others operate similar infrastructure — many configured (or limited by older firmware) with older TLS versions. The issue also affects enterprise EAP deployments on legacy equipment.

There's an existing forum thread with a workaround that hardcodes a fix in hostapd.sh, but that approach is for older OpenWrt versions, breaks on every upgrade, and doesn't apply to current 25.x (which uses ucode): WPA2-Enterprise Client, 802.1x - How to allow weak SSL Ciphers?. Separately, there is another post here with a solution that does not work with 25.x: WPA2-EAP client mode with TLSv1.0

Root Cause

There is no way to express the needed wpa_supplicant parameters through UCI in /etc/config/wireless.

For OpenSSL, two things are needed in the wpa-supplicant generated config:

  • Top-level: openssl_ciphers=DEFAULT@SECLEVEL=0
  • network={} (for TLS/TTLS/PEAP/FAST): phase1="tls_disable_tlsv1_0=0 tls_disable_tlsv1_1=0"
  • Note: tls_min_version= in phase1 is silently ignored by OpenSSL.

For WolfSSL, the requirements for the generated config are different and partially incompatible:

  • Top-level openssl_ciphers=DEFAULT@SECLEVEL=0 causes WolfSSL to fail cipher initialization entirely
  • network={}: phase1="tls_min_version=1.0"
  • Note: tls_disable_tlsv1_* in phase1 is silently ignored by WolfSSL

For MbedTLS: not tested; my understanding is that it doesn't support EAP.

This means the correctly generated config depends on which TLS library wpad was compiled against — but ucode scripts have no built-in way to determine this, and wpad -v does not expose it.

A practical detection method: since wpad built with WolfSSL or MbedTLS is statically linked, ldd /usr/sbin/wpad | grep /usr/lib/libssl returns output only for OpenSSL builds. Is there a cleaner/more reliable way to do this detection?

Proposed Options

Again, I'm planning to submit a PR and would like feedback on which approach is preferred before I do so.

Option 1 — Do nothing

The workaround is hand-editing /usr/share/ucode/wifi/supplicant.uc, which breaks (will be overwritten) on every upgrade of OpenWRT. From a user perspective: not viable long-term.

Option 2 — Raw passthrough: option openssl_ciphers and option phase1

Expose both parameters directly in UCI as pass-through strings. supplicant.uc would apply light guardrails: only emit openssl_ciphers if wpad is linked with OpenSSL; only emit phase1 for EAP types that use it (TLS, TTLS, PEAP, FAST).

// wireless.wifi-iface.json additions
"openssl_ciphers": {
    "description": "Override for OpenSSL ciphers (OpenSSL builds only)",
    "type": "string"
},
"phase1": {
    "description": "Phase 1 EAP parameters (TLS/TTLS/PEAP/FAST only)",
    "type": "string"
}
  // supplicant.uc — top level
  if (interface.config.openssl_ciphers &&
      interface.config.eap_type in ['tls', 'ttls', 'peap', 'fast'] &&
      length(fs.popen('/usr/bin/ldd /usr/sbin/wpad 2>/dev/null | grep /usr/lib/libssl').read('all')) > 0)
         append_raw(`openssl_ciphers=${interface.config.openssl_ciphers}`);
  
     
  
  // supplicant.uc - optional filter then, necessary per-network phase1 passthrough
  // optionally if NOT EAP, TLS, TTLS, PEAP, FAST - remove phase1
  // I'm not sure if this is even necessary
  if (!interface.config.eap_type in ['tls', 'ttls', 'peap', 'fast'])
  		interface.config.phase1 = "";

  network_append_string_vars(config, ['ssid', 
     ... 
     'phase1',
  ]);

Pros: Maximum flexibility, future-proof, LuCI or users can supply whatever parameters are needed.
Cons: Shifts complexity to the user/LuCI; less discoverable; feels inconsistent with other UCI options.

Option 3 — Single option tls_min with generated output (preferred candidate)

Add a single option tls_min '1.0' UCI option. supplicant.uc then generates the correct openssl_ciphers and phase1 content for whichever TLS library is present.

// wireless.wifi-iface.json addition
"tls_min": {
    "description": "Minimum TLS version for EAP authentication",
    "type": "string",
    "enum": [null, "1.0", "1.1", "1.2", "1.3"],
    "default": null
}
  // supplicant.uc — top level
  let tls_min = interface.config.tls_min
      ? (([a, b] = interface.config.tls_min.split(".")), a * 1 + b / 10)
      : null;

  if (tls_min != null &&
      tls_min < 1.2 &&
      interface.config.eap_type in ['tls', 'ttls', 'peap', 'fast'] &&
      length(fs.popen('/usr/bin/ldd /usr/sbin/wpad 2>/dev/null | grep /usr/lib/libssl').read('all')) > 0)
      append_raw(`openssl_ciphers=DEFAULT@SECLEVEL=0`);

  // per-network phase1 generation (applies for both EAP-TLS and EAP-PEAP/TTLS/FAST sections in supplicant.uc)
  if (tls_min != null) {
      let d10 = (tls_min <= 1.0) ? 0 : 1;
      let d11 = (tls_min <= 1.1) ? 0 : 1;
      let d12 = (tls_min <= 1.2) ? 0 : 1;
      let d13 = (tls_min <= 1.3) ? 0 : 1;
      // openssl uses tls_disable_tlsv1_#; wolfssl uses tls_min_version; both silently ignore the other
      config.phase1 = `tls_disable_tlsv1_0=${d10} tls_disable_tlsv1_1=${d11} tls_disable_tlsv1_2=${d12} tls_disable_tlsv1_3=${d13} tls_min_version=${tls_min}`;
  }

  network_append_string_vars(config, ['ssid', 
     ... 
     'phase1',
  ]);

Pros: Single intuitive UCI option; correct output generated automatically per TLS library; consistent with existing UCI design patterns.
Cons: Less flexible if future parameters are needed; logic must be maintained in two places (EAP-TLS section and EAP-PEAP/TTLS/FAST section).

Open Questions for the Community

  1. Option 2 vs Option 3 — Is the passthrough approach or the abstracted tls_min option more consistent with OpenWrt's UCI design philosophy? Option 3 feels cleaner for users and LuCI, but Option 2 offers more flexibility if other phase1 or cipher parameters are ever needed.
  2. Library detection — Is ldd /usr/sbin/wpad | grep libssl the right approach for detecting whether the installed wpad is linked against OpenSSL, or is there a more idiomatic/reliable method from ucode? Since WolfSSL and MbedTLS builds are statically linked, this ldd check appears to reliably distinguish them, but it feels fragile.

From a user/admin side I would lean toward option 3, but only if tls_min '1.0' is very explicit in LuCI/docs that it is for legacy EAP only and weakens the TLS floor for that network. A raw phase1 escape hatch is useful for odd cases, but it is easy to paste a workaround and forget why it is risky. The single option also seems easier to remove later when those hotspots finally move on.

Agreed. Conversely and not the use case I originally highlighted, but a tls_min '1.3' could be used to enforce a stronger level of security. I.e., prevent connecting to older TLS versions even if the defaults allowed it.

What about asking apk what the installed version of wpad is?

Being relatively new to OpenWRT, I was a little cautious about using apk because it seemed new. After your post and doing some reading, I am starting to think that makes a lot of sense. Although perhaps still a little fragile. Maybe something along the lines of:

	if (interface.config.openssl_ciphers &&
	    interface.config.eap_type in ['tls', 'ttls', 'peap', 'fast']) {
		//  cipher strings require wpad-openssl, but not wpad-basic-openssl or others
		let pipe = fs.popen('/usr/bin/apk list --installed wpad-openssl 2>/dev/null');
		let result = pipe.read('all');
		pipe.close();
		if (length(result) > 0)
			append_raw(`openssl_ciphers=${interface.config.openssl_ciphers}`);
	}

The above is for option 2, but a similar pattern could exist for option 3 by replacing the lld check in the original option 3 description with the apk wpad-openssl check listed here.

Option 3 looks easier to document and safer for LuCI, especially if the UI text is explicit that TLS 1.0/1.1 is only for legacy EAP networks. I would also prefer keeping it scoped to the network block, not a global wpad knob. For detection, package-name checks are readable but a bit brittle for variants; if there is no better runtime flag, maybe fail closed for openssl_ciphers and only emit it when the OpenSSL build is positively identified.

Agreed (in concept, some aspects are not in my control). Similarly, one of the things I really didn't like about the "patches" mentioned in the other linked/referenced forum posts is that they were blindly applied to all interfaces

Just for clarity: WPA Supplicant used by OpenWRT requires the openssl_ciphers at the interface level (not the network block, and not blindly applied globally to all interfaces), and both option 2 and option 3 follow that pattern. WPA Supplicant then requires the definition of individual TLS versions in the network block (phase1) of the specific interface for EAP, and again, both option 2 and option 3 follow that pattern.

Yes, the code proposed (per reply in this post) is designed not to fail, but only to emit the configuration when wpad-openssl is positively identified. That pseudo-code is for option 2; however, with option 3, it would also be gated by a check for TLS 1.0 or 1.1 (e.g., tls < 1.2).

If you wanted something more deterministic you could propose a patch to extend hostapd’s build_features mechanism. You could then add a switch for each ssl library and do a simple wpad -v openssl

This suggestion makes a lot of sense, but it make take some time for me to figure out how to achieve this. I'm not really familiar with how the patches structure works or the build of wpad-* to understand what such a patch would look like. I'm sure with enough time, I could probably figure it out but if someone already knows - a little more help would be very welcomed!

Just wanted to call out and share a thanks to @stangri for sharing a fixed implementation example with me. In particular, this both addresses a potential integer division bug in the pseudo-code and avoids any potential floating point problems.

In a real implementation, I might split the location of the append_raw and the phase1= to better align with the supplicant.uc functional structure; however, I wanted to post and share this as a reference that addresses/fixes some of the pseudo-code issues.

  const TLS_ORDER = [ '1.0', '1.1', '1.2', '1.3' ];
  let tls_min = config.tls_min;                         // string or null
  if (tls_min != null && index(TLS_ORDER, tls_min) >= 0 &&
      config.eap_type in [ 'tls', 'ttls', 'peap', 'fast' ]) {

      // OpenSSL must drop SECLEVEL to negotiate <1.2; WolfSSL must NOT get this line.
      if (index(TLS_ORDER, tls_min) < index(TLS_ORDER, '1.2') && wpad_uses_openssl())
          append_raw('openssl_ciphers=DEFAULT@SECLEVEL=0');

      // OpenSSL honors tls_disable_tlsv1_*; WolfSSL honors tls_min_version. Emit both.
      let dis = (v) => (index(TLS_ORDER, v) < index(TLS_ORDER, tls_min)) ? 1 : 0;
      let p1 = sprintf('tls_disable_tlsv1_0=%d tls_disable_tlsv1_1=%d tls_min_version=%s',
                       dis('1.0'), dis('1.1'), tls_min);
      config.phase1 = config.phase1 ? `${config.phase1} ${p1}` : p1;   // append, don't clobber
  }

The patches already take care of integrating the function into wpad for you. You only need to extend build_features.h to include your additional checks.

You also might need to send another CFLAG to the make process, I don’t think anything is actually sent through containing the ssl library at the moment. Closest is CONFIG_TLS but I haven’t had time to see if it triggers anything you could pick up in the code.

CFLAGS += -DCONFIG_SSL_LIB_OPENSSL

And then in build features

#ifdef CONFIG_SSL_LIB_OPENSSL

Etc etc.

@lantis1008 - following up on your suggestion:
It's been a few days, but I wanted to share that I'm still working on this and specifically pursuing your recommendation. I there is already a CONFIG_TLS=openssl that I may be able to use rather than add another flag; however, I'm also considering the wpad-basic-openssl package case (as opposed to wpad-openssl package). It looks like the former may not honor openssl_ciphers, but it also should not fail if it is present (where wolfssl does fail).

I'm leaning toward checking if perhaps for both openssl and CONFIG_EAP_MD5=y where SECLEVEL=0 is required for the legacy MD5 hash algorithm. I'm also considering if this solution should be explicitly fit-for-purpose (only check this and nothing else) or if it should be a little more complete, e.g., indicating mini/basic/full and ssl lib? I'm leaning toward fit-for-purpose because the latter is at risk of quickly becoming too complex...but open to suggestions.

Edit: thinking about this a little more, CONFIG_EAP_MD5 is probably over-complicating things. For consistency, perhaps the ifdef on CONFIG_TLS_OPENSSL, WOLFSSL, MBEDTLS, respectively, is the idiomatic/reliable way to go... I'll give it a try.

Following the suggestion of @lantis1008 - I am testing something along the lines of the following. I did feel it is idiomatic to allow checking the tls implementation rather than a one off only for openssl.

package/network/services/hostapd/Makefile:
just below the SSL_VARIANT definition

ifeq ($(SSL_VARIANT),openssl)
  TARGET_CPPFLAGS += -DCONFIG_TLS_OPENSSL
else ifeq ($(SSL_VARIANT),wolfssl)
  TARGET_CPPFLAGS += -DCONFIG_TLS_WOLFSSL
else ifeq ($(SSL_VARIANT),mbedtls)
  TARGET_CPPFLAGS += -DCONFIG_TLS_MBEDTLS
else ifeq ($(SSL_VARIANT),internal)
  TARGET_CPPFLAGS += -DCONFIG_TLS_INTERNAL
endif

and then package/network/services/hostapd/src/src/utils/build_features.h:

#if defined(CONFIG_TLS_OPENSSL)
	if (!strcmp(feat, "openssl"))
		return 1;
#elif defined(CONFIG_TLS_WOLFSSL)
	if (!strcmp(feat, "wolfssl"))
		return 1;
#elif defined(CONFIG_TLS_MBEDTLS)
	if (!strcmp(feat, "mbedtls"))
		return 1;
#elif defined(CONFIG_TLS_INTERNAL)
	if (!strcmp(feat, "internal"))
		return 1;
#endif

That would allow calling hostapd -vopenssl , hostapd -vwolfssl , etc... consistently and behave like other build features.

Thoughts?

That’s exactly what I had imagined when I was looking at it, I was just hopeful there was already a variable you could hook. I think you’re right and what you’ve done is neat and minimal impact.

Just an update, I have a pretty complete implementation that seems to be working well - and about ready with a draft PR. Unfortunately, I was actively testing/developing, which had me connecting/disconnecting across several networks (and IP addresses). I think this triggered a false positive flag with GitHub - specifically, no email or explaination, just got kicked out and when I tried to log in, it says my account was suspected and points to the terms of service. Since the only thing that correlates is working on this, I think it could only be a false positive. I filed a ticket, but I've read that with a free account, it could take quite a while for them to get to it.

Anybody work or have friends at GitHub that might be able to help?