diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5879729a22b..6e75c57d1b2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -259,6 +259,22 @@ WiFiConfigureAction = wifi_ns.class_( ) +# The supplicant parses the bootstrapping key as a DER encoded P-256 private key in hex; these wrap the +# 32 byte key the way the ESP-IDF enrollee example does +DPP_KEY_DER_PREFIX = "30310201010420" +DPP_KEY_DER_SUFFIX = "a00a06082a8648ce3d030107" + + +def _validate_dpp_private_key(value: object) -> str: + """The key is 32 bytes written as 64 hexadecimal characters.""" + value = cv.string_strict(value) + if len(value) != 64 or any(c not in "0123456789abcdefABCDEF" for c in value): + raise cv.Invalid( + "private_key must be 32 bytes written as 64 hexadecimal characters" + ) + return value.lower() + + def _validate_dpp_device_info(value: object) -> str: """The supplicant puts the text on a space separated command line and reads it up to the next space.""" value = cv.string_strict(value) @@ -336,7 +352,7 @@ EAP_AUTH_SCHEMA = cv.All( # Wi-Fi Easy Connect (DPP) enrollee; the ESP-IDF supplicant supports the QR code method DPP_SCHEMA = cv.Schema( { - cv.Optional(CONF_PRIVATE_KEY): cv.sensitive(cv.string_strict), + cv.Optional(CONF_PRIVATE_KEY): cv.sensitive(_validate_dpp_private_key), cv.Optional(CONF_DEVICE_INFO): _validate_dpp_device_info, cv.Optional(CONF_CHANNELS, default=[6]): cv.ensure_list( cv.int_range(min=1, max=14) @@ -853,7 +869,7 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DPP_SUPPORT", True) add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_DPP", True) if (key := dpp.get(CONF_PRIVATE_KEY)) is not None: - cg.add(var.set_dpp_key(key)) + cg.add(var.set_dpp_key(f"{DPP_KEY_DER_PREFIX}{key}{DPP_KEY_DER_SUFFIX}")) if (info := dpp.get(CONF_DEVICE_INFO)) is not None: cg.add(var.set_dpp_device_info(info)) cg.add(var.set_dpp_channels(",".join(str(c) for c in dpp[CONF_CHANNELS]))) diff --git a/tests/component_tests/wifi/config/dpp_key.yaml b/tests/component_tests/wifi/config/dpp_key.yaml new file mode 100644 index 00000000000..ba25f5e911e --- /dev/null +++ b/tests/component_tests/wifi/config/dpp_key.yaml @@ -0,0 +1,12 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + dpp: + private_key: 00112233445566778899AABBCCDDEEFF00112233445566778899aabbccddeeff diff --git a/tests/component_tests/wifi/test_dpp_key.py b/tests/component_tests/wifi/test_dpp_key.py new file mode 100644 index 00000000000..96697dbb865 --- /dev/null +++ b/tests/component_tests/wifi/test_dpp_key.py @@ -0,0 +1,42 @@ +"""The DPP bootstrapping key is given as 32 raw bytes in hex and handed to the supplicant DER encoded.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components import wifi +import esphome.config_validation as cv + +RAW_KEY = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff" + + +@pytest.mark.parametrize( + "value", + [ + RAW_KEY[:-2], + RAW_KEY + "00", + "zz" + RAW_KEY[2:], + wifi.DPP_KEY_DER_PREFIX + RAW_KEY + wifi.DPP_KEY_DER_SUFFIX, + ], +) +def test_key_must_be_64_hex_digits(value: str) -> None: + with pytest.raises(cv.Invalid, match="64 hexadecimal characters"): + wifi._validate_dpp_private_key(value) + + +def test_key_is_lower_cased() -> None: + assert wifi._validate_dpp_private_key(RAW_KEY.upper()) == RAW_KEY + + +def test_generated_key_is_der_encoded( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The ESP-IDF supplicant expects the P-256 DER header around the raw key.""" + main_cpp = generate_main(component_config_path("dpp_key.yaml")) + + assert ( + f'set_dpp_key("{wifi.DPP_KEY_DER_PREFIX}{RAW_KEY}{wifi.DPP_KEY_DER_SUFFIX}")' + in main_cpp + ) diff --git a/tests/components/wifi/test-dpp.esp32-idf.yaml b/tests/components/wifi/test-dpp.esp32-idf.yaml index 29ec40ba797..1bd0d64fb5e 100644 --- a/tests/components/wifi/test-dpp.esp32-idf.yaml +++ b/tests/components/wifi/test-dpp.esp32-idf.yaml @@ -2,6 +2,7 @@ wifi: dpp: channels: [1, 6, 11] device_info: ESPHome + private_key: 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff timeout: 30s on_dpp_uri: - logger.log: