From 7d26655eb4f45854cb837cd8cfa8a1eef95c91a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 22:01:24 -0500 Subject: [PATCH] Disable mbedTLS TLS and TLS-only crypto when no component needs them --- esphome/components/audio/__init__.py | 5 +- esphome/components/esp32/__init__.py | 184 ++++++++++++----- esphome/components/esp32/const.py | 1 + esphome/components/http_request/__init__.py | 6 +- esphome/components/mqtt/__init__.py | 5 +- esphome/components/nextion/display.py | 5 +- esphome/components/openthread/__init__.py | 3 + esphome/components/web_server_idf/__init__.py | 5 +- .../web_server_idf/web_server_idf.cpp | 8 +- esphome/components/wifi/__init__.py | 4 + .../esp32/config/tls_openthread_c6.yaml | 19 ++ .../esp32/config/tls_sdkconfig_esp_tls.yaml | 9 + .../esp32/config/tls_sdkconfig_tls_role.yaml | 9 + .../esp32/config/tls_wifi_eap.yaml | 15 ++ tests/component_tests/esp32/test_esp32.py | 191 +++++++++++++++++- 15 files changed, 390 insertions(+), 79 deletions(-) create mode 100644 tests/component_tests/esp32/config/tls_openthread_c6.yaml create mode 100644 tests/component_tests/esp32/config/tls_sdkconfig_esp_tls.yaml create mode 100644 tests/component_tests/esp32/config/tls_sdkconfig_tls_role.yaml create mode 100644 tests/component_tests/esp32/config/tls_wifi_eap.yaml diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 2a5304be77..8eeac47347 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -6,7 +6,7 @@ import esphome.codegen as cg from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, - include_builtin_idf_component, + request_http_client, require_certificate_bundle, ) import esphome.config_validation as cv @@ -334,8 +334,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N async def to_code(config: ConfigType) -> None: - # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) - include_builtin_idf_component("esp_http_client") + request_http_client() # HTTPS streams verify the server against the root certificate bundle require_certificate_bundle() diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 48bb7bf6a1..dae8ff0367 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -73,6 +73,7 @@ from .const import ( KEY_FLASH_SIZE, KEY_FULL_CERT_BUNDLE, KEY_IDF_VERSION, + KEY_MBEDTLS_SDKCONFIG, KEY_NETWORK_SDKCONFIG, KEY_PATH, KEY_REF, @@ -218,7 +219,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing "console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers - "esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf + "esp-tls", # TLS wrapper - re-included by request_tls() "esp_adc", # ADC driver - only needed by adc component "esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back "esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back @@ -740,6 +741,48 @@ def request_software_coexistence() -> None: include_builtin_idf_component("esp_coex") +@dataclass +class MbedtlsSdkconfigData: + """Inputs for the mbedTLS sdkconfig flags, reconciled at FINAL. + + Components call request_tls() / require_mbedtls_*() instead of writing the + CONFIG_MBEDTLS_* flags directly; _reconcile_mbedtls_sdkconfig() decides the + final values once every to_code has run. + """ + + tls_required: bool = False # TLS/DTLS handshake user + ecp_required: bool = False # ECDH/ECDSA without TLS (openthread SRP host key) + peer_cert_required: bool = False # keep the peer certificate after the handshake + pkcs7_required: bool = False # PKCS#7 parsing + sha512_required: bool = False # SHA-384/SHA-512 + # esp32 advanced disable_mbedtls_peer_cert / disable_mbedtls_pkcs7 options + disable_peer_cert: bool = True + disable_pkcs7: bool = True + + +def _mbedtls_sdkconfig() -> MbedtlsSdkconfigData: + data = CORE.data[KEY_ESP32] + if KEY_MBEDTLS_SDKCONFIG not in data: + data[KEY_MBEDTLS_SDKCONFIG] = MbedtlsSdkconfigData() + return data[KEY_MBEDTLS_SDKCONFIG] + + +def request_tls() -> None: + """Request the mbedTLS TLS stack and the esp-tls wrapper. + + Without a request TLS and its ECP/PEM-write/CRL/CSR crypto compile out; + hashes, AES and RSA stay available. + """ + _mbedtls_sdkconfig().tls_required = True + include_builtin_idf_component("esp-tls") + + +def request_http_client() -> None: + """Request ESP-IDF's HTTP client; it links esp_tls even for plain http.""" + include_builtin_idf_component("esp_http_client") + request_tls() + + def add_idf_component( *, name: str, @@ -1737,10 +1780,7 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required" # Feature requirement tracking - components can call require_* functions to re-enable # These are stored in CORE.data[KEY_ESP32] dict KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required" -KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required" -KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required" KEY_FATFS_REQUIRED = "fatfs_required" -KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required" KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required" KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED = "libc_picolibc_newlib_compat_required" @@ -1779,6 +1819,8 @@ def require_certificate_bundle() -> None: certificates (http_request, audio streaming) call this so the bundle is compiled and gen_crt_bundle runs only when something uses it. """ + # esp_crt_bundle.c calls mbedtls_ssl_conf_*, so a bundle always needs TLS. + request_tls() CORE.data[KEY_ESP32][KEY_CERT_BUNDLE] = True @@ -1804,33 +1846,29 @@ def require_usb_serial_jtag_secondary() -> None: CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True -def require_mbedtls_peer_cert() -> None: - """Mark that mbedTLS peer certificate retention is required by a component. +def require_mbedtls_ecp() -> None: + """Keep mbedTLS elliptic curve support (ECDH/ECDSA) without requesting TLS. - Call this from components that need access to the peer certificate after - the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE - from being disabled. + Call this from components that sign or verify with ECDSA outside a TLS + handshake (openthread's SRP host key). WiFi, Bluetooth and secure boot + select it through Kconfig on their own. """ - CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True + _mbedtls_sdkconfig().ecp_required = True + + +def require_mbedtls_peer_cert() -> None: + """Keep the peer certificate after the TLS handshake (CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE).""" + _mbedtls_sdkconfig().peer_cert_required = True def require_mbedtls_pkcs7() -> None: - """Mark that mbedTLS PKCS#7 support is required by a component. - - Call this from components that need PKCS#7 certificate validation. - This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled. - """ - CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True + """Keep mbedTLS PKCS#7 support (CONFIG_MBEDTLS_PKCS7_C).""" + _mbedtls_sdkconfig().pkcs7_required = True def require_mbedtls_sha512() -> None: - """Mark that mbedTLS SHA-384/SHA-512 support is required by a component. - - Call this from components that need to verify TLS certificates or signatures - using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C - and CONFIG_MBEDTLS_SHA512_C from being disabled. - """ - CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True + """Keep mbedTLS SHA-384/SHA-512 (CONFIG_MBEDTLS_SHA384_C / CONFIG_MBEDTLS_SHA512_C).""" + _mbedtls_sdkconfig().sha512_required = True def idf_version() -> cv.Version: @@ -2295,6 +2333,62 @@ 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(). +_MBEDTLS_TLS_ON_OPTIONS = ( + "CONFIG_MBEDTLS_TLS_ENABLED", + "CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", + "CONFIG_MBEDTLS_TLS_SERVER_ONLY", + "CONFIG_MBEDTLS_TLS_CLIENT_ONLY", +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _reconcile_mbedtls_sdkconfig() -> None: + """Reconcile the mbedTLS sdkconfig flags after every request_tls() / require_mbedtls_*() call. + + mbedtls cannot be excluded from an IDF build (bootloader_support needs its + SHA-256), but with no TLS user the ssl_*.c sources and the TLS-only crypto + compile to empty objects. User sdkconfig_options win. + """ + data = _mbedtls_sdkconfig() + idf6 = idf_version() >= cv.Version(6, 0, 0) + + if not CORE.using_arduino and not data.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: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_ENABLED", False) + else: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_DISABLED", True) + # 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. + if not data.ecp_required: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_ECP_C", False) + set_idf_sdkconfig_default("CONFIG_MBEDTLS_PEM_WRITE_C", False) + set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CRL_PARSE_C", False) + set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CSR_PARSE_C", False) + + # Keeping the peer certificate costs ~4KB heap per connection. + if data.peer_cert_required: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True) + elif data.disable_peer_cert: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False) + + if data.pkcs7_required: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", True) + elif data.disable_pkcs7: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", False) + + # SHA-384 shares the SHA-512 compression function, so both go together. + # Only IDF 6.0's PSA engine links a ~3KB software fallback for them; on + # IDF 5 they are a single hardware-only option with no code size cost. + if idf6 and not data.sha512_required: + set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA384_C", False) + set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA512_C", False) + + @coroutine_with_priority(CoroPriority.FINAL) async def _reconcile_network_sdkconfig() -> None: """Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags. @@ -2949,38 +3043,6 @@ async def to_code(config): if advanced[CONF_DISABLE_DEV_NULL_VFS]: add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False) - # Disable keeping peer certificate after TLS handshake - # Saves ~4KB heap per connection, but prevents certificate inspection after handshake - # Components that need it can call require_mbedtls_peer_cert() - if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False): - add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True) - elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]: - add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False) - - # Disable PKCS#7 support in mbedTLS - # Only needed for specific certificate validation scenarios - # Components that need it can call require_mbedtls_pkcs7() - if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False): - # Component called require_mbedtls_pkcs7() - enable regardless of user setting - add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True) - elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]: - add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False) - - # Disable SHA-384 and SHA-512 in mbedTLS - # ESPHome doesn't use either algorithm. SHA-384 shares the same - # compression function as SHA-512 (mbedtls_internal_sha512_process), - # so both must be disabled to eliminate the ~3KB software fallback - # that IDF 6.0's PSA parallel engine always links in. - # On IDF < 6.0 these are a single config and hardware-only (no - # software fallback), so there was no code size cost to leaving - # them enabled. - # Components that need SHA-384/SHA-512 can call require_mbedtls_sha512() - if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get( - KEY_MBEDTLS_SHA512_REQUIRED, False - ): - add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) - add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) - # FINAL priority: runs after every require_libc_picolibc_newlib_compat() call CORE.add_job(_set_libc_picolibc_newlib_compat) @@ -2990,6 +3052,12 @@ async def to_code(config): # FINAL priority: runs after every require_certificate_bundle() call CORE.add_job(_reconcile_certificate_bundle_sdkconfig) + # FINAL priority: runs after every request_tls() / require_mbedtls_*() call + mbedtls = _mbedtls_sdkconfig() + mbedtls.disable_peer_cert = advanced[CONF_DISABLE_MBEDTLS_PEER_CERT] + mbedtls.disable_pkcs7 = advanced[CONF_DISABLE_MBEDTLS_PKCS7] + CORE.add_job(_reconcile_mbedtls_sdkconfig) + # FINAL: require_*() calls can come from to_code at or below this priority, so an # inline read would be iteration-order-dependent; reconcile once after every job ran. CORE.add_job( @@ -3021,6 +3089,12 @@ 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] + ): + request_tls() # Components from YAML are added in a separate coroutine with FINAL priority # Schedule it to run after all other components diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index e7d8a66e7a..087df4d605 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -30,6 +30,7 @@ KEY_EXTRA_BUILD_FILES = "extra_build_files" KEY_CERT_BUNDLE = "cert_bundle" KEY_FULL_CERT_BUNDLE = "full_cert_bundle" KEY_NETWORK_SDKCONFIG = "network_sdkconfig" +KEY_MBEDTLS_SDKCONFIG = "mbedtls_sdkconfig" VARIANT_ESP32 = "ESP32" VARIANT_ESP32C2 = "ESP32C2" diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index de35d52a40..5faf989686 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -202,11 +202,7 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: - # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time). - # esp-tls is re-enabled too because http_request includes - # directly and esp_http_client only pulls it in as a private dependency. - esp32.include_builtin_idf_component("esp_http_client") - esp32.include_builtin_idf_component("esp-tls") + esp32.request_http_client() cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 3050ceb1a4..5302554361 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -7,6 +7,7 @@ from esphome.components.esp32 import ( add_idf_sdkconfig_option, idf_version, include_builtin_idf_component, + request_tls, ) from esphome.config_helpers import ( filter_source_files_from_defines, @@ -364,8 +365,8 @@ async def to_code(config): add_idf_component(name="espressif/mqtt", ref="1.0.0") else: include_builtin_idf_component("mqtt") - # mqtt_client.h drags in esp_tls types; esp-tls is excluded by default - include_builtin_idf_component("esp-tls") + # esp-mqtt links transport_ssl.c (esp_tls) even for plain MQTT + request_tls() cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 3f5ba94b40..63f3d51cf0 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -289,10 +289,7 @@ async def to_code(config): ) if CORE.is_esp32: - # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) - # and esp-tls, whose sdkconfig options below need the component present - esp32.include_builtin_idf_component("esp_http_client") - esp32.include_builtin_idf_component("esp-tls") + esp32.request_http_client() esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index ab69f5d9ae..15a871f761 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32 import ( get_esp32_variant, include_builtin_idf_component, only_on_variant, + require_mbedtls_ecp, require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage @@ -282,6 +283,8 @@ async def to_code(config: ConfigType) -> None: # Re-enable openthread IDF component (excluded by default) if CORE.is_esp32: include_builtin_idf_component("openthread") + # OPENTHREAD_CONFIG_ECDSA_ENABLE: the SRP client host key uses mbedtls_ecdsa_* + require_mbedtls_ecp() cg.add_define("USE_OPENTHREAD") if config.get(CONF_FORCE_DATASET): diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 5a400dfbf3..2d1c72faf4 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -17,9 +17,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config: ConfigType) -> None: # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) - # Re-enable esp-tls (excluded by default to save compile time); - # web_server_idf.cpp includes for digest auth - include_builtin_idf_component("esp-tls") + # Re-enable ESP-IDF's HTTP server (excluded by default to save compile time). + # Basic auth uses mbedtls_base64_encode directly, so no TLS stack is needed. include_builtin_idf_component("esp_http_server") diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 9550570cdc..6d14611149 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -9,7 +9,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esp_tls_crypto.h" +#include #include #include @@ -544,14 +544,14 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw constexpr size_t max_digest_len = 350; char digest[max_digest_len]; size_t out; - esp_crypto_base64_encode(reinterpret_cast(digest), max_digest_len, &out, - reinterpret_cast(user_info), user_info_len); + mbedtls_base64_encode(reinterpret_cast(digest), max_digest_len, &out, + reinterpret_cast(user_info), user_info_len); // Constant-time comparison to avoid timing side channels. // No early return on length mismatch — the length difference is folded // into the accumulator so any mismatch is rejected. const char *provided = auth_str + auth_prefix_len; - size_t digest_len = out; // length from esp_crypto_base64_encode + size_t digest_len = out; // Derive provided_len from the already-sized std::string rather than // rescanning with strlen (avoids attacker-controlled scan length). size_t provided_len = auth.value().size() - auth_prefix_len; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b8c6d774ac..20197e398f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -11,6 +11,7 @@ from esphome.components.esp32 import ( const, get_esp32_variant, only_on_variant, + request_tls, request_wifi, ) from esphome.components.network import ( @@ -656,6 +657,9 @@ async def to_code(config): # Disable Enterprise WiFi support if no EAP is configured if CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap) + # The supplicant's Kconfig select cannot override the IDF 5 TLS role choice + if has_eap: + request_tls() # Only define USE_WIFI_MANUAL_IP if any AP uses manual IP if has_manual_ip: diff --git a/tests/component_tests/esp32/config/tls_openthread_c6.yaml b/tests/component_tests/esp32/config/tls_openthread_c6.yaml new file mode 100644 index 0000000000..f4204eb9ea --- /dev/null +++ b/tests/component_tests/esp32/config/tls_openthread_c6.yaml @@ -0,0 +1,19 @@ +esphome: + name: test + +esp32: + board: esp32-c6-devkitc-1 + framework: + type: esp-idf + +network: + enable_ipv6: true + +openthread: + channel: 13 + network_name: OpenThread-8f28 + network_key: 0xdfd34f0f05cad978ec4e32b0413038ff + pan_id: 0x8f28 + ext_pan_id: 0xd63e8e3e495ebbc3 + pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 + mesh_local_prefix: fd53:145f:ed22:ad81::/64 diff --git a/tests/component_tests/esp32/config/tls_sdkconfig_esp_tls.yaml b/tests/component_tests/esp32/config/tls_sdkconfig_esp_tls.yaml new file mode 100644 index 0000000000..888a62b849 --- /dev/null +++ b/tests/component_tests/esp32/config/tls_sdkconfig_esp_tls.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + sdkconfig_options: + CONFIG_ESP_TLS_INSECURE: y diff --git a/tests/component_tests/esp32/config/tls_sdkconfig_tls_role.yaml b/tests/component_tests/esp32/config/tls_sdkconfig_tls_role.yaml new file mode 100644 index 0000000000..1713da494f --- /dev/null +++ b/tests/component_tests/esp32/config/tls_sdkconfig_tls_role.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + sdkconfig_options: + CONFIG_MBEDTLS_TLS_CLIENT_ONLY: y diff --git a/tests/component_tests/esp32/config/tls_wifi_eap.yaml b/tests/component_tests/esp32/config/tls_wifi_eap.yaml new file mode 100644 index 0000000000..4c7619b540 --- /dev/null +++ b/tests/component_tests/esp32/config/tls_wifi_eap.yaml @@ -0,0 +1,15 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + networks: + - ssid: "test_ssid" + eap: + username: username + password: password + identity: identity diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index c72c4c3a6b..6803f2ac19 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -16,15 +16,19 @@ from esphome.components.esp32 import ( KEY_VFS_TERMIOS_REQUIRED, VARIANT_ESP32, VARIANTS, + MbedtlsSdkconfigData, NetworkSdkconfigData, RawSdkconfigValue, _ota_downgrade_protection_errors, + _reconcile_mbedtls_sdkconfig, _reconcile_network_sdkconfig, _reconcile_vfs_fatfs_sdkconfig, ) from esphome.components.esp32.const import ( KEY_ESP32, KEY_EXCLUDE_COMPONENTS, + KEY_IDF_VERSION, + KEY_MBEDTLS_SDKCONFIG, KEY_NETWORK_SDKCONFIG, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, @@ -260,8 +264,8 @@ def test_esp32_configuration_errors( ("esp_driver_i2c", "esp_driver_ledc", "esp_driver_gptimer"), id="i2c_ledc_ac_dimmer", ), - # esp-tls has three owners; a per-owner config makes a dropped - # re-include from any single one fail the test. + # esp-tls comes back through request_tls(); a per-owner config makes + # a dropped request from any single one fail the test. pytest.param( "exclusion_reincludes_http_request.yaml", ("esp-tls", "esp_http_client"), @@ -275,8 +279,9 @@ def test_esp32_configuration_errors( id="mqtt", ), pytest.param( + # Basic auth uses mbedtls_base64_encode directly, so no esp-tls. "exclusion_reincludes_web_server.yaml", - ("esp-tls", "esp_http_server"), + ("esp_http_server",), id="web_server_idf", ), pytest.param( @@ -403,6 +408,186 @@ def test_user_sdkconfig_certificate_bundle_wins( assert sdkconfig.get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL") is False +_TLS_OFF_CRYPTO = { + "CONFIG_MBEDTLS_ECP_C": False, + "CONFIG_MBEDTLS_PEM_WRITE_C": False, + "CONFIG_MBEDTLS_X509_CRL_PARSE_C": False, + "CONFIG_MBEDTLS_X509_CSR_PARSE_C": False, +} +_TLS_OFF_IDF5 = { + "CONFIG_MBEDTLS_TLS_DISABLED": True, + "CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT": False, + **_TLS_OFF_CRYPTO, +} +_TLS_OFF_IDF6 = { + "CONFIG_MBEDTLS_TLS_ENABLED": False, + "CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT": False, + **_TLS_OFF_CRYPTO, +} +_PEER_CERT_PKCS7_OFF = { + "CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE": False, + "CONFIG_MBEDTLS_PKCS7_C": False, +} +_IDF5 = cv.Version(5, 5, 5) +_IDF6 = cv.Version(6, 0, 0) + + +@pytest.mark.parametrize( + ("framework", "idf", "data", "preset", "expected"), + [ + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(), + {}, + {**_TLS_OFF_IDF5, **_PEER_CERT_PKCS7_OFF}, + id="idf5_no_tls_user", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF6, + MbedtlsSdkconfigData(), + {}, + { + **_TLS_OFF_IDF6, + **_PEER_CERT_PKCS7_OFF, + "CONFIG_MBEDTLS_SHA384_C": False, + "CONFIG_MBEDTLS_SHA512_C": False, + }, + id="idf6_drops_sha512", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF6, + MbedtlsSdkconfigData(sha512_required=True), + {}, + {**_TLS_OFF_IDF6, **_PEER_CERT_PKCS7_OFF}, + id="idf6_sha512_required", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(tls_required=True), + {}, + _PEER_CERT_PKCS7_OFF, + id="idf_tls_requested", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(ecp_required=True), + {}, + { + "CONFIG_MBEDTLS_TLS_DISABLED": True, + "CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT": False, + "CONFIG_MBEDTLS_PEM_WRITE_C": False, + "CONFIG_MBEDTLS_X509_CRL_PARSE_C": False, + "CONFIG_MBEDTLS_X509_CSR_PARSE_C": False, + **_PEER_CERT_PKCS7_OFF, + }, + id="idf_ecp_without_tls", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(), + {"CONFIG_MBEDTLS_ECP_C": RawSdkconfigValue("y")}, + { + **_TLS_OFF_IDF5, + "CONFIG_MBEDTLS_ECP_C": RawSdkconfigValue("y"), + **_PEER_CERT_PKCS7_OFF, + }, + id="idf_user_ecp_wins", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(peer_cert_required=True, pkcs7_required=True), + {}, + { + **_TLS_OFF_IDF5, + "CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE": True, + "CONFIG_MBEDTLS_PKCS7_C": True, + }, + id="idf_peer_cert_pkcs7_required", + ), + pytest.param( + PlatformFramework.ESP32_IDF, + _IDF5, + MbedtlsSdkconfigData(disable_peer_cert=False, disable_pkcs7=False), + {}, + _TLS_OFF_IDF5, + id="idf_advanced_disables_off", + ), + pytest.param( + PlatformFramework.ESP32_ARDUINO, + _IDF5, + MbedtlsSdkconfigData(), + {}, + _PEER_CERT_PKCS7_OFF, + id="arduino_keeps_tls", + ), + ], +) +def test_reconcile_mbedtls_sdkconfig( + set_core_config: SetCoreConfigCallable, + framework: PlatformFramework, + idf: cv.Version, + data: MbedtlsSdkconfigData, + preset: dict[str, Any], + expected: dict[str, Any], +) -> None: + """The FINAL-priority reconciler turns TLS off only when nothing requested it; + user sdkconfig_options always win.""" + set_core_config(framework) + CORE.data[KEY_ESP32] = { + KEY_IDF_VERSION: idf, + KEY_SDKCONFIG_OPTIONS: dict(preset), + KEY_MBEDTLS_SDKCONFIG: data, + } + + asyncio.run(_reconcile_mbedtls_sdkconfig()) + + assert CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] == expected + + +@pytest.mark.parametrize( + ("config_file", "tls_off", "ecp_off"), + [ + pytest.param("network_ethernet_only.yaml", True, True, id="ethernet_api"), + pytest.param( + "exclusion_reincludes_web_server.yaml", True, True, id="web_server_idf" + ), + # openthread's SRP host key needs ECDSA, so ECP stays while TLS is off + pytest.param("tls_openthread_c6.yaml", True, False, id="openthread"), + pytest.param( + "exclusion_reincludes_http_request.yaml", False, False, id="http_request" + ), + pytest.param("exclusion_reincludes_mqtt.yaml", False, False, id="mqtt"), + pytest.param("exclusion_reincludes_nextion.yaml", False, False, id="nextion"), + pytest.param("tls_wifi_eap.yaml", False, False, id="wifi_eap"), + pytest.param( + "certificate_bundle_sdkconfig.yaml", False, False, id="raw_bundle" + ), + 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"), + ], +) +def test_tls_disabled_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + tls_off: bool, + ecp_off: bool, +) -> None: + """TLS is compiled out unless a component or a raw sdkconfig option asks for it.""" + generate_main(component_config_path(config_file)) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert ("CONFIG_MBEDTLS_TLS_DISABLED" in sdkconfig) is tls_off + assert ("CONFIG_MBEDTLS_ECP_C" in sdkconfig) is ecp_off + assert ("esp-tls" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]) is tls_off + + def test_execute_from_psram_s3_sdkconfig( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path],