From 72dbe4edbc87bca52d9a565ad6265601f07971ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 22:23:49 -0500 Subject: [PATCH] Keep TLS for components that re-include esp-tls and gate the sdkconfig escape hatch on value --- esphome/components/esp32/__init__.py | 29 ++++++++++++++----- .../config/tls_sdkconfig_tls_enabled_n.yaml | 9 ++++++ tests/component_tests/esp32/test_esp32.py | 29 ++++++++++++++++++- 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 tests/component_tests/esp32/config/tls_sdkconfig_tls_enabled_n.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index dae8ff0367..69efca27ce 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2333,13 +2333,23 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None: set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True) -# User sdkconfig_options that mean "keep TLS on"; each becomes a request_tls(). +# User sdkconfig_options that mean "keep TLS on" when set to y. _MBEDTLS_TLS_ON_OPTIONS = ( "CONFIG_MBEDTLS_TLS_ENABLED", "CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", "CONFIG_MBEDTLS_TLS_SERVER_ONLY", "CONFIG_MBEDTLS_TLS_CLIENT_ONLY", ) +# Any user option under these prefixes only makes sense with TLS compiled in. +_TLS_OPTION_PREFIXES = ("CONFIG_ESP_TLS_", "CONFIG_MBEDTLS_SSL_", "CONFIG_ESP_HTTPS_") + + +def _user_sdkconfig_wants_tls(options: dict[str, Any]) -> bool: + return any( + (name in _MBEDTLS_TLS_ON_OPTIONS and value == "y") + or name.startswith(_TLS_OPTION_PREFIXES) + for name, value in options.items() + ) @coroutine_with_priority(CoroPriority.FINAL) @@ -2352,8 +2362,14 @@ async def _reconcile_mbedtls_sdkconfig() -> None: """ data = _mbedtls_sdkconfig() idf6 = idf_version() >= cv.Version(6, 0, 0) + # A component that re-includes esp-tls on its own (external components + # predating request_tls()) wants TLS just as much as a request_tls() call. + tls_required = ( + data.tls_required + or "esp-tls" not in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] + ) - if not CORE.using_arduino and not data.tls_required: + if not CORE.using_arduino and not tls_required: # IDF 6 made CONFIG_MBEDTLS_TLS_ENABLED a normal bool; on IDF 5 it has # no prompt and is only reachable through the "None" TLS role choice. if idf6: @@ -2363,7 +2379,8 @@ async def _reconcile_mbedtls_sdkconfig() -> None: # Enterprise WiFi selects TLS back on; wifi writes this itself, but # esp_wifi can also be in the build without a wifi: block (openthread). set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False) - # WiFi, Bluetooth and secure boot re-select ECP through Kconfig. + # WiFi (ESP_WIFI_MBEDTLS_CRYPTO), Bluetooth and signed apps + # (SECURE_SIGNED_APPS) select ECP back on through Kconfig. if not data.ecp_required: set_idf_sdkconfig_default("CONFIG_MBEDTLS_ECP_C", False) set_idf_sdkconfig_default("CONFIG_MBEDTLS_PEM_WRITE_C", False) @@ -3089,11 +3106,7 @@ async def to_code(config): # so it still gets the CMN variant pinned. if conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE") == "y": require_certificate_bundle() - # A TLS role or esp-tls option in sdkconfig_options means the user relies on TLS. - elif any( - name in _MBEDTLS_TLS_ON_OPTIONS or name.startswith("CONFIG_ESP_TLS_") - for name in conf[CONF_SDKCONFIG_OPTIONS] - ): + if _user_sdkconfig_wants_tls(conf[CONF_SDKCONFIG_OPTIONS]): request_tls() # Components from YAML are added in a separate coroutine with FINAL priority diff --git a/tests/component_tests/esp32/config/tls_sdkconfig_tls_enabled_n.yaml b/tests/component_tests/esp32/config/tls_sdkconfig_tls_enabled_n.yaml new file mode 100644 index 0000000000..5bd4e97130 --- /dev/null +++ b/tests/component_tests/esp32/config/tls_sdkconfig_tls_enabled_n.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + sdkconfig_options: + CONFIG_MBEDTLS_TLS_ENABLED: n diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 6803f2ac19..ae87e8c3e4 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -433,7 +433,7 @@ _IDF6 = cv.Version(6, 0, 0) @pytest.mark.parametrize( - ("framework", "idf", "data", "preset", "expected"), + ("framework", "idf", "data", "preset", "expected", "excluded"), [ pytest.param( PlatformFramework.ESP32_IDF, @@ -441,8 +441,19 @@ _IDF6 = cv.Version(6, 0, 0) MbedtlsSdkconfigData(), {}, {**_TLS_OFF_IDF5, **_PEER_CERT_PKCS7_OFF}, + {"esp-tls"}, id="idf5_no_tls_user", ), + pytest.param( + # An external component that only re-included esp-tls keeps TLS. + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(), + {}, + _PEER_CERT_PKCS7_OFF, + set(), + id="idf_esp_tls_reincluded", + ), pytest.param( PlatformFramework.ESP32_IDF, _IDF6, @@ -454,6 +465,7 @@ _IDF6 = cv.Version(6, 0, 0) "CONFIG_MBEDTLS_SHA384_C": False, "CONFIG_MBEDTLS_SHA512_C": False, }, + {"esp-tls"}, id="idf6_drops_sha512", ), pytest.param( @@ -462,6 +474,7 @@ _IDF6 = cv.Version(6, 0, 0) MbedtlsSdkconfigData(sha512_required=True), {}, {**_TLS_OFF_IDF6, **_PEER_CERT_PKCS7_OFF}, + {"esp-tls"}, id="idf6_sha512_required", ), pytest.param( @@ -470,6 +483,7 @@ _IDF6 = cv.Version(6, 0, 0) MbedtlsSdkconfigData(tls_required=True), {}, _PEER_CERT_PKCS7_OFF, + {"esp-tls"}, id="idf_tls_requested", ), pytest.param( @@ -485,6 +499,7 @@ _IDF6 = cv.Version(6, 0, 0) "CONFIG_MBEDTLS_X509_CSR_PARSE_C": False, **_PEER_CERT_PKCS7_OFF, }, + {"esp-tls"}, id="idf_ecp_without_tls", ), pytest.param( @@ -497,6 +512,7 @@ _IDF6 = cv.Version(6, 0, 0) "CONFIG_MBEDTLS_ECP_C": RawSdkconfigValue("y"), **_PEER_CERT_PKCS7_OFF, }, + {"esp-tls"}, id="idf_user_ecp_wins", ), pytest.param( @@ -509,6 +525,7 @@ _IDF6 = cv.Version(6, 0, 0) "CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE": True, "CONFIG_MBEDTLS_PKCS7_C": True, }, + {"esp-tls"}, id="idf_peer_cert_pkcs7_required", ), pytest.param( @@ -517,6 +534,7 @@ _IDF6 = cv.Version(6, 0, 0) MbedtlsSdkconfigData(disable_peer_cert=False, disable_pkcs7=False), {}, _TLS_OFF_IDF5, + {"esp-tls"}, id="idf_advanced_disables_off", ), pytest.param( @@ -525,6 +543,7 @@ _IDF6 = cv.Version(6, 0, 0) MbedtlsSdkconfigData(), {}, _PEER_CERT_PKCS7_OFF, + {"esp-tls"}, id="arduino_keeps_tls", ), ], @@ -536,6 +555,7 @@ def test_reconcile_mbedtls_sdkconfig( data: MbedtlsSdkconfigData, preset: dict[str, Any], expected: dict[str, Any], + excluded: set[str], ) -> None: """The FINAL-priority reconciler turns TLS off only when nothing requested it; user sdkconfig_options always win.""" @@ -544,6 +564,7 @@ def test_reconcile_mbedtls_sdkconfig( KEY_IDF_VERSION: idf, KEY_SDKCONFIG_OPTIONS: dict(preset), KEY_MBEDTLS_SDKCONFIG: data, + KEY_EXCLUDE_COMPONENTS: excluded, } asyncio.run(_reconcile_mbedtls_sdkconfig()) @@ -571,6 +592,12 @@ def test_reconcile_mbedtls_sdkconfig( ), pytest.param("tls_sdkconfig_esp_tls.yaml", False, False, id="raw_esp_tls"), pytest.param("tls_sdkconfig_tls_role.yaml", False, False, id="raw_tls_role"), + # A role option set to n is not a request. + pytest.param( + "tls_sdkconfig_tls_enabled_n.yaml", True, True, id="raw_tls_enabled_n" + ), + # SECURE_SIGNED_APPS selects ECP back on in Kconfig; ESPHome still writes the default. + pytest.param("signed_ota_ecdsa256_c6.yaml", True, True, id="signed_ota_ecdsa"), ], ) def test_tls_disabled_sdkconfig(