ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
ota_signature_esp_idf.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
3
4#ifdef USE_OTA_SIGNED_VERIFICATION_MULTI_KEY
6#include "esphome/core/log.h"
7
8#include <algorithm>
9#include <array>
10#include <cstring>
11#include <memory>
12#include <new>
13#include <esp_image_format.h>
14#include <esp_partition.h>
15#include <esp_rom_crc.h>
16
17#include <esp_idf_version.h>
18#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
19// mbedtls 4.0 (IDF 6.0) made the legacy mbedtls_rsa_*/mbedtls_sha256_* headers
20// private. Use the PSA Crypto API instead, like the sha256 component does. PSA
21// crypto is auto-initialized by ESP-IDF at startup (esp_psa_crypto_init.c,
22// priority 104), so no psa_crypto_init() call is needed.
23#define USE_OTA_SIG_PSA
24#include "ota_rsa_der.h"
25#include <psa/crypto.h>
26#else
27#include <mbedtls/md.h>
28#include <mbedtls/rsa.h>
29#include <mbedtls/sha256.h>
30#endif
31
32namespace esphome::ota {
33
34static const char *const TAG = "ota.idf";
35
36// Route the "Signature check: " prefix (and its per-block form) through one
37// shared format string each, so the prefix is pooled once by the linker instead
38// of duplicated at every call site. The level macro is forwarded so compile-time
39// log-level stripping still applies.
40#define OTA_IDF_SIG_LOG(level, msg) level(TAG, "Signature check: %s", msg)
41#define OTA_IDF_SIG_LOG_BLOCK(level, i, msg) level(TAG, "Signature check: block %zu: %s", static_cast<size_t>(i), msg)
42
43// Secure Boot v2 RSA-3072 signature block, as written by espsecure and stored
44// in the 4 KiB sector following the (4 KiB-padded) app image. All bignum
45// fields are byte-reversed to little-endian for the RSA accelerator; software
46// verification reverses them back. See the espsecure "<BBxx32s384sI384sI384s"
47// packing for the authoritative layout.
48namespace {
49constexpr uint8_t SIG_BLOCK_MAGIC = 0xE7;
50constexpr uint8_t SIG_BLOCK_VERSION_RSA = 0x02;
51constexpr size_t SIG_BLOCK_SIZE = 1216;
52constexpr size_t SIG_SECTOR_ALIGN = 4096;
53constexpr size_t SIG_BLOCK_MAX_COUNT = 3;
54constexpr size_t RSA_3072_BYTES = 384;
55constexpr size_t SHA256_BYTES = 32;
56
57constexpr size_t OFFSET_KEY = 36; // start of the hashed public-key region
58constexpr size_t KEY_REGION_LEN = 776; // n[384] + e[4] + rinv[384] + m[4]
59constexpr size_t OFFSET_MODULUS = 36; // n[384], little-endian
60constexpr size_t OFFSET_EXPONENT = 420; // e, uint32 little-endian
61constexpr size_t OFFSET_SIGNATURE = 812; // signature[384], little-endian
62constexpr size_t OFFSET_CRC = 1196; // crc32 over bytes [0, 1196)
63
64// A public key is identified by the SHA-256 of its 776-byte key region, exactly
65// as the ROM computes it. The trusted set is compiled into the app from the
66// config's verification_keys (esp32 signed_ota codegen) -- an immutable anchor
67// that, unlike the appendable signature sector, an OTA cannot enlarge.
68using KeyDigest = std::array<uint8_t, SHA256_BYTES>;
69constexpr uint8_t TRUSTED_KEY_DIGESTS[OTA_TRUSTED_KEY_COUNT][SHA256_BYTES] = OTA_TRUSTED_KEY_DIGESTS;
70
71// A block is structurally valid if the magic, version, and CRC all check out.
72// The CRC covers everything before it and uses the same ROM routine the
73// bootloader validates the block with, so the check matches byte-for-byte.
74bool block_is_valid(const uint8_t *block) {
75 if (block[0] != SIG_BLOCK_MAGIC || block[1] != SIG_BLOCK_VERSION_RSA) {
76 return false;
77 }
78 uint32_t stored_crc;
79 memcpy(&stored_crc, block + OFFSET_CRC, sizeof(stored_crc));
80 return esp_rom_crc32_le(0, block, OFFSET_CRC) == stored_crc;
81}
82
83bool key_digest_of(const uint8_t *block, KeyDigest &out) {
84#ifdef USE_OTA_SIG_PSA
85 size_t out_len = 0;
86 return psa_hash_compute(PSA_ALG_SHA_256, block + OFFSET_KEY, KEY_REGION_LEN, out.data(), out.size(), &out_len) ==
87 PSA_SUCCESS &&
88 out_len == out.size();
89#else
90 return mbedtls_sha256(block + OFFSET_KEY, KEY_REGION_LEN, out.data(), /*is224=*/0) == 0;
91#endif
92}
93
94// The offset of the signature sector: the app length rounded up to 4 KiB.
95bool signature_sector_offset(const esp_partition_t *part, size_t &out_offset) {
96 esp_partition_pos_t pos{.offset = part->address, .size = part->size};
97 esp_image_metadata_t meta{};
98 if (esp_image_get_metadata(&pos, &meta) != ESP_OK) {
99 return false;
100 }
101 // Bound the image length before rounding up so a crafted header can't
102 // overflow the addition; the image plus its signature sector must fit.
103 if (meta.image_len > part->size) {
104 return false;
105 }
106 out_offset = (meta.image_len + SIG_SECTOR_ALIGN - 1) & ~(SIG_SECTOR_ALIGN - 1);
107 return out_offset + SIG_BLOCK_SIZE <= part->size;
108}
109
110// SHA-256 over the 4 KiB-padded image, i.e. everything the signature covers.
111// Returns false on a read or hash error so a hash failure is not later
112// misreported as a signature mismatch.
113bool image_digest(const esp_partition_t *part, size_t image_padded_len, uint8_t *out) {
114#ifdef USE_OTA_SIG_PSA
115 psa_hash_operation_t ctx = PSA_HASH_OPERATION_INIT;
116 bool ok = psa_hash_setup(&ctx, PSA_ALG_SHA_256) == PSA_SUCCESS;
117#else
118 mbedtls_sha256_context ctx;
119 mbedtls_sha256_init(&ctx);
120 bool ok = mbedtls_sha256_starts(&ctx, /*is224=*/0) == 0;
121#endif
122 uint8_t buf[512];
123 for (size_t off = 0; ok && off < image_padded_len; off += sizeof(buf)) {
124 size_t chunk = std::min(sizeof(buf), image_padded_len - off);
125 if (esp_partition_read(part, off, buf, chunk) != ESP_OK) {
126 ok = false;
127 break;
128 }
129#ifdef USE_OTA_SIG_PSA
130 ok = psa_hash_update(&ctx, buf, chunk) == PSA_SUCCESS;
131#else
132 ok = mbedtls_sha256_update(&ctx, buf, chunk) == 0;
133#endif
134 }
135#ifdef USE_OTA_SIG_PSA
136 size_t out_len = 0;
137 if (ok) {
138 ok = psa_hash_finish(&ctx, out, SHA256_BYTES, &out_len) == PSA_SUCCESS && out_len == SHA256_BYTES;
139 }
140 // A no-op once the operation has been finished
141 psa_hash_abort(&ctx);
142#else
143 if (ok) {
144 ok = mbedtls_sha256_finish(&ctx, out) == 0;
145 }
146 mbedtls_sha256_free(&ctx);
147#endif
148 return ok;
149}
150
151// Verify one RSA-PSS-3072-SHA256 signature block over the image digest. The
152// block's modulus and signature are stored little-endian; reverse them in place
153// -- block is the caller's scratch buffer, overwritten on the next iteration --
154// rather than stacking a second 384-byte copy of each bignum.
155
156bool rsa_pss_verify(uint8_t *block, const uint8_t *digest) {
157 std::reverse(block + OFFSET_MODULUS, block + OFFSET_MODULUS + RSA_3072_BYTES);
158 std::reverse(block + OFFSET_SIGNATURE, block + OFFSET_SIGNATURE + RSA_3072_BYTES);
159 uint32_t exponent_le;
160 memcpy(&exponent_le, block + OFFSET_EXPONENT, sizeof(exponent_le));
161 uint8_t exponent_be[4] = {static_cast<uint8_t>(exponent_le >> 24), static_cast<uint8_t>(exponent_le >> 16),
162 static_cast<uint8_t>(exponent_le >> 8), static_cast<uint8_t>(exponent_le)};
163
164#ifdef USE_OTA_SIG_PSA
165 static_assert(RSA_3072_BYTES == RSA_3072_MODULUS_BYTES, "signature block and DER encoder disagree on modulus size");
166 uint8_t der[RSA_DER_PUBKEY_MAX];
167 const size_t der_len = rsa_der_public_key(block + OFFSET_MODULUS, exponent_be, sizeof(exponent_be), der, sizeof(der));
168 psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
169 psa_set_key_type(&attr, PSA_KEY_TYPE_RSA_PUBLIC_KEY);
170 psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_VERIFY_HASH);
171 // ANY_SALT preserves the salt-length acceptance of mbedtls_rsa_rsassa_pss_verify(),
172 // which this replaces; espsecure signs with a 32-byte salt. TF-PSA-Crypto defines
173 // PSA_WANT_ALG_RSA_PSS_ANY_SALT from PSA_WANT_ALG_RSA_PSS, which IDF enables.
174 psa_set_key_algorithm(&attr, PSA_ALG_RSA_PSS_ANY_SALT(PSA_ALG_SHA_256));
175 mbedtls_svc_key_id_t key = MBEDTLS_SVC_KEY_ID_INIT;
176 const bool key_ok = der_len != 0 && psa_import_key(&attr, der, der_len, &key) == PSA_SUCCESS;
177#else
178 mbedtls_rsa_context rsa;
179 mbedtls_rsa_init(&rsa);
180 const bool key_ok = mbedtls_rsa_import_raw(&rsa, block + OFFSET_MODULUS, RSA_3072_BYTES, nullptr, 0, nullptr, 0,
181 nullptr, 0, exponent_be, sizeof(exponent_be)) == 0 &&
182 mbedtls_rsa_complete(&rsa) == 0 &&
183 mbedtls_rsa_set_padding(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256) == 0;
184#endif
185 bool verified = false;
186 if (!key_ok) {
187 // A setup/allocation failure (e.g. OOM right after the download) is not a
188 // signature mismatch -- log it distinctly so it isn't read as "wrong key".
189 OTA_IDF_SIG_LOG(ESP_LOGE, "RSA key setup failed");
190 } else {
191#ifdef USE_OTA_SIG_PSA
192 verified = psa_verify_hash(key, PSA_ALG_RSA_PSS_ANY_SALT(PSA_ALG_SHA_256), digest, SHA256_BYTES,
193 block + OFFSET_SIGNATURE, RSA_3072_BYTES) == PSA_SUCCESS;
194#else
195 verified =
196 mbedtls_rsa_rsassa_pss_verify(&rsa, MBEDTLS_MD_SHA256, SHA256_BYTES, digest, block + OFFSET_SIGNATURE) == 0;
197#endif
198 }
199#ifdef USE_OTA_SIG_PSA
200 if (key_ok) {
201 psa_destroy_key(key);
202 }
203#else
204 mbedtls_rsa_free(&rsa);
205#endif
206 return verified;
207}
208
209} // namespace
210
211bool IDFOTABackend::verify_signed_image_(const esp_partition_t *incoming) {
212 // Verification re-hashes the full image (after esp_ota_end already did one
213 // pass), which can approach the task WDT budget on a large app. Extend it for
214 // the duration, mirroring the erase budget in begin().
215 const uint32_t verify_budget_ms = 15000 + (incoming->size >> 10) * 10;
216 watchdog::WatchdogManager watchdog(verify_budget_ms);
217
218 size_t incoming_sector;
219 if (!signature_sector_offset(incoming, incoming_sector)) {
220 OTA_IDF_SIG_LOG(ESP_LOGE, "cannot locate incoming signature sector");
221 return false;
222 }
223 uint8_t digest[SHA256_BYTES];
224 if (!image_digest(incoming, incoming_sector, digest)) {
225 OTA_IDF_SIG_LOG(ESP_LOGE, "cannot hash incoming image");
226 return false;
227 }
228
229 // Accept if any incoming block is signed by a compiled-in trusted key AND its
230 // signature verifies over the image. Iterating all blocks (not just the
231 // first) is the whole point -- it lets a bridge/backup key in a later block
232 // be the match. The trust check is against the immutable compiled-in set, so
233 // extra (self-signed) blocks an attacker appends carry keys we simply ignore.
234 // Heap-allocate the 1216-byte block for the duration of verification: this
235 // runs mid-OTA on the loop task, on top of the caller's live 1 KB OTA buffer
236 // and mbedtls's own ~1 KB verify scratch, so keeping it off the stack widens
237 // a thin margin. One short-lived allocation right before reboot is not the
238 // fragmentation pattern the project guards against. nothrow so an OOM here
239 // fails closed like every other error path, rather than aborting.
240 std::unique_ptr<uint8_t[]> block(new (std::nothrow) uint8_t[SIG_BLOCK_SIZE]);
241 if (!block) {
242 OTA_IDF_SIG_LOG(ESP_LOGE, "out of memory");
243 return false;
244 }
245 bool any_valid_block = false;
246 for (size_t i = 0; i < SIG_BLOCK_MAX_COUNT; i++) {
247 size_t off = incoming_sector + i * SIG_BLOCK_SIZE;
248 if (off + SIG_BLOCK_SIZE > incoming->size) {
249 break; // partition has no room for another block; done scanning
250 }
251 // A read fault is not "no trusted key" -- fail closed with a distinct error.
252 if (esp_partition_read(incoming, off, block.get(), SIG_BLOCK_SIZE) != ESP_OK) {
253 OTA_IDF_SIG_LOG_BLOCK(ESP_LOGE, i, "unreadable");
254 return false;
255 }
256 if (!block_is_valid(block.get())) {
257 OTA_IDF_SIG_LOG_BLOCK(ESP_LOGD, i, "absent or malformed");
258 continue;
259 }
260 any_valid_block = true;
261 KeyDigest incoming_key;
262 if (!key_digest_of(block.get(), incoming_key)) {
263 OTA_IDF_SIG_LOG_BLOCK(ESP_LOGE, i, "key hash failed");
264 return false;
265 }
266 bool trusted_key = false;
267 for (const auto &trusted : TRUSTED_KEY_DIGESTS) {
268 if (memcmp(incoming_key.data(), trusted, SHA256_BYTES) == 0) {
269 trusted_key = true;
270 break;
271 }
272 }
273 if (!trusted_key) {
274 OTA_IDF_SIG_LOG_BLOCK(ESP_LOGW, i, "signed by an untrusted key");
275 continue;
276 }
277 if (rsa_pss_verify(block.get(), digest)) {
278 OTA_IDF_SIG_LOG_BLOCK(ESP_LOGD, i, "verified with a trusted key");
279 return true;
280 }
281 OTA_IDF_SIG_LOG_BLOCK(ESP_LOGW, i, "trusted key failed to verify");
282 }
283
284 // Separate "not signed at all" from "signed by an untrusted key" -- the former
285 // otherwise reads as the latter on a device that only logs at INFO.
286 if (!any_valid_block) {
287 OTA_IDF_SIG_LOG(ESP_LOGE, "image has no signature block");
288 } else {
289 OTA_IDF_SIG_LOG(ESP_LOGE, "no trusted key produced a valid signature");
290 }
291 return false;
292}
293
294} // namespace esphome::ota
295
296#endif // USE_OTA_SIGNED_VERIFICATION_MULTI_KEY
297#endif // USE_ESP32
constexpr size_t RSA_3072_MODULUS_BYTES
Definition ota_rsa_der.h:20
size_t rsa_der_public_key(const uint8_t *modulus_be, const uint8_t *exponent_be, size_t exponent_len, uint8_t *out, size_t out_len)
Wrap a raw RSA-3072 modulus and exponent as a DER RSAPublicKey.
Definition ota_rsa_der.h:33
constexpr size_t RSA_DER_PUBKEY_MAX
Definition ota_rsa_der.h:25
size_t size_t pos
Definition helpers.h:1062
static void uint32_t