mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
[esp32] Trim mbedTLS to client-only defaults and stub vasprintf on the C6
Linking an HTTPS client (http_request, mqtt) pulls in TLS features no ESPHome client ever negotiates: the server-side handshake, static RSA and static ECDH key exchange, renegotiation, session tickets, AES-CCM suites, the EC private-key parsing extras and deterministic ECDSA. Two new esp32 advanced options, on by default, turn them off and save around 14 KB per build; OpenThread opts back in through require hooks because its DTLS commissioner is a server and it uses CCM and deterministic ECDSA directly. On the ESP32-C6 the ROM exports a full-format vsnprintf but no vasprintf, so esp_http_client's vasprintf call was the only thing dragging newlib's printf engine (~20 KB) into the image. A linker-wrapped vasprintf built on the ROM vsnprintf keeps it out.
This commit is contained in:
@@ -193,6 +193,13 @@ PSRAM_XIP_VARIANTS = {
|
||||
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
|
||||
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
|
||||
# should be added here.
|
||||
# Variants whose ROM exports a full-format vsnprintf but no vasprintf
|
||||
# (esp32c6.rom.newlib-normal.ld). There, the newlib printf engine is only
|
||||
# linked because esp_http_client calls vasprintf; see vasprintf_stubs.cpp.
|
||||
# The other variants either export both (classic ESP32, nano-format only) or
|
||||
# neither, so the engine is already in the image and the wrap saves nothing.
|
||||
ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6}
|
||||
|
||||
NVS_ENCRYPTION_HMAC_VARIANTS = {
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
@@ -1732,6 +1739,8 @@ CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
|
||||
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
|
||||
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||
CONF_DISABLE_MBEDTLS_TLS_SERVER = "disable_mbedtls_tls_server"
|
||||
CONF_DISABLE_MBEDTLS_TLS_EXTRAS = "disable_mbedtls_tls_extras"
|
||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||
@@ -1746,6 +1755,8 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
|
||||
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_MBEDTLS_TLS_SERVER_REQUIRED = "mbedtls_tls_server_required"
|
||||
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED = "mbedtls_tls_extras_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
@@ -1830,6 +1841,26 @@ def require_mbedtls_pkcs7() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_tls_server() -> None:
|
||||
"""Mark that the mbedTLS server-side TLS/DTLS handshake is required.
|
||||
|
||||
Call this from components that accept TLS connections (OpenThread's DTLS
|
||||
commissioner does). This prevents CONFIG_MBEDTLS_TLS_CLIENT_ONLY from
|
||||
being selected.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_tls_extras() -> None:
|
||||
"""Mark that the TLS features disabled by ``disable_mbedtls_tls_extras`` are required.
|
||||
|
||||
Call this from components that need AES-CCM, deterministic ECDSA signing,
|
||||
static RSA/ECDH key exchange, TLS renegotiation or session tickets.
|
||||
OpenThread uses CCM and deterministic ECDSA directly.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_sha512() -> None:
|
||||
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
||||
|
||||
@@ -1987,6 +2018,8 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_SERVER, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_EXTRAS, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
@@ -2302,6 +2335,47 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
|
||||
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||
|
||||
|
||||
# TLS features an HTTPS/MQTT client talking to a modern server never
|
||||
# negotiates. Static RSA and static ECDH key exchange have no forward secrecy
|
||||
# and are gone in TLS 1.3, renegotiation is deprecated, esp-tls never enables
|
||||
# session tickets, AES-CCM ciphersuites are not offered by web servers, and
|
||||
# the EC key parsing extras and deterministic ECDSA only matter when signing
|
||||
# with a private key. Together they cost ~11 KB of flash whenever TLS is
|
||||
# linked (http_request, mqtt).
|
||||
MBEDTLS_TLS_EXTRA_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_KEY_EXCHANGE_RSA",
|
||||
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA",
|
||||
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA",
|
||||
"CONFIG_MBEDTLS_SSL_RENEGOTIATION",
|
||||
"CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS",
|
||||
"CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS",
|
||||
"CONFIG_MBEDTLS_CCM_C",
|
||||
"CONFIG_MBEDTLS_PK_PARSE_EC_EXTENDED",
|
||||
"CONFIG_MBEDTLS_PK_PARSE_EC_COMPRESSED",
|
||||
"CONFIG_MBEDTLS_ECDSA_DETERMINISTIC",
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_mbedtls_tls_sdkconfig(
|
||||
disable_tls_server: bool, disable_tls_extras: bool
|
||||
) -> None:
|
||||
"""Trim mbedTLS to what a TLS client needs unless a component asked otherwise.
|
||||
|
||||
Runs at FINAL priority so every require_mbedtls_tls_server() and
|
||||
require_mbedtls_tls_extras() call has happened. Only the server-side
|
||||
handshake (~7 KB) is a separate option; nothing in ESPHome accepts TLS
|
||||
connections, but OpenThread's DTLS commissioner does.
|
||||
"""
|
||||
data = CORE.data[KEY_ESP32]
|
||||
if disable_tls_server and not data.get(KEY_MBEDTLS_TLS_SERVER_REQUIRED, False):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_CLIENT_ONLY", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", False)
|
||||
if disable_tls_extras and not data.get(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, False):
|
||||
for option in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||
add_idf_sdkconfig_option(option, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_network_sdkconfig() -> None:
|
||||
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
|
||||
@@ -2566,6 +2640,17 @@ async def to_code(config):
|
||||
else:
|
||||
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
|
||||
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
||||
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
|
||||
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
|
||||
# _dtoa_r and their helpers); every other caller resolves to the
|
||||
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
|
||||
# because libsrc.a is scanned before the IDF libraries that
|
||||
# reference the symbol, so the stub would otherwise never be pulled
|
||||
# from the archive.
|
||||
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
|
||||
cg.add_define("USE_ESP32_VASPRINTF_STUB")
|
||||
cg.add_build_flag("-Wl,--wrap=vasprintf")
|
||||
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
|
||||
else:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||
@@ -2991,6 +3076,13 @@ 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 require_mbedtls_tls_*() call
|
||||
CORE.add_job(
|
||||
_reconcile_mbedtls_tls_sdkconfig,
|
||||
advanced[CONF_DISABLE_MBEDTLS_TLS_SERVER],
|
||||
advanced[CONF_DISABLE_MBEDTLS_TLS_EXTRAS],
|
||||
)
|
||||
|
||||
# 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(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Linker wrap stub for vasprintf() on variants whose ROM exports a
|
||||
* full-format vsnprintf() but no vasprintf() (ESP32-C6, newlib only).
|
||||
*
|
||||
* On those chips every snprintf/vsnprintf call in the image resolves to
|
||||
* the ROM, so the newlib printf engine (_svfprintf_r, _dtoa_r and their
|
||||
* helpers, ~20 KB) is not linked at all until something references a
|
||||
* printf-family function the ROM lacks. esp_http_client does exactly that
|
||||
* through vasprintf() in its header and auth helpers, so adding
|
||||
* http_request to a build costs the whole engine on top of the HTTP and
|
||||
* TLS code itself.
|
||||
*
|
||||
* This stub reimplements vasprintf() on top of the ROM vsnprintf(), which
|
||||
* keeps the engine out of the image. It is only compiled in when codegen
|
||||
* defines USE_ESP32_VASPRINTF_STUB, which is gated on the variant's ROM
|
||||
* linker script and on the same newlib condition as printf_stubs.cpp.
|
||||
*/
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_ESP32_VASPRINTF_STUB)
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vasprintf(char **strp, const char *fmt, va_list ap) {
|
||||
va_list ap_copy;
|
||||
va_copy(ap_copy, ap);
|
||||
int len = vsnprintf(nullptr, 0, fmt, ap_copy);
|
||||
va_end(ap_copy);
|
||||
if (len < 0) {
|
||||
return len;
|
||||
}
|
||||
char *buf = static_cast<char *>(malloc(static_cast<size_t>(len) + 1));
|
||||
if (buf == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
vsnprintf(buf, static_cast<size_t>(len) + 1, fmt, ap);
|
||||
*strp = buf;
|
||||
return len;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_ESP_IDF && USE_ESP32_VASPRINTF_STUB
|
||||
@@ -13,6 +13,8 @@ from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
only_on_variant,
|
||||
require_mbedtls_tls_extras,
|
||||
require_mbedtls_tls_server,
|
||||
require_vfs_select,
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
@@ -109,6 +111,12 @@ def set_sdkconfig_options(config: ConfigType) -> None:
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
|
||||
|
||||
# OpenThread's DTLS commissioner is a TLS server, and its crypto platform
|
||||
# uses AES-CCM and deterministic ECDSA directly. Keep the esp32 component
|
||||
# from trimming them out of mbedTLS.
|
||||
require_mbedtls_tls_server()
|
||||
require_mbedtls_tls_extras()
|
||||
|
||||
if not config.get(CONF_TLV):
|
||||
if pan_id := config.get(CONF_PAN_ID):
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id)
|
||||
|
||||
@@ -298,6 +298,7 @@
|
||||
// ESP32-specific feature flags
|
||||
#ifdef USE_ESP32
|
||||
#define USE_ESP32_CRASH_HANDLER
|
||||
#define USE_ESP32_VASPRINTF_STUB
|
||||
#define USE_ESP32_INTERNAL_GPIO
|
||||
#define USE_MQTT_IDF_ENQUEUE
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
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
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
disable_mbedtls_tls_server: false
|
||||
disable_mbedtls_tls_extras: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_full_printf: true
|
||||
@@ -11,9 +11,12 @@ import pytest
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
KEY_FATFS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_SERVER_REQUIRED,
|
||||
KEY_VFS_DIR_REQUIRED,
|
||||
KEY_VFS_SELECT_REQUIRED,
|
||||
KEY_VFS_TERMIOS_REQUIRED,
|
||||
MBEDTLS_TLS_EXTRA_OPTIONS,
|
||||
VARIANT_ESP32,
|
||||
VARIANTS,
|
||||
NetworkSdkconfigData,
|
||||
@@ -1339,3 +1342,67 @@ def test_esp32_s31_gpio_validation(
|
||||
with caplog.at_level("WARNING"):
|
||||
validate_supports(pin)
|
||||
assert "GPIO36 is a strapping PIN" in caplog.text
|
||||
|
||||
|
||||
_TLS_SERVER_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "server", "extras"),
|
||||
[
|
||||
pytest.param("mbedtls_tls_default.yaml", (True, False), False, id="default"),
|
||||
pytest.param("mbedtls_tls_opt_out.yaml", (None, None), None, id="opt_out"),
|
||||
pytest.param(
|
||||
"mbedtls_tls_openthread.yaml", (None, None), None, id="openthread"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_mbedtls_tls_trim_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
server: tuple[bool | None, bool | None],
|
||||
extras: bool | None,
|
||||
) -> None:
|
||||
"""Client-only TLS and the unused-feature trims apply unless opted out or required."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == server
|
||||
assert {sdkconfig.get(name) for name in MBEDTLS_TLS_EXTRA_OPTIONS} == {extras}
|
||||
|
||||
|
||||
def test_mbedtls_tls_openthread_requires_server_and_extras(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The OpenThread hooks mark the DTLS server and CCM/deterministic ECDSA as required."""
|
||||
generate_main(component_config_path("mbedtls_tls_openthread.yaml"))
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] is True
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] is True
|
||||
|
||||
|
||||
_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("vasprintf_stub_c6.yaml", True, id="c6"),
|
||||
pytest.param("vasprintf_stub_c6_full_printf.yaml", False, id="c6_full_printf"),
|
||||
pytest.param("exclusion_reincludes.yaml", False, id="esp32"),
|
||||
],
|
||||
)
|
||||
def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""The vasprintf wrap is emitted only where the ROM lacks vasprintf but has vsnprintf."""
|
||||
generate_main(component_config_path(config_file))
|
||||
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
|
||||
|
||||
@@ -17,6 +17,8 @@ esp32:
|
||||
disable_dev_null_vfs: true
|
||||
disable_mbedtls_peer_cert: true
|
||||
disable_mbedtls_pkcs7: true
|
||||
disable_mbedtls_tls_server: true
|
||||
disable_mbedtls_tls_extras: true
|
||||
disable_regi2c_in_iram: true
|
||||
disable_fatfs: true
|
||||
sram1_as_iram: true
|
||||
|
||||
Reference in New Issue
Block a user