mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
[esp32] Add software OTA downgrade protection (#17315)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2dd7ac090f
commit
d9998eff20
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANTS,
|
||||
NetworkSdkconfigData,
|
||||
_ota_downgrade_protection_errors,
|
||||
_reconcile_network_sdkconfig,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
@@ -560,3 +561,35 @@ def test_network_wifi_ble_coexistence_reconciles_end_to_end(
|
||||
assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False
|
||||
# WiFi present alongside BT -> WiFi stack must stay enabled.
|
||||
assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig
|
||||
|
||||
|
||||
def test_downgrade_protection_passes_with_numeric_version_and_signing() -> None:
|
||||
assert _ota_downgrade_protection_errors("1.2.3", signed_ota_enabled=True) == []
|
||||
|
||||
|
||||
def test_downgrade_protection_accepts_calendar_version() -> None:
|
||||
assert _ota_downgrade_protection_errors("2024.12.0", signed_ota_enabled=True) == []
|
||||
|
||||
|
||||
def test_downgrade_protection_requires_project_version() -> None:
|
||||
errs = _ota_downgrade_protection_errors(None, signed_ota_enabled=True)
|
||||
assert len(errs) == 1
|
||||
assert "version" in str(errs[0])
|
||||
|
||||
|
||||
def test_downgrade_protection_rejects_non_numeric_version() -> None:
|
||||
errs = _ota_downgrade_protection_errors("1.0-beta", signed_ota_enabled=True)
|
||||
assert len(errs) == 1
|
||||
assert "dotted-numeric" in str(errs[0])
|
||||
|
||||
|
||||
def test_downgrade_protection_requires_signed_ota() -> None:
|
||||
errs = _ota_downgrade_protection_errors("1.2.3", signed_ota_enabled=False)
|
||||
assert len(errs) == 1
|
||||
assert "signed_ota_verification" in str(errs[0])
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
project:
|
||||
name: esphome.downgrade_test
|
||||
version: "1.2.3"
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_ota_downgrade_protection: true
|
||||
signed_ota_verification:
|
||||
signing_key: ../../components/esp32/dummy_signing_key.pem
|
||||
signing_scheme: rsa3072
|
||||
|
||||
# wifi + ota so the IDF OTA backend compiles with USE_OTA_DOWNGRADE_PROTECTION.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,9 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# md5's to_code calls cg.add_define("USE_MD5"), which gates md5.h. C++ unit
|
||||
# test builds that pull md5 in transitively (e.g. ota's host backend, which
|
||||
# has an md5::MD5Digest member) need that define, otherwise md5.h compiles to
|
||||
# nothing and the dependent headers fail to find md5::MD5Digest.
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
|
||||
namespace esphome::ota::testing {
|
||||
|
||||
// version_is_older(candidate, reference) == true means candidate is a downgrade
|
||||
// and should be rejected.
|
||||
|
||||
TEST(VersionIsOlder, PatchOlder) {
|
||||
EXPECT_TRUE(version_is_older("1.2.3", "1.2.4"));
|
||||
EXPECT_FALSE(version_is_older("1.2.4", "1.2.3"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, NumericNotLexical) {
|
||||
// "1.10.0" is newer than "1.9.0" even though '1' < '9' lexically.
|
||||
EXPECT_TRUE(version_is_older("1.9.0", "1.10.0"));
|
||||
EXPECT_FALSE(version_is_older("1.10.0", "1.9.0"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, MajorMinor) {
|
||||
EXPECT_TRUE(version_is_older("1.9.9", "2.0.0"));
|
||||
EXPECT_TRUE(version_is_older("1.2.9", "1.3.0"));
|
||||
EXPECT_FALSE(version_is_older("2.0.0", "1.9.9"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, EqualVersionsAllowed) {
|
||||
// Re-flashing the same version must be permitted.
|
||||
EXPECT_FALSE(version_is_older("1.2.3", "1.2.3"));
|
||||
EXPECT_FALSE(version_is_older("2024.1.0", "2024.1.0"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, DifferingComponentCounts) {
|
||||
// Missing trailing components count as 0.
|
||||
EXPECT_FALSE(version_is_older("1.2", "1.2.0"));
|
||||
EXPECT_FALSE(version_is_older("1.2.0", "1.2"));
|
||||
EXPECT_TRUE(version_is_older("1.2", "1.2.1"));
|
||||
EXPECT_FALSE(version_is_older("1.2.1", "1.2"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, CalendarVersions) {
|
||||
EXPECT_TRUE(version_is_older("2024.12.0", "2025.1.0"));
|
||||
EXPECT_FALSE(version_is_older("2025.1.0", "2024.12.0"));
|
||||
}
|
||||
|
||||
TEST(VersionIsOlder, NullInputsAreSafe) {
|
||||
EXPECT_FALSE(version_is_older(nullptr, "1.2.3"));
|
||||
EXPECT_FALSE(version_is_older("1.2.3", nullptr));
|
||||
EXPECT_FALSE(version_is_older(nullptr, nullptr));
|
||||
}
|
||||
|
||||
} // namespace esphome::ota::testing
|
||||
Reference in New Issue
Block a user