From b5b7e8f72e09d527cd474546199878eac4228aeb Mon Sep 17 00:00:00 2001 From: Filipe Cavalcanti Date: Fri, 27 Mar 2026 11:20:54 -0300 Subject: [PATCH] arch/espressif: add flash encryption support on CMake Add support for burning flash encryption E-Fuses on target. Signed-off-by: Filipe Cavalcanti --- .../src/common/espressif/CMakeLists.txt | 15 ++- tools/espressif/burn_flash_enc_key.py | 124 ++++++++++++++++++ tools/espressif/espressif_burn_enc_key.cmake | 95 ++++++++++++++ .../espressif/espressif_esptool_common.cmake | 41 ++++-- tools/espressif/espressif_mkimage.cmake | 93 +++++++++++++ 5 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 tools/espressif/burn_flash_enc_key.py create mode 100644 tools/espressif/espressif_burn_enc_key.cmake diff --git a/arch/risc-v/src/common/espressif/CMakeLists.txt b/arch/risc-v/src/common/espressif/CMakeLists.txt index aac5d5c1fbf..ed18f13a1d4 100644 --- a/arch/risc-v/src/common/espressif/CMakeLists.txt +++ b/arch/risc-v/src/common/espressif/CMakeLists.txt @@ -65,12 +65,6 @@ if(CONFIG_ESPRESSIF_HR_TIMER) endif() if(CONFIG_ESPRESSIF_EFUSE) - if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_ENABLED) - message( - WARNING - "Flash Encryption is not supported on CMake. Use Make to build the image instead." - ) - endif() list(APPEND SRCS esp_efuse.c) endif() @@ -424,6 +418,15 @@ add_custom_command( COMMENT "Generating ESP-compatible binary" VERBATIM) +# Flash encryption: burn key to eFuses (similar to tools/espressif/Config.mk +# BURN_EFUSES). Build: ESPTOOL_PORT=/dev/ttyUSB0 NOCHECK=1 cmake --build +# -t burn_enc_key (NOCHECK is required by burn_flash_enc_key.py; same +# idea as make NOCHECK.) + +if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_ENABLED) + include(${NUTTX_DIR}/tools/espressif/espressif_burn_enc_key.cmake) +endif() + # ############################################################################## # Flash operation Usage: ESPTOOL_PORT=/dev/ttyUSBx cmake --build # --target flash diff --git a/tools/espressif/burn_flash_enc_key.py b/tools/espressif/burn_flash_enc_key.py new file mode 100644 index 00000000000..00340d125af --- /dev/null +++ b/tools/espressif/burn_flash_enc_key.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# tools/espressif/burn_flash_enc_key.py +# +# SPDX-License-Identifier: Apache-2.0 +# +# Burn the flash encryption key into hardware eFuses (same role as +# tools/espressif/Config.mk BURN_EFUSES + espefuse.py burn_key). +# +# Invocation +# ---------- +# Normally you do not run this script by hand. The CMake target ``burn_enc_key`` +# (arch/risc-v/src/common/espressif/CMakeLists.txt) runs: +# +# cmake -E env NUTTX_ESPEFUSE= NUTTX_KEY= -- python3 "$0" +# +# The serial port is not passed by CMake: set ESPTOOL_PORT in the environment +# when you run the build, e.g.: +# +# ESPTOOL_PORT=/dev/ttyUSB0 NOCHECK=1 cmake --build -t burn_enc_key +# +# Environment variables +# --------------------- +# Required (set by CMake via -E env): +# NUTTX_ESPEFUSE Absolute path to espefuse.py (from find_program at configure time). +# NUTTX_KEY Absolute path to the XTS_AES_128 flash encryption key (.bin). +# +# Required (your shell / build environment): +# ESPTOOL_PORT Serial device for espefuse.py -p (e.g. /dev/ttyUSB0). +# +# Required (safety gate; same idea as ``make NOCHECK`` in Config.mk): +# NOCHECK Must be set (any value) so non-interactive burn is explicit. +# If unset, the script exits with an error before calling espefuse. +# +# Behaviour +# --------- +# 1. Runs ``espefuse.py --port $ESPTOOL_PORT summary``; fails if that command fails. +# 2. If the summary shows ``?? ??`` in the BLOCK1 region, the key is treated as +# already programmed and the script exits 0 without burning again. +# 3. Otherwise runs ``burn_key BLOCK_KEY0 XTS_AES_128_KEY``, always with +# ``--do-not-confirm`` when NOCHECK is set (as required above). +# +# Exit status: 0 on skip (already burned) or successful burn; non-zero on error. + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _efuse_summary_lines(summary_out: str) -> str: + """Text from espefuse summary corresponding to ``grep -A1 BLOCK1``.""" + + chunks: list[str] = [] + lines = summary_out.splitlines() + for i, line in enumerate(lines): + if "BLOCK1" in line: + chunks.append(line) + if i + 1 < len(lines): + chunks.append(lines[i + 1]) + return "\n".join(chunks) + + +def main() -> int: + for var in ("NUTTX_ESPEFUSE", "ESPTOOL_PORT", "NUTTX_KEY"): + if var not in os.environ or not os.environ[var]: + print( + f"Error: {var} is not set or empty.", + file=sys.stderr, + ) + return 1 + + if "NOCHECK" not in os.environ: + print( + "Error: NOCHECK is not set; refusing to run without explicit confirmation bypass.", + file=sys.stderr, + ) + print( + "Set NOCHECK=1 (or NOCHECK with any value) to proceed non-interactively.", + file=sys.stderr, + ) + return 1 + + espefuse = os.environ["NUTTX_ESPEFUSE"] + port = os.environ["ESPTOOL_PORT"] + key_path = os.environ["NUTTX_KEY"] + + proc = subprocess.run( + [espefuse, "--port", port, "summary"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + summary_out = proc.stdout or "" + if proc.returncode != 0: + print( + f"espefuse.py summary failed for {port}: {summary_out}", + file=sys.stderr, + ) + return proc.returncode + + efuse_summary = _efuse_summary_lines(summary_out) + if "?? ??" in efuse_summary: + print("Encryption key already burned. Skipping...") + return 0 + + print("Burning flash encryption key...") + burn = subprocess.run( + [ + espefuse, + "--do-not-confirm", + "--port", + port, + "burn_key", + "BLOCK_KEY0", + key_path, + "XTS_AES_128_KEY", + ], + ) + return burn.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/espressif/espressif_burn_enc_key.cmake b/tools/espressif/espressif_burn_enc_key.cmake new file mode 100644 index 00000000000..54864d5d479 --- /dev/null +++ b/tools/espressif/espressif_burn_enc_key.cmake @@ -0,0 +1,95 @@ +# ############################################################################## +# tools/espressif/espressif_burn_enc_key.cmake +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## +# +# Include() from the main NuttX CMake configure when +# CONFIG_ESPRESSIF_SECURE_FLASH_ENC_ENABLED. Defines the ``burn_enc_key`` target +# (no cmake -P). Expects NUTTX_DIR, CMAKE_BINARY_DIR, CMAKE_SOURCE_DIR. +# +# Serial port: ESPTOOL_PORT in the environment at build time (e.g. +# ESPTOOL_PORT=/dev/ttyUSB0). NOCHECK must also be set so the burn is explicit +# (see burn_flash_enc_key.py). +# +# ############################################################################## + +find_program(ESPEFUSE espefuse espefuse.py) +if(NOT ESPEFUSE) + message(FATAL_ERROR "espefuse.py not found (required for burn_enc_key)") +endif() + +find_program(PYTHON3 python3) +if(NOT PYTHON3) + message(FATAL_ERROR "python3 not found (required for burn_enc_key)") +endif() + +set(BINARY_DIR "${CMAKE_BINARY_DIR}") +set(SOURCE_DIR "${CMAKE_SOURCE_DIR}") +include(${NUTTX_DIR}/tools/espressif/espressif_esptool_common.cmake) + +if(NOT EXISTS "${FLASH_ENC_KEY_PATH}") + message( + FATAL_ERROR + "burn_enc_key: flash encryption key file missing. " + "Generate the encryption key using: espsecure.py generate_flash_encryption_key " + ) +endif() + +if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_FLASH_DEVICE_ENCRYPTED) + add_custom_target( + burn_enc_key + COMMAND + ${CMAKE_COMMAND} -E echo + "burn_enc_key: device already encrypted (Kconfig); skipping E-Fuse burn." + VERBATIM) +elseif(CONFIG_ESPRESSIF_EFUSE_VIRTUAL) + add_custom_target( + burn_enc_key + COMMAND + ${CMAKE_COMMAND} -E echo + "burn_enc_key: virtual E-Fuses enabled (Kconfig); skipping E-Fuse burn." + VERBATIM) +else() + if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_USE_HOST_KEY) + set(_nuttx_burn_key_msg + "Using host key: ${CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME}") + else() + set(_nuttx_burn_key_msg + "Using randomly generated key flow (see Kconfig / documentation).") + endif() + + add_custom_target( + burn_enc_key + COMMAND + bash -c + "if [ -z \"$$ESPTOOL_PORT\" ]; then echo 'burn_enc_key: ESPTOOL_PORT is not set. Example: ESPTOOL_PORT=/dev/ttyUSB0 cmake --build . -t burn_enc_key'; exit 1; fi" + COMMAND ${CMAKE_COMMAND} -E echo "${_nuttx_burn_key_msg}" + COMMAND + ${CMAKE_COMMAND} -E echo + "This operation is NOT REVERSIBLE. See flash encryption documentation." + COMMAND + ${CMAKE_COMMAND} -E env "NUTTX_ESPEFUSE=${ESPEFUSE}" + "NUTTX_KEY=${FLASH_ENC_KEY_PATH}" -- ${PYTHON3} + "${NUTTX_DIR}/tools/espressif/burn_flash_enc_key.py" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Burn flash encryption key to eFuses (espefuse.py)" + VERBATIM) + unset(_nuttx_burn_key_msg) +endif() diff --git a/tools/espressif/espressif_esptool_common.cmake b/tools/espressif/espressif_esptool_common.cmake index c16dcba335f..60c3d0a2086 100644 --- a/tools/espressif/espressif_esptool_common.cmake +++ b/tools/espressif/espressif_esptool_common.cmake @@ -25,7 +25,8 @@ # Shared Espressif + esptool layout and flash parameters for CMake scripts. # # Prerequisites (callers must do this first): - include(nuttx_kconfig.cmake) - -# nuttx_export_kconfig(${BINARY_DIR}/.config) +# nuttx_export_kconfig(${DOTCONFIG}) - BINARY_DIR and SOURCE_DIR set (for +# FLASH_ENC_KEY_PATH) # # Defines set by including this file: # CHIP_SERIES - Chip string for esptool -c (from CONFIG_ESPRESSIF_CHIP_SERIES) @@ -36,6 +37,8 @@ # FLASH_SIZE - Flash size (e.g. 2MB, 4MB), for merge_bin/elf2image -fs # FLASH_MODE - Flash mode (dio, dout, qio, qout), for elf2image -fm only # FLASH_FREQ - Flash speed (e.g. 40m), for elf2image -ff/write_flash -ff +# FLASH_ENC_KEY_PATH - Resolved path to CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME +# (empty if unset; relative paths checked vs BINARY_DIR then SOURCE_DIR) # # ############################################################################## # cmake-format: on @@ -66,13 +69,15 @@ else() endif() set(EFUSE_OFFSET ${EFUSE_FLASH_OFFSET}) -# MCUboot application slot (Config.mk APP_OFFSET) -if(CONFIG_ESPRESSIF_ESPTOOL_TARGET_PRIMARY) - set(MCUBOOT_APP_OFFSET ${CONFIG_ESPRESSIF_OTA_PRIMARY_SLOT_OFFSET}) -elseif(CONFIG_ESPRESSIF_ESPTOOL_TARGET_SECONDARY) - set(MCUBOOT_APP_OFFSET ${CONFIG_ESPRESSIF_OTA_SECONDARY_SLOT_OFFSET}) -else() - message(FATAL_ERROR "Missing MCUBoot slot target: PRIMARY or SECONDARY") +# MCUboot application slot (Config.mk APP_OFFSET); default when not MCUboot boot +if(CONFIG_ESPRESSIF_BOOTLOADER_MCUBOOT) + if(CONFIG_ESPRESSIF_ESPTOOL_TARGET_PRIMARY) + set(MCUBOOT_APP_OFFSET ${CONFIG_ESPRESSIF_OTA_PRIMARY_SLOT_OFFSET}) + elseif(CONFIG_ESPRESSIF_ESPTOOL_TARGET_SECONDARY) + set(MCUBOOT_APP_OFFSET ${CONFIG_ESPRESSIF_OTA_SECONDARY_SLOT_OFFSET}) + else() + message(FATAL_ERROR "Missing MCUBoot slot target: PRIMARY or SECONDARY") + endif() endif() # Flash capacity (merge_bin --fill-flash-size, elf2image -fs) @@ -109,3 +114,23 @@ if(DEFINED CONFIG_ESPRESSIF_FLASH_FREQ) else() set(FLASH_FREQ "40m") endif() + +# Host flash encryption key file (Config.mk / espressif_mkimage FLASH_ENC) +if((NOT DEFINED BINARY_DIR) OR (NOT DEFINED SOURCE_DIR)) + message( + FATAL_ERROR + "espressif_esptool_common.cmake: BINARY_DIR and SOURCE_DIR must be set before include()" + ) +endif() + +set(FLASH_ENC_KEY_PATH "") +if(DEFINED CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME) + set(FLASH_ENC_KEY_PATH "${CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME}") + if(NOT IS_ABSOLUTE "${FLASH_ENC_KEY_PATH}") + if(EXISTS "${BINARY_DIR}/${FLASH_ENC_KEY_PATH}") + set(FLASH_ENC_KEY_PATH "${BINARY_DIR}/${FLASH_ENC_KEY_PATH}") + elseif(EXISTS "${SOURCE_DIR}/${FLASH_ENC_KEY_PATH}") + set(FLASH_ENC_KEY_PATH "${SOURCE_DIR}/${FLASH_ENC_KEY_PATH}") + endif() + endif() +endif() diff --git a/tools/espressif/espressif_mkimage.cmake b/tools/espressif/espressif_mkimage.cmake index 66797c48363..9befb06aeb4 100644 --- a/tools/espressif/espressif_mkimage.cmake +++ b/tools/espressif/espressif_mkimage.cmake @@ -187,6 +187,90 @@ endif() file(APPEND "${BINARY_DIR}/nuttx.manifest" "nuttx.bin\n") +# ############################################################################## +# Flash encryption (matches tools/espressif/Config.mk FLASH_ENC + ENC_APP) +# ############################################################################## + +if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_ENABLED) + message(STATUS "Flash Encryption is enabled!") + find_program(ESPSECURE espsecure espsecure.py) + + if(CONFIG_ESPRESSIF_EFUSE_VIRTUAL) + message(WARNING "Virtual E-Fuses are enabled! E-Fuses will not be burned.") + endif() + + if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_USE_HOST_KEY) + if(NOT EXISTS "${FLASH_ENC_KEY_PATH}") + message( + FATAL_ERROR + "FLASH ENCRYPTION error: Key file '${CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME}' not found.\n" + "Generate the encryption key using: espsecure.py generate_flash_encryption_key \n" + "Refer to the documentation on flash encryption before proceeding.") + endif() + endif() + + if(CONFIG_ESPRESSIF_SPIFLASH) + message(STATUS "Applying encryption to user MTD partition on flash.") + if(NOT EXISTS "${FLASH_ENC_KEY_PATH}") + message( + FATAL_ERROR + "Flash encryption key is required for user MTD partition encryption. Key file: '${CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME}'\n" + "Make sure CONFIG_ESPRESSIF_SECURE_FLASH_ENC_HOST_KEY_NAME is set or disable SPI Flash." + ) + endif() + if(NOT ESPSECURE) + message( + FATAL_ERROR "espsecure.py not found - cannot encrypt MTD partition") + endif() + if(NOT DEFINED CONFIG_ESPRESSIF_STORAGE_MTD_SIZE) + message( + FATAL_ERROR + "CONFIG_ESPRESSIF_STORAGE_MTD_SIZE is required for SPI Flash + flash encryption" + ) + endif() + if(NOT DEFINED CONFIG_ESPRESSIF_STORAGE_MTD_OFFSET) + message( + FATAL_ERROR + "CONFIG_ESPRESSIF_STORAGE_MTD_OFFSET is required for SPI Flash + flash encryption" + ) + endif() + + math(EXPR MTD_SIZE_INT "${CONFIG_ESPRESSIF_STORAGE_MTD_SIZE}") + message( + STATUS + "Encrypting user MTD partition offset: ${CONFIG_ESPRESSIF_STORAGE_MTD_OFFSET}, size: ${CONFIG_ESPRESSIF_STORAGE_MTD_SIZE} (${MTD_SIZE_INT})" + ) + + execute_process( + COMMAND + ${CMAKE_COMMAND} -E env LC_ALL=C sh -c + "dd if=/dev/zero ibs=1 count=${MTD_SIZE_INT} 2>/dev/null | tr '\\000' '\\377' > blank_mtd.bin" + RESULT_VARIABLE BLANK_MTD_RESULT + WORKING_DIRECTORY ${BINARY_DIR}) + + if(NOT BLANK_MTD_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to create blank_mtd.bin for MTD encryption") + endif() + + execute_process( + COMMAND + ${ESPSECURE} encrypt_flash_data --aes_xts --keyfile + ${FLASH_ENC_KEY_PATH} --address 0 --output ${BINARY_DIR}/enc_mtd.bin + ${BINARY_DIR}/blank_mtd.bin + RESULT_VARIABLE ENC_MTD_RESULT + WORKING_DIRECTORY ${BINARY_DIR}) + + if(NOT ENC_MTD_RESULT EQUAL 0) + file(REMOVE "${BINARY_DIR}/blank_mtd.bin") + message(FATAL_ERROR "espsecure.py encrypt_flash_data failed") + endif() + + file(REMOVE "${BINARY_DIR}/blank_mtd.bin") + + message(STATUS "Generated: enc_mtd.bin (encrypted user MTD placeholder)") + endif() +endif() + # ############################################################################## # Merge binaries (optional) # ############################################################################## @@ -226,6 +310,15 @@ if(CONFIG_ESPRESSIF_MERGE_BINS) message( STATUS "Merge bin: ${MCUBOOT_APP_OFFSET} -> ${BINARY_DIR}/nuttx.bin") + if(CONFIG_ESPRESSIF_SECURE_FLASH_ENC_ENABLED AND CONFIG_ESPRESSIF_SPIFLASH) + list(APPEND ESPTOOL_BINS ${CONFIG_ESPRESSIF_STORAGE_MTD_OFFSET} + "${BINARY_DIR}/enc_mtd.bin") + message( + STATUS + "Merge bin: ${CONFIG_ESPRESSIF_STORAGE_MTD_OFFSET} -> ${BINARY_DIR}/enc_mtd.bin" + ) + endif() + elseif(CONFIG_ESPRESSIF_SIMPLE_BOOT) # Simple boot: same base offset as BL_OFFSET (0x2000 on ESP32-P4, else 0x0) list(APPEND ESPTOOL_BINS ${BL_OFFSET} "${BINARY_DIR}/nuttx.bin")