diff --git a/esphome/components/ota/ota_rsa_der.h b/esphome/components/ota/ota_rsa_der.h new file mode 100644 index 00000000000..1ec4e3cc62f --- /dev/null +++ b/esphome/components/ota/ota_rsa_der.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +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(body_len >> 8); + out[i++] = static_cast(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(exponent_content_len); + if (pad) { + out[i++] = 0x00; + } + memcpy(out + i, exponent_be, exponent_len); + return total_len; +} + +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_signature_esp_idf.cpp b/esphome/components/ota/ota_signature_esp_idf.cpp index edee594bfe5..b327988d2d5 100644 --- a/esphome/components/ota/ota_signature_esp_idf.cpp +++ b/esphome/components/ota/ota_signature_esp_idf.cpp @@ -14,9 +14,20 @@ #include #include +#include +#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 +#else #include #include #include +#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(exponent_le >> 24), static_cast(exponent_le >> 16), static_cast(exponent_le >> 8), static_cast(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; } diff --git a/tests/components/ota/test_rsa_der.cpp b/tests/components/ota/test_rsa_der.cpp new file mode 100644 index 00000000000..aefce6769ae --- /dev/null +++ b/tests/components/ota/test_rsa_der.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include + +#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 make_modulus(uint8_t first = 0xC5) { + std::array 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 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