Skip to content

Secure Boot and Flash Encryption

ESP32 chips have hardware-level security features controlled by eFuses — one-time-programmable bits burned into silicon. Once burned, they cannot be reversed. This guide covers assessing a device’s current security posture, enabling flash encryption, and understanding the secure boot workflow.

Before changing anything, audit the device to understand what is already configured.

esp_security_audit({
"port": "/dev/ttyUSB0"
})

The audit returns a structured report including:

  • Chip identity and chip ID
  • Security posture summary: flash encryption (enabled/disabled), secure boot (enabled/disabled), JTAG (enabled/disabled)
  • Security-relevant eFuses: FLASH_CRYPT_CNT, ABS_DONE_0, JTAG_DISABLE, and others

If the posture section shows everything as “disabled”, the device is in its factory default state with no security features active.

To inspect a specific eFuse value:

esp_efuse_read({
"port": "/dev/ttyUSB0",
"efuse_name": "FLASH_CRYPT_CNT"
})

Omit efuse_name to get the full eFuse summary.

Flash encryption prevents reading firmware from the flash chip. The process involves checking the current state, then burning the appropriate eFuses.

  1. Check current flash encryption status:

    esp_enable_flash_encryption({
    "port": "/dev/ttyUSB0"
    })

    If encryption is already enabled, the response reports the current state and no further action is needed. If disabled, it returns guidance on the required eFuse burns.

  2. On a QEMU instance (for testing), burn the encryption eFuses:

    esp_efuse_burn({
    "port": "socket://localhost:5555",
    "efuse_name": "FLASH_CRYPT_CNT",
    "value": "0x1"
    })
  3. Verify the change took effect:

    esp_efuse_read({
    "port": "socket://localhost:5555",
    "efuse_name": "FLASH_CRYPT_CNT"
    })

    The response shows value_before and value_after to confirm the burn.

Secure boot ensures only signed firmware can execute on the device. It is controlled by the ABS_DONE_0 eFuse.

  1. Run a security audit to confirm secure boot is not already enabled:

    esp_security_audit({
    "port": "/dev/ttyUSB0"
    })

    Look for secure_boot: "disabled" in the posture section.

  2. Build your firmware with secure boot enabled in the ESP-IDF menuconfig. This generates signing keys and embeds the public key in the bootloader.

  3. Flash the signed bootloader, partition table, and application using esp_flash_multi.

  4. Burn the secure boot eFuse to lock it in:

    esp_efuse_burn({
    "port": "/dev/ttyUSB0",
    "efuse_name": "ABS_DONE_0",
    "value": "0x1"
    })
  5. Optionally disable JTAG to prevent debug-port attacks:

    esp_efuse_burn({
    "port": "/dev/ttyUSB0",
    "efuse_name": "JTAG_DISABLE",
    "value": "0x1"
    })
eFuse NamePurposeReversible
FLASH_CRYPT_CNTControls flash encryption enable/disableNo
FLASH_CRYPT_CONFIGEncryption configuration bitsNo
ABS_DONE_0Enables secure boot v1No
ABS_DONE_1Enables secure boot v2 (ESP32-V3+)No
JTAG_DISABLEDisables JTAG debug interfaceNo
DISABLE_DL_ENCRYPTDisables flash encryption in download modeNo
DISABLE_DL_DECRYPTDisables flash decryption in download modeNo
DISABLE_DL_CACHEDisables flash cache in download modeNo

QEMU emulates eFuses in a file on disk. When you destroy and recreate a QEMU instance, the eFuse file is regenerated from defaults — effectively giving you a fresh, unburnished chip.

  1. Start a QEMU instance:

    esp_qemu_start({
    "chip_type": "esp32",
    "boot_mode": "download"
    })
  2. Run your entire security configuration workflow against the socket URI.

  3. Verify with esp_security_audit that the posture matches expectations.

  4. Stop and delete the instance. Start a fresh one to repeat the test or try a different configuration.

See the Security reference for full parameter details on esp_security_audit, esp_efuse_read, esp_efuse_burn, and esp_enable_flash_encryption.