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