mirror of
https://github.com/esphome/esphome.git
synced 2026-05-28 04:55:48 +08:00
[esp32] Add Secure Boot V1 ECDSA signing scheme for pre-rev-3.0 ESP32 (#15882)
This commit is contained in:
@@ -128,23 +128,30 @@ ASSERTION_LEVELS = {
|
|||||||
SIGNING_SCHEMES = {
|
SIGNING_SCHEMES = {
|
||||||
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
|
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
|
||||||
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
|
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
|
||||||
|
"ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Chip variants that only support one signing scheme for Secure Boot V2.
|
# Chip variants that only support one V2 signing scheme.
|
||||||
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
|
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
|
||||||
# Variants not listed in either set support both RSA and ECDSA
|
# Variants not listed in either set support both RSA and ECDSA V2
|
||||||
# (e.g. C5, C6, H2, P4). New variants should be added to the
|
# (e.g. C5, C6, H2, P4). New variants should be added to the
|
||||||
# appropriate set if they only support one scheme.
|
# appropriate set if they only support one scheme.
|
||||||
SIGNED_OTA_RSA_ONLY_VARIANTS = {
|
# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only
|
||||||
VARIANT_ESP32,
|
# when minimum_chip_revision >= 3.0, which requires special handling.
|
||||||
|
SIGNED_OTA_V2_RSA_ONLY_VARIANTS = {
|
||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
VARIANT_ESP32C3,
|
VARIANT_ESP32C3,
|
||||||
}
|
}
|
||||||
SIGNED_OTA_ECC_ONLY_VARIANTS = {
|
SIGNED_OTA_V2_ECC_ONLY_VARIANTS = {
|
||||||
VARIANT_ESP32C2,
|
VARIANT_ESP32C2,
|
||||||
VARIANT_ESP32C61,
|
VARIANT_ESP32C61,
|
||||||
}
|
}
|
||||||
|
# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32.
|
||||||
|
# Based on SOC_SECURE_BOOT_V1 in soc_caps.h.
|
||||||
|
SIGNED_OTA_V1_ECDSA_VARIANTS = {
|
||||||
|
VARIANT_ESP32,
|
||||||
|
}
|
||||||
|
|
||||||
COMPILER_OPTIMIZATIONS = {
|
COMPILER_OPTIMIZATIONS = {
|
||||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||||
@@ -991,25 +998,73 @@ def final_validate(config):
|
|||||||
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
||||||
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
||||||
variant = config[CONF_VARIANT]
|
variant = config[CONF_VARIANT]
|
||||||
scheme_variant_conflicts = {
|
min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION)
|
||||||
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
|
scheme_path = [
|
||||||
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
|
CONF_FRAMEWORK,
|
||||||
}
|
CONF_ADVANCED,
|
||||||
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
|
CONF_SIGNED_OTA_VERIFICATION,
|
||||||
0
|
CONF_SIGNING_SCHEME,
|
||||||
]:
|
]
|
||||||
|
|
||||||
|
# V1 ECDSA is only available on the original ESP32
|
||||||
|
if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS:
|
||||||
errs.append(
|
errs.append(
|
||||||
cv.Invalid(
|
cv.Invalid(
|
||||||
f"Signing scheme '{scheme}' is not supported on "
|
f"Signing scheme 'ecdsa_v1' is only supported on "
|
||||||
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
|
f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. "
|
||||||
path=[
|
f"Use 'rsa3072' or 'ecdsa256' instead.",
|
||||||
CONF_FRAMEWORK,
|
path=scheme_path,
|
||||||
CONF_ADVANCED,
|
|
||||||
CONF_SIGNED_OTA_VERIFICATION,
|
|
||||||
CONF_SIGNING_SCHEME,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif variant == VARIANT_ESP32:
|
||||||
|
# On ESP32, V2 RSA requires minimum_chip_revision >= 3.0
|
||||||
|
# Note: string comparison works here because cv.one_of constrains
|
||||||
|
# min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1").
|
||||||
|
if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"):
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} "
|
||||||
|
f"requires minimum_chip_revision: '3.0' or higher "
|
||||||
|
f"(Secure Boot V2 RSA needs chip revision 3.0+). "
|
||||||
|
f"For older chip revisions, use 'ecdsa_v1' instead.",
|
||||||
|
path=scheme_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC)
|
||||||
|
elif scheme == "ecdsa256":
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"Signing scheme 'ecdsa256' is not supported on "
|
||||||
|
f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with "
|
||||||
|
f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.",
|
||||||
|
path=scheme_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# V1 on rev 3.0+ -- suggest V2 RSA for stronger security
|
||||||
|
elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0":
|
||||||
|
_LOGGER.info(
|
||||||
|
"Using Secure Boot V1 ECDSA on %s rev %s. "
|
||||||
|
"Consider using 'rsa3072' (Secure Boot V2 RSA) for "
|
||||||
|
"stronger security on chip revision 3.0+.",
|
||||||
|
VARIANT_FRIENDLY[variant],
|
||||||
|
min_rev,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Non-ESP32 variants: check V2 scheme-variant compatibility
|
||||||
|
scheme_variant_conflicts = {
|
||||||
|
"ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"),
|
||||||
|
"rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"),
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
conflict := scheme_variant_conflicts.get(scheme)
|
||||||
|
) and variant in conflict[0]:
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"Signing scheme '{scheme}' is not supported on "
|
||||||
|
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
|
||||||
|
path=scheme_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
if CONF_OTA not in full_config:
|
if CONF_OTA not in full_config:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Signed OTA verification is enabled but no OTA component is configured. "
|
"Signed OTA verification is enabled but no OTA component is configured. "
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import json # noqa: E402
|
|||||||
import os # noqa: E402
|
import os # noqa: E402
|
||||||
import pathlib # noqa: E402
|
import pathlib # noqa: E402
|
||||||
import shutil # noqa: E402
|
import shutil # noqa: E402
|
||||||
|
import subprocess # noqa: E402
|
||||||
from glob import glob # noqa: E402
|
from glob import glob # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +26,114 @@ def _parse_sdkconfig(sdkconfig_path):
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_v1_verification_key(env):
|
||||||
|
"""Generate the V1 ECDSA verification key binary and assembly source file.
|
||||||
|
|
||||||
|
Secure Boot V1 embeds the public verification key directly in the app binary
|
||||||
|
as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates
|
||||||
|
these files via custom commands, but PlatformIO's SCons bridge does not execute
|
||||||
|
them. This function replicates that logic:
|
||||||
|
1. Extracts the raw public key from the PEM signing key using espsecure.
|
||||||
|
2. Generates the .S assembly source that embeds the key bytes.
|
||||||
|
"""
|
||||||
|
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
|
||||||
|
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
|
||||||
|
pioenv = env.subst("$PIOENV")
|
||||||
|
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
|
||||||
|
|
||||||
|
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y":
|
||||||
|
return
|
||||||
|
|
||||||
|
bin_path = build_dir / "signature_verification_key.bin"
|
||||||
|
asm_path = build_dir / "signature_verification_key.bin.S"
|
||||||
|
|
||||||
|
# Determine the source of the verification key
|
||||||
|
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y":
|
||||||
|
# Extract public key from the signing key
|
||||||
|
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
|
||||||
|
if not signing_key:
|
||||||
|
return
|
||||||
|
signing_key_path = pathlib.Path(signing_key)
|
||||||
|
if not signing_key_path.exists():
|
||||||
|
print(f"Error: V1 ECDSA signing key not found: {signing_key_path}")
|
||||||
|
env.Exit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime:
|
||||||
|
python_exe = env.subst("$PYTHONEXE")
|
||||||
|
result = subprocess.run(
|
||||||
|
[python_exe, "-m", "espsecure", "extract_public_key",
|
||||||
|
"--keyfile", str(signing_key_path), str(bin_path)],
|
||||||
|
capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error extracting V1 verification key: {result.stderr}")
|
||||||
|
env.Exit(1)
|
||||||
|
return
|
||||||
|
print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}")
|
||||||
|
else:
|
||||||
|
# User-provided verification key -- should already be a raw binary file
|
||||||
|
verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY")
|
||||||
|
if not verification_key:
|
||||||
|
return
|
||||||
|
verification_key_path = pathlib.Path(verification_key)
|
||||||
|
if not verification_key_path.exists():
|
||||||
|
print(f"Error: Verification key not found: {verification_key_path}")
|
||||||
|
env.Exit(1)
|
||||||
|
return
|
||||||
|
shutil.copyfile(str(verification_key_path), str(bin_path))
|
||||||
|
|
||||||
|
if not bin_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate the .S assembly file from the binary key data.
|
||||||
|
# Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin.
|
||||||
|
# The file is needed in both the app build dir and the bootloader build dir, since
|
||||||
|
# the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT
|
||||||
|
# is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that
|
||||||
|
# normally generate these files.
|
||||||
|
data = bin_path.read_bytes()
|
||||||
|
varname = "signature_verification_key_bin"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f"/* Data converted from {bin_path.name} */")
|
||||||
|
lines.append(".data")
|
||||||
|
lines.append("#if !defined (__APPLE__) && !defined (__linux__)")
|
||||||
|
lines.append(".section .rodata.embedded")
|
||||||
|
lines.append("#endif")
|
||||||
|
lines.append(f"\n.global {varname}")
|
||||||
|
lines.append(f"{varname}:")
|
||||||
|
lines.append(f"\n.global _binary_{varname}_start")
|
||||||
|
lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */")
|
||||||
|
|
||||||
|
# Format binary data as .byte lines (16 bytes per line)
|
||||||
|
for i in range(0, len(data), 16):
|
||||||
|
chunk = data[i:i + 16]
|
||||||
|
hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk)
|
||||||
|
lines.append(f".byte {hex_bytes}")
|
||||||
|
|
||||||
|
lines.append(f"\n.global _binary_{varname}_end")
|
||||||
|
lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */")
|
||||||
|
lines.append(f"\n.global {varname}_length")
|
||||||
|
lines.append(f"{varname}_length:")
|
||||||
|
lines.append(f".long {len(data)}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append('#if defined (__linux__)')
|
||||||
|
lines.append('.section .note.GNU-stack,"",@progbits')
|
||||||
|
lines.append("#endif")
|
||||||
|
|
||||||
|
asm_content = "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
# Write to app build dir and bootloader build dir
|
||||||
|
asm_path.write_text(asm_content)
|
||||||
|
bootloader_dir = build_dir / "bootloader"
|
||||||
|
if bootloader_dir.is_dir():
|
||||||
|
bootloader_bin = bootloader_dir / "signature_verification_key.bin"
|
||||||
|
bootloader_asm = bootloader_dir / "signature_verification_key.bin.S"
|
||||||
|
shutil.copyfile(str(bin_path), str(bootloader_bin))
|
||||||
|
bootloader_asm.write_text(asm_content)
|
||||||
|
|
||||||
|
|
||||||
def sign_firmware(source, target, env):
|
def sign_firmware(source, target, env):
|
||||||
"""
|
"""
|
||||||
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
|
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
|
||||||
@@ -55,9 +164,12 @@ def sign_firmware(source, target, env):
|
|||||||
env.Exit(1)
|
env.Exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
|
# Determine espsecure signature version from the signing scheme:
|
||||||
# so the espsecure signature version is always 2.
|
# V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2.
|
||||||
sign_version = "2"
|
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y":
|
||||||
|
sign_version = "1"
|
||||||
|
else:
|
||||||
|
sign_version = "2"
|
||||||
|
|
||||||
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
|
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
|
||||||
firmware_path = build_dir / firmware_name
|
firmware_path = build_dir / firmware_name
|
||||||
@@ -217,6 +329,11 @@ def esp32_copy_ota_bin(source, target, env):
|
|||||||
print(f"Copied firmware to {new_file_name}")
|
print(f"Copied firmware to {new_file_name}")
|
||||||
|
|
||||||
|
|
||||||
|
# Generate V1 ECDSA verification key files before build starts.
|
||||||
|
# Workaround for PlatformIO not executing CMake custom commands that extract
|
||||||
|
# the public key and generate the .S assembly file for Secure Boot V1.
|
||||||
|
_generate_v1_verification_key(env) # noqa: F821
|
||||||
|
|
||||||
# Run signing first, then merge, then ota copy
|
# Run signing first, then merge, then ota copy
|
||||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
|
||||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
*** DO NOT USE THIS KEY...EVER ***
|
||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIEZIp96p7Z7QN6vxOFE5FdRNm535vW81Ax07KnGxVjiMoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEK+fBQDn1Q+r5lGwcDoMUgeg2Aq16LLrLUz7xWI6mS0PUClzolDIo
|
||||||
|
eaV/Pfjl7zAvkbQQsZq3rTNnr1eGAk5P+A==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
|
*** DO NOT USE THIS KEY...EVER ***
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
esp32:
|
||||||
|
variant: esp32
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
signed_ota_verification:
|
||||||
|
signing_key: ../../components/esp32/dummy_signing_key_v1_ecdsa.pem
|
||||||
|
signing_scheme: ecdsa_v1
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
Reference in New Issue
Block a user