mirror of
https://github.com/esphome/esphome.git
synced 2026-08-18 03:59:08 +08:00
[esp32] Do not require verification_key with Secure Boot V2 signing schemes (#17497)
This commit is contained in:
@@ -1160,6 +1160,74 @@ def _ota_downgrade_protection_errors(
|
||||
return errs
|
||||
|
||||
|
||||
_SIGNED_OTA_VERIFICATION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SIGNING_KEY): cv.file_,
|
||||
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
|
||||
cv.Optional(CONF_SIGNING_SCHEME, default="rsa3072"): cv.one_of(
|
||||
*SIGNING_SCHEMES, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def _validate_signed_ota_verification(value):
|
||||
if value is SCHEMA_EXTRACT:
|
||||
# Expose the inner schema so the language-schema dumper can walk the
|
||||
# signing_key / verification_key / signing_scheme options.
|
||||
return _SIGNED_OTA_VERIFICATION_SCHEMA
|
||||
if value is None:
|
||||
# A bare `signed_ota_verification:` block is valid: the default V2
|
||||
# scheme needs no keys (verify externally-signed binaries).
|
||||
value = {}
|
||||
return _validate_signed_ota_keys(_SIGNED_OTA_VERIFICATION_SCHEMA(value))
|
||||
|
||||
|
||||
def _validate_signed_ota_keys(config: ConfigType) -> ConfigType:
|
||||
"""Validate the signing/verification key combination for the selected scheme.
|
||||
|
||||
A verification key is only used by the Secure Boot V1 scheme (ecdsa_v1):
|
||||
the public key is compiled into the app so it can verify externally-signed
|
||||
images. ESP-IDF's CONFIG_SECURE_BOOT_VERIFICATION_KEY only takes effect
|
||||
when the V1 ECDSA scheme is selected and binaries are not signed during
|
||||
the build (see SECURE_BOOT_VERIFICATION_KEY in the bootloader Kconfig).
|
||||
|
||||
The V2 schemes (rsa3072, ecdsa256) embed the public key in the signature
|
||||
block appended to each image, so verifying externally-signed binaries
|
||||
needs no key in the config at all -- omitting both keys selects that
|
||||
external-signing mode.
|
||||
"""
|
||||
has_signing_key = CONF_SIGNING_KEY in config
|
||||
has_verification_key = CONF_VERIFICATION_KEY in config
|
||||
scheme = config[CONF_SIGNING_SCHEME]
|
||||
if has_signing_key and has_verification_key:
|
||||
raise cv.Invalid(
|
||||
f"Provide at most one of '{CONF_SIGNING_KEY}' and "
|
||||
f"'{CONF_VERIFICATION_KEY}', not both.",
|
||||
path=[CONF_VERIFICATION_KEY],
|
||||
)
|
||||
if scheme == "ecdsa_v1":
|
||||
if not has_signing_key and not has_verification_key:
|
||||
raise cv.Invalid(
|
||||
f"Signing scheme 'ecdsa_v1' requires either '{CONF_SIGNING_KEY}' "
|
||||
f"(to sign binaries during the build) or '{CONF_VERIFICATION_KEY}' "
|
||||
f"(to verify binaries signed externally).",
|
||||
path=[CONF_SIGNING_KEY],
|
||||
)
|
||||
elif has_verification_key:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_VERIFICATION_KEY}' is only used with signing scheme "
|
||||
f"'ecdsa_v1'. With '{scheme}' the public key is embedded in each "
|
||||
f"image's signature block, so no key file is needed to verify "
|
||||
f"externally-signed binaries: remove '{CONF_VERIFICATION_KEY}', and "
|
||||
f"set '{CONF_SIGNING_KEY}' only if binaries should be signed during "
|
||||
f"the build.",
|
||||
path=[CONF_VERIFICATION_KEY],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
# Imported locally to avoid circular import issues
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
@@ -1361,7 +1429,7 @@ def final_validate(config):
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Signed OTA verification is configured with a public verification key. "
|
||||
"Signed OTA verification is enabled without a signing key. "
|
||||
"Binaries will NOT be signed automatically during build. "
|
||||
"You must sign them externally before flashing."
|
||||
)
|
||||
@@ -1640,18 +1708,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, default=False
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SIGNING_KEY): cv.file_,
|
||||
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
|
||||
cv.Optional(
|
||||
CONF_SIGNING_SCHEME, default="rsa3072"
|
||||
): cv.one_of(*SIGNING_SCHEMES, lower=True),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_SIGNED_OTA_VERIFICATION
|
||||
): _validate_signed_ota_verification,
|
||||
cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema(
|
||||
{
|
||||
# eFuse key block (0-5) that stores the HMAC key from
|
||||
@@ -2498,12 +2557,16 @@ async def to_code(config):
|
||||
signed_ota[CONF_SIGNING_KEY].resolve().as_posix(),
|
||||
)
|
||||
else:
|
||||
# Public key mode — verification only, external signing required
|
||||
# External signing mode — binaries must be signed after the build
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(),
|
||||
)
|
||||
if CONF_VERIFICATION_KEY in signed_ota:
|
||||
# V1 ECDSA only: the public key is compiled into the app to
|
||||
# verify externally-signed images. V2 schemes carry the public
|
||||
# key in each image's signature block and need no key here.
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(),
|
||||
)
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
|
||||
@@ -665,3 +665,78 @@ def test_downgrade_protection_reports_all_unmet_requirements() -> None:
|
||||
# No project version and no signing -> two distinct errors.
|
||||
errs = _ota_downgrade_protection_errors(None, signed_ota_enabled=False)
|
||||
assert len(errs) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
# V2 schemes: signing key (sign during build) or no key at all
|
||||
# (external signing; the public key travels in the signature block).
|
||||
{"signing_scheme": "rsa3072", "signing_key": "key.pem"},
|
||||
{"signing_scheme": "rsa3072"},
|
||||
{"signing_scheme": "ecdsa256", "signing_key": "key.pem"},
|
||||
{"signing_scheme": "ecdsa256"},
|
||||
# V1 ECDSA: exactly one of signing key / verification key.
|
||||
{"signing_scheme": "ecdsa_v1", "signing_key": "key.pem"},
|
||||
{"signing_scheme": "ecdsa_v1", "verification_key": "key.bin"},
|
||||
],
|
||||
)
|
||||
def test_signed_ota_keys_valid_combinations(config: dict) -> None:
|
||||
from esphome.components.esp32 import _validate_signed_ota_keys
|
||||
|
||||
assert _validate_signed_ota_keys(config) is config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [None, {}])
|
||||
def test_signed_ota_bare_block_selects_v2_external_signing(value: dict | None) -> None:
|
||||
"""A bare `signed_ota_verification:` block is valid: the default V2
|
||||
scheme embeds the public key in the signature block, so verifying
|
||||
externally-signed binaries needs no keys in the config."""
|
||||
from esphome.components.esp32 import _validate_signed_ota_verification
|
||||
|
||||
config = _validate_signed_ota_verification(value)
|
||||
assert config == {"signing_scheme": "rsa3072"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "match"),
|
||||
[
|
||||
# A verification key is meaningless with the V2 schemes -- the public
|
||||
# key is embedded in each image's signature block.
|
||||
(
|
||||
{"signing_scheme": "rsa3072", "verification_key": "key.bin"},
|
||||
"only used with signing scheme 'ecdsa_v1'",
|
||||
),
|
||||
(
|
||||
{"signing_scheme": "ecdsa256", "verification_key": "key.bin"},
|
||||
"only used with signing scheme 'ecdsa_v1'",
|
||||
),
|
||||
# V1 ECDSA needs a key either way.
|
||||
(
|
||||
{"signing_scheme": "ecdsa_v1"},
|
||||
"Signing scheme 'ecdsa_v1' requires either",
|
||||
),
|
||||
# Never both keys at once.
|
||||
(
|
||||
{
|
||||
"signing_scheme": "rsa3072",
|
||||
"signing_key": "key.pem",
|
||||
"verification_key": "key.bin",
|
||||
},
|
||||
"not both",
|
||||
),
|
||||
(
|
||||
{
|
||||
"signing_scheme": "ecdsa_v1",
|
||||
"signing_key": "key.pem",
|
||||
"verification_key": "key.bin",
|
||||
},
|
||||
"not both",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_signed_ota_keys_invalid_combinations(config: dict, match: str) -> None:
|
||||
from esphome.components.esp32 import _validate_signed_ota_keys
|
||||
|
||||
with pytest.raises(cv.Invalid, match=match):
|
||||
_validate_signed_ota_keys(config)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Secure Boot V2 schemes carry the public key inside each image's signature
|
||||
# block, so verifying externally-signed binaries needs no key in the config:
|
||||
# a bare block enables verification with the default rsa3072 scheme.
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user