mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00: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_partition.h>
|
||||||
#include <esp_rom_crc.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/md.h>
|
||||||
#include <mbedtls/rsa.h>
|
#include <mbedtls/rsa.h>
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome::ota {
|
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) {
|
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;
|
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.
|
// 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
|
// Returns false on a read or hash error so a hash failure is not later
|
||||||
// misreported as a signature mismatch.
|
// misreported as a signature mismatch.
|
||||||
bool image_digest(const esp_partition_t *part, size_t image_padded_len, uint8_t *out) {
|
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_context ctx;
|
||||||
mbedtls_sha256_init(&ctx);
|
mbedtls_sha256_init(&ctx);
|
||||||
bool ok = mbedtls_sha256_starts(&ctx, /*is224=*/0) == 0;
|
bool ok = mbedtls_sha256_starts(&ctx, /*is224=*/0) == 0;
|
||||||
|
#endif
|
||||||
uint8_t buf[512];
|
uint8_t buf[512];
|
||||||
for (size_t off = 0; ok && off < image_padded_len; off += sizeof(buf)) {
|
for (size_t off = 0; ok && off < image_padded_len; off += sizeof(buf)) {
|
||||||
size_t chunk = std::min(sizeof(buf), image_padded_len - off);
|
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;
|
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) {
|
if (ok) {
|
||||||
ok = mbedtls_sha256_finish(&ctx, out) == 0;
|
ok = mbedtls_sha256_finish(&ctx, out) == 0;
|
||||||
}
|
}
|
||||||
mbedtls_sha256_free(&ctx);
|
mbedtls_sha256_free(&ctx);
|
||||||
|
#endif
|
||||||
return ok;
|
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's modulus and signature are stored little-endian; reverse them in place
|
||||||
// -- block is the caller's scratch buffer, overwritten on the next iteration --
|
// -- block is the caller's scratch buffer, overwritten on the next iteration --
|
||||||
// rather than stacking a second 384-byte copy of each bignum.
|
// rather than stacking a second 384-byte copy of each bignum.
|
||||||
|
|
||||||
bool rsa_pss_verify(uint8_t *block, const uint8_t *digest) {
|
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_MODULUS, block + OFFSET_MODULUS + RSA_3072_BYTES);
|
||||||
std::reverse(block + OFFSET_SIGNATURE, block + OFFSET_SIGNATURE + 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),
|
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)};
|
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_context rsa;
|
||||||
mbedtls_rsa_init(&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,
|
const bool key_ok = mbedtls_rsa_import_raw(&rsa, block + OFFSET_MODULUS, RSA_3072_BYTES, nullptr, 0, nullptr, 0,
|
||||||
exponent_be, sizeof(exponent_be)) == 0 &&
|
nullptr, 0, exponent_be, sizeof(exponent_be)) == 0 &&
|
||||||
mbedtls_rsa_complete(&rsa) == 0 &&
|
mbedtls_rsa_complete(&rsa) == 0 &&
|
||||||
mbedtls_rsa_set_padding(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256) == 0;
|
mbedtls_rsa_set_padding(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256) == 0;
|
||||||
|
#endif
|
||||||
bool verified = false;
|
bool verified = false;
|
||||||
if (!key_ok) {
|
if (!key_ok) {
|
||||||
// A setup/allocation failure (e.g. OOM right after the download) is not a
|
// 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".
|
// signature mismatch -- log it distinctly so it isn't read as "wrong key".
|
||||||
OTA_IDF_SIG_LOG(ESP_LOGE, "RSA key setup failed");
|
OTA_IDF_SIG_LOG(ESP_LOGE, "RSA key setup failed");
|
||||||
} else {
|
} 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 =
|
verified =
|
||||||
mbedtls_rsa_rsassa_pss_verify(&rsa, MBEDTLS_MD_SHA256, SHA256_BYTES, digest, block + OFFSET_SIGNATURE) == 0;
|
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);
|
mbedtls_rsa_free(&rsa);
|
||||||
|
#endif
|
||||||
return verified;
|
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