mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 10:52:56 +08:00
[ota] Use PSA crypto for signature verification on ESP-IDF 6 (#18145)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::ota {
|
||||
|
||||
// The PSA Crypto API imports an RSA public key as a DER RSAPublicKey
|
||||
// (RFC 3279 2.3.1), not as raw bignums:
|
||||
//
|
||||
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
||||
//
|
||||
// The Secure Boot v2 signature block stores the modulus and exponent raw, so
|
||||
// they are wrapped here. Only RSA-3072 exists in that format, which fixes both
|
||||
// headers: a 3072-bit modulus always has its top bit set, so its INTEGER is
|
||||
// always tag + 2-byte length (0x181 = 385) + the sign pad; and the SEQUENCE
|
||||
// body is always 392..396 bytes, so its header is always tag + 2-byte length.
|
||||
// Only the exponent varies in width.
|
||||
constexpr size_t RSA_3072_MODULUS_BYTES = 384;
|
||||
constexpr uint8_t RSA_DER_MODULUS_PREFIX[] = {0x02, 0x82, 0x01, 0x81, 0x00};
|
||||
constexpr size_t RSA_DER_MODULUS_LEN = sizeof(RSA_DER_MODULUS_PREFIX) + RSA_3072_MODULUS_BYTES; // 389
|
||||
// 4-byte SEQUENCE header + modulus + the widest exponent INTEGER (tag, length,
|
||||
// sign pad, 4 bytes).
|
||||
constexpr size_t RSA_DER_PUBKEY_MAX = 4 + RSA_DER_MODULUS_LEN + 7;
|
||||
|
||||
/// Wrap a raw RSA-3072 modulus and exponent as a DER RSAPublicKey.
|
||||
///
|
||||
/// @param modulus_be Big-endian modulus, RSA_3072_MODULUS_BYTES long.
|
||||
/// @param exponent_be Big-endian exponent, exponent_len bytes, leading zeros allowed.
|
||||
/// Rejected if the significant bytes would not fit a short-form length.
|
||||
/// @return the encoded length, or 0 if the exponent is zero or the buffer is too small.
|
||||
inline 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) {
|
||||
// A DER INTEGER is signed: drop leading zero bytes, then prepend one back if
|
||||
// the value would otherwise read as negative.
|
||||
while (exponent_len > 0 && exponent_be[0] == 0x00) {
|
||||
exponent_be++;
|
||||
exponent_len--;
|
||||
}
|
||||
if (exponent_len == 0) {
|
||||
return 0; // a zero exponent is not a usable key
|
||||
}
|
||||
const bool pad = (exponent_be[0] & 0x80) != 0;
|
||||
const size_t exponent_content_len = exponent_len + (pad ? 1 : 0);
|
||||
if (exponent_content_len > 0x7F) {
|
||||
return 0; // would need a long-form length, which this encoder does not write
|
||||
}
|
||||
const size_t exponent_der_len = 2 + exponent_content_len;
|
||||
const size_t body_len = RSA_DER_MODULUS_LEN + exponent_der_len;
|
||||
const size_t total_len = 4 + body_len;
|
||||
if (total_len > out_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t i = 0;
|
||||
out[i++] = 0x30; // SEQUENCE
|
||||
out[i++] = 0x82; // 2-byte length follows
|
||||
out[i++] = static_cast<uint8_t>(body_len >> 8);
|
||||
out[i++] = static_cast<uint8_t>(body_len);
|
||||
memcpy(out + i, RSA_DER_MODULUS_PREFIX, sizeof(RSA_DER_MODULUS_PREFIX));
|
||||
i += sizeof(RSA_DER_MODULUS_PREFIX);
|
||||
memcpy(out + i, modulus_be, RSA_3072_MODULUS_BYTES);
|
||||
i += RSA_3072_MODULUS_BYTES;
|
||||
out[i++] = 0x02; // INTEGER
|
||||
out[i++] = static_cast<uint8_t>(exponent_content_len);
|
||||
if (pad) {
|
||||
out[i++] = 0x00;
|
||||
}
|
||||
memcpy(out + i, exponent_be, exponent_len);
|
||||
return total_len;
|
||||
}
|
||||
|
||||
} // namespace esphome::ota
|
||||
@@ -14,9 +14,20 @@
|
||||
#include <esp_partition.h>
|
||||
#include <esp_rom_crc.h>
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// mbedtls 4.0 (IDF 6.0) made the legacy mbedtls_rsa_*/mbedtls_sha256_* headers
|
||||
// private. Use the PSA Crypto API instead, like the sha256 component does. PSA
|
||||
// crypto is auto-initialized by ESP-IDF at startup (esp_psa_crypto_init.c,
|
||||
// priority 104), so no psa_crypto_init() call is needed.
|
||||
#define USE_OTA_SIG_PSA
|
||||
#include "ota_rsa_der.h"
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#include <mbedtls/md.h>
|
||||
#include <mbedtls/rsa.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::ota {
|
||||
|
||||
@@ -70,7 +81,14 @@ bool block_is_valid(const uint8_t *block) {
|
||||
}
|
||||
|
||||
bool key_digest_of(const uint8_t *block, KeyDigest &out) {
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
size_t out_len = 0;
|
||||
return psa_hash_compute(PSA_ALG_SHA_256, block + OFFSET_KEY, KEY_REGION_LEN, out.data(), out.size(), &out_len) ==
|
||||
PSA_SUCCESS &&
|
||||
out_len == out.size();
|
||||
#else
|
||||
return mbedtls_sha256(block + OFFSET_KEY, KEY_REGION_LEN, out.data(), /*is224=*/0) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// The offset of the signature sector: the app length rounded up to 4 KiB.
|
||||
@@ -93,20 +111,40 @@ bool signature_sector_offset(const esp_partition_t *part, size_t &out_offset) {
|
||||
// Returns false on a read or hash error so a hash failure is not later
|
||||
// misreported as a signature mismatch.
|
||||
bool image_digest(const esp_partition_t *part, size_t image_padded_len, uint8_t *out) {
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
psa_hash_operation_t ctx = PSA_HASH_OPERATION_INIT;
|
||||
bool ok = psa_hash_setup(&ctx, PSA_ALG_SHA_256) == PSA_SUCCESS;
|
||||
#else
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
bool ok = mbedtls_sha256_starts(&ctx, /*is224=*/0) == 0;
|
||||
#endif
|
||||
uint8_t buf[512];
|
||||
for (size_t off = 0; ok && off < image_padded_len; off += sizeof(buf)) {
|
||||
size_t chunk = std::min(sizeof(buf), image_padded_len - off);
|
||||
if (esp_partition_read(part, off, buf, chunk) != ESP_OK || mbedtls_sha256_update(&ctx, buf, chunk) != 0) {
|
||||
if (esp_partition_read(part, off, buf, chunk) != ESP_OK) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
ok = psa_hash_update(&ctx, buf, chunk) == PSA_SUCCESS;
|
||||
#else
|
||||
ok = mbedtls_sha256_update(&ctx, buf, chunk) == 0;
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
size_t out_len = 0;
|
||||
if (ok) {
|
||||
ok = psa_hash_finish(&ctx, out, SHA256_BYTES, &out_len) == PSA_SUCCESS && out_len == SHA256_BYTES;
|
||||
}
|
||||
// A no-op once the operation has been finished
|
||||
psa_hash_abort(&ctx);
|
||||
#else
|
||||
if (ok) {
|
||||
ok = mbedtls_sha256_finish(&ctx, out) == 0;
|
||||
}
|
||||
mbedtls_sha256_free(&ctx);
|
||||
#endif
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -114,6 +152,7 @@ bool image_digest(const esp_partition_t *part, size_t image_padded_len, uint8_t
|
||||
// block's modulus and signature are stored little-endian; reverse them in place
|
||||
// -- block is the caller's scratch buffer, overwritten on the next iteration --
|
||||
// rather than stacking a second 384-byte copy of each bignum.
|
||||
|
||||
bool rsa_pss_verify(uint8_t *block, const uint8_t *digest) {
|
||||
std::reverse(block + OFFSET_MODULUS, block + OFFSET_MODULUS + RSA_3072_BYTES);
|
||||
std::reverse(block + OFFSET_SIGNATURE, block + OFFSET_SIGNATURE + RSA_3072_BYTES);
|
||||
@@ -122,22 +161,48 @@ bool rsa_pss_verify(uint8_t *block, const uint8_t *digest) {
|
||||
uint8_t exponent_be[4] = {static_cast<uint8_t>(exponent_le >> 24), static_cast<uint8_t>(exponent_le >> 16),
|
||||
static_cast<uint8_t>(exponent_le >> 8), static_cast<uint8_t>(exponent_le)};
|
||||
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
static_assert(RSA_3072_BYTES == RSA_3072_MODULUS_BYTES, "signature block and DER encoder disagree on modulus size");
|
||||
uint8_t der[RSA_DER_PUBKEY_MAX];
|
||||
const size_t der_len = rsa_der_public_key(block + OFFSET_MODULUS, exponent_be, sizeof(exponent_be), der, sizeof(der));
|
||||
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attr, PSA_KEY_TYPE_RSA_PUBLIC_KEY);
|
||||
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_VERIFY_HASH);
|
||||
// ANY_SALT preserves the salt-length acceptance of mbedtls_rsa_rsassa_pss_verify(),
|
||||
// which this replaces; espsecure signs with a 32-byte salt. TF-PSA-Crypto defines
|
||||
// PSA_WANT_ALG_RSA_PSS_ANY_SALT from PSA_WANT_ALG_RSA_PSS, which IDF enables.
|
||||
psa_set_key_algorithm(&attr, PSA_ALG_RSA_PSS_ANY_SALT(PSA_ALG_SHA_256));
|
||||
mbedtls_svc_key_id_t key = MBEDTLS_SVC_KEY_ID_INIT;
|
||||
const bool key_ok = der_len != 0 && psa_import_key(&attr, der, der_len, &key) == PSA_SUCCESS;
|
||||
#else
|
||||
mbedtls_rsa_context rsa;
|
||||
mbedtls_rsa_init(&rsa);
|
||||
bool key_ok = mbedtls_rsa_import_raw(&rsa, block + OFFSET_MODULUS, RSA_3072_BYTES, nullptr, 0, nullptr, 0, nullptr, 0,
|
||||
exponent_be, sizeof(exponent_be)) == 0 &&
|
||||
mbedtls_rsa_complete(&rsa) == 0 &&
|
||||
mbedtls_rsa_set_padding(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256) == 0;
|
||||
const bool key_ok = mbedtls_rsa_import_raw(&rsa, block + OFFSET_MODULUS, RSA_3072_BYTES, nullptr, 0, nullptr, 0,
|
||||
nullptr, 0, exponent_be, sizeof(exponent_be)) == 0 &&
|
||||
mbedtls_rsa_complete(&rsa) == 0 &&
|
||||
mbedtls_rsa_set_padding(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256) == 0;
|
||||
#endif
|
||||
bool verified = false;
|
||||
if (!key_ok) {
|
||||
// A setup/allocation failure (e.g. OOM right after the download) is not a
|
||||
// signature mismatch -- log it distinctly so it isn't read as "wrong key".
|
||||
OTA_IDF_SIG_LOG(ESP_LOGE, "RSA key setup failed");
|
||||
} else {
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
verified = psa_verify_hash(key, PSA_ALG_RSA_PSS_ANY_SALT(PSA_ALG_SHA_256), digest, SHA256_BYTES,
|
||||
block + OFFSET_SIGNATURE, RSA_3072_BYTES) == PSA_SUCCESS;
|
||||
#else
|
||||
verified =
|
||||
mbedtls_rsa_rsassa_pss_verify(&rsa, MBEDTLS_MD_SHA256, SHA256_BYTES, digest, block + OFFSET_SIGNATURE) == 0;
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_OTA_SIG_PSA
|
||||
if (key_ok) {
|
||||
psa_destroy_key(key);
|
||||
}
|
||||
#else
|
||||
mbedtls_rsa_free(&rsa);
|
||||
#endif
|
||||
return verified;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/ota/ota_rsa_der.h"
|
||||
|
||||
namespace esphome::ota::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// A modulus with the top bit set, as every real 3072-bit modulus has.
|
||||
std::array<uint8_t, RSA_3072_MODULUS_BYTES> make_modulus(uint8_t first = 0xC5) {
|
||||
std::array<uint8_t, RSA_3072_MODULUS_BYTES> modulus{};
|
||||
modulus.fill(0xAB);
|
||||
modulus[0] = first;
|
||||
modulus[RSA_3072_MODULUS_BYTES - 1] = 0x01; // odd, like a real modulus
|
||||
return modulus;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// e = 65537, the exponent espsecure uses.
|
||||
TEST(RsaDerPublicKey, StandardExponent) {
|
||||
const auto modulus = make_modulus();
|
||||
const uint8_t exponent[4] = {0x00, 0x01, 0x00, 0x01};
|
||||
uint8_t der[RSA_DER_PUBKEY_MAX];
|
||||
const size_t len = rsa_der_public_key(modulus.data(), exponent, sizeof(exponent), der, sizeof(der));
|
||||
|
||||
// 4 (SEQUENCE header) + 389 (modulus) + 5 (exponent) = 398
|
||||
ASSERT_EQ(len, 398u);
|
||||
// SEQUENCE, 2-byte length of the 394-byte body
|
||||
EXPECT_EQ(der[0], 0x30);
|
||||
EXPECT_EQ(der[1], 0x82);
|
||||
EXPECT_EQ((der[2] << 8) | der[3], 394);
|
||||
// INTEGER, 2-byte length 385, sign pad, then the modulus
|
||||
EXPECT_EQ(der[4], 0x02);
|
||||
EXPECT_EQ(der[5], 0x82);
|
||||
EXPECT_EQ((der[6] << 8) | der[7], 385);
|
||||
EXPECT_EQ(der[8], 0x00);
|
||||
EXPECT_EQ(0, memcmp(der + 9, modulus.data(), modulus.size()));
|
||||
// INTEGER, 3 bytes, leading zero of the input dropped
|
||||
const size_t exp_at = 9 + RSA_3072_MODULUS_BYTES;
|
||||
EXPECT_EQ(der[exp_at], 0x02);
|
||||
EXPECT_EQ(der[exp_at + 1], 0x03);
|
||||
EXPECT_EQ(der[exp_at + 2], 0x01);
|
||||
EXPECT_EQ(der[exp_at + 3], 0x00);
|
||||
EXPECT_EQ(der[exp_at + 4], 0x01);
|
||||
}
|
||||
|
||||
// An exponent whose top bit is set needs a 0x00 sign pad, widening the body.
|
||||
TEST(RsaDerPublicKey, ExponentNeedingSignPad) {
|
||||
const auto modulus = make_modulus();
|
||||
const uint8_t exponent[4] = {0x00, 0x00, 0x00, 0x81};
|
||||
uint8_t der[RSA_DER_PUBKEY_MAX];
|
||||
const size_t len = rsa_der_public_key(modulus.data(), exponent, sizeof(exponent), der, sizeof(der));
|
||||
|
||||
ASSERT_EQ(len, 397u); // 4 + 389 + 4
|
||||
const size_t exp_at = 9 + RSA_3072_MODULUS_BYTES;
|
||||
EXPECT_EQ(der[exp_at], 0x02);
|
||||
EXPECT_EQ(der[exp_at + 1], 0x02); // pad + one value byte
|
||||
EXPECT_EQ(der[exp_at + 2], 0x00);
|
||||
EXPECT_EQ(der[exp_at + 3], 0x81);
|
||||
}
|
||||
|
||||
// The widest exponent still fits the documented buffer size.
|
||||
TEST(RsaDerPublicKey, WidestExponentFitsBuffer) {
|
||||
const auto modulus = make_modulus();
|
||||
const uint8_t exponent[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
||||
uint8_t der[RSA_DER_PUBKEY_MAX];
|
||||
const size_t len = rsa_der_public_key(modulus.data(), exponent, sizeof(exponent), der, sizeof(der));
|
||||
|
||||
ASSERT_EQ(len, RSA_DER_PUBKEY_MAX); // 4 + 389 + 7
|
||||
EXPECT_LE(len, sizeof(der));
|
||||
}
|
||||
|
||||
TEST(RsaDerPublicKey, ZeroExponentRejected) {
|
||||
const auto modulus = make_modulus();
|
||||
const uint8_t exponent[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t der[RSA_DER_PUBKEY_MAX];
|
||||
EXPECT_EQ(rsa_der_public_key(modulus.data(), exponent, sizeof(exponent), der, sizeof(der)), 0u);
|
||||
}
|
||||
|
||||
// A buffer that cannot hold the result must be refused, not overrun. Sized
|
||||
// against a heap vector so ASAN catches a write past the end.
|
||||
TEST(RsaDerPublicKey, ShortBufferRejected) {
|
||||
const auto modulus = make_modulus();
|
||||
const uint8_t exponent[4] = {0x00, 0x01, 0x00, 0x01};
|
||||
for (size_t out_len : {size_t(0), size_t(1), size_t(4), size_t(100), size_t(397)}) {
|
||||
std::vector<uint8_t> der(out_len);
|
||||
EXPECT_EQ(rsa_der_public_key(modulus.data(), exponent, sizeof(exponent), der.data(), out_len), 0u)
|
||||
<< "out_len=" << out_len;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ota::testing
|
||||
Reference in New Issue
Block a user