mirror of
https://github.com/esphome/esphome.git
synced 2026-05-20 17:52:00 +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 = {
|
||||
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_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.
|
||||
# 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
|
||||
# appropriate set if they only support one scheme.
|
||||
SIGNED_OTA_RSA_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only
|
||||
# when minimum_chip_revision >= 3.0, which requires special handling.
|
||||
SIGNED_OTA_V2_RSA_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C3,
|
||||
}
|
||||
SIGNED_OTA_ECC_ONLY_VARIANTS = {
|
||||
SIGNED_OTA_V2_ECC_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32C2,
|
||||
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 = {
|
||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||
@@ -991,25 +998,73 @@ def final_validate(config):
|
||||
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
||||
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
||||
variant = config[CONF_VARIANT]
|
||||
scheme_variant_conflicts = {
|
||||
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
|
||||
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
|
||||
}
|
||||
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
|
||||
0
|
||||
]:
|
||||
min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION)
|
||||
scheme_path = [
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ADVANCED,
|
||||
CONF_SIGNED_OTA_VERIFICATION,
|
||||
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(
|
||||
cv.Invalid(
|
||||
f"Signing scheme '{scheme}' is not supported on "
|
||||
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
|
||||
path=[
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ADVANCED,
|
||||
CONF_SIGNED_OTA_VERIFICATION,
|
||||
CONF_SIGNING_SCHEME,
|
||||
],
|
||||
f"Signing scheme 'ecdsa_v1' is only supported on "
|
||||
f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. "
|
||||
f"Use 'rsa3072' or 'ecdsa256' instead.",
|
||||
path=scheme_path,
|
||||
)
|
||||
)
|
||||
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:
|
||||
_LOGGER.warning(
|
||||
"Signed OTA verification is enabled but no OTA component is configured. "
|
||||
|
||||
@@ -5,6 +5,7 @@ import json # noqa: E402
|
||||
import os # noqa: E402
|
||||
import pathlib # noqa: E402
|
||||
import shutil # noqa: E402
|
||||
import subprocess # noqa: E402
|
||||
from glob import glob # noqa: E402
|
||||
|
||||
|
||||
@@ -25,6 +26,114 @@ def _parse_sdkconfig(sdkconfig_path):
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
return
|
||||
|
||||
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
|
||||
# so the espsecure signature version is always 2.
|
||||
sign_version = "2"
|
||||
# Determine espsecure signature version from the signing scheme:
|
||||
# V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --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_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}")
|
||||
|
||||
|
||||
# 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
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # 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