ESP32-S3 Secure Boot V2 + Flash Encryption

EmbeddedIoT

ESP32-S3 양산 펌웨어에 보안을 적용하려면 Secure Boot V2와 Flash Encryption을 동시에 활성화해야 한다. 하나만 걸면 나머지 절반이 뚫린다.

하나만 적용하면 생기는 구멍

Flash Encryption만 적용 (Secure Boot 없음)

플래시 내용이 XTS-AES로 암호화되어 읽을 수 없다. 그러나 서명 검증이 없으므로 공격자가 자신의 코드를 플래시에 쓸 수 있다. ESP32-S3의 플래시 컨트롤러가 쓰기 시점에 자동으로 암호화해주기 때문에, 공격자의 평문 코드도 정상 암호화된 상태로 저장된다. 다음 부팅 시 그대로 실행된다.

기밀성은 있지만 무결성이 없다.

Secure Boot만 적용 (Flash Encryption 없음)

부트로더와 앱 이미지의 RSA-PSS 서명을 검증하므로, 변조된 펌웨어는 실행되지 않는다. 그러나 플래시가 평문이므로 물리적 덤프로 펌웨어 바이너리를 그대로 읽을 수 있다. 통신 프로토콜, 하드코딩된 인증 정보, 알고리즘이 모두 노출된다.

무결성은 있지만 기밀성이 없다.

둘 다 적용

공격Secure BootFlash Encryption둘 다
펌웨어 읽기 (IP 유출)XOO
펌웨어 변조 (악성 코드 주입)OXO
플래시 교체 후 부팅OXO
TOCTOU (검증 후 교체)XOO

각각의 메커니즘

Secure Boot V2

ROM 부트로더(실리콘에 고정) → 2nd stage 부트로더 서명 검증 → 앱 이미지 서명 검증. 체인이 끊기면 부팅이 멈춘다.

+------------------+     서명 검증     +------------------+     서명 검증     +--------+
|  ROM Bootloader  | ───────────────> |  2nd Bootloader  | ───────────────> |  App   |
|  (실리콘 고정)    |                  |  (RSA-PSS 서명)   |                  | (서명)  |
+------------------+                  +------------------+                  +--------+
        |
   eFuse에 저장된 공개키 다이제스트(SHA-256)로 검증
  • 알고리즘: RSA-3072 + PSS 패딩 (ESP32-S3 기준. ESP32-C 시리즈는 ECDSA 지원)
  • 서명 블록(1216바이트)이 이미지 끝에 붙는다. 최대 3개까지 가능
  • eFuse에는 공개키의 SHA-256 다이제스트(32바이트)를 저장한다
  • SECURE_BOOT_EN eFuse를 1로 굽는 순간 활성화. 비가역

Flash Encryption

플래시 컨트롤러가 읽기 시 자동 복호화, 쓰기 시 자동 암호화한다. 키는 소프트웨어에 노출되지 않는다.

  • 알고리즘: XTS-AES. 플래시 물리 주소를 tweak으로 사용하므로, 같은 평문이라도 주소가 다르면 다른 암호문이 된다. 블록 스왑 공격 방어
  • 키 크기: XTS-AES-128(256-bit 키, eFuse 1블록) 또는 XTS-AES-256(512-bit 키, eFuse 2블록)
  • SPI_BOOT_CRYPT_CNT eFuse(3-bit)로 제어:
    • 0b001: 암호화 활성(Development — 토글 가능)
    • 0b111: 암호화 영구 활성(Release — 더 이상 변경 불가)
  • 첫 부팅 시 부트로더가 플래시를 in-place 암호화한다. 이 과정에서 전원이 끊기면 브릭된다

eFuse — 되돌릴 수 없는 비트

eFuse는 0→1 단방향이다. 한 번 1로 굽힌 비트는 영원히 1이다. 팩토리 리셋이 없다.

ESP32-S3의 키 블록 할당 예시 (XTS-AES-256 사용 시):

eFuse 블록KEY_PURPOSE용도
BLOCK_KEY0SECURE_BOOT_DIGEST0Secure Boot 공개키 다이제스트
BLOCK_KEY1XTS_AES_256_KEY_1Flash Encryption 키 전반부
BLOCK_KEY2XTS_AES_256_KEY_2Flash Encryption 키 후반부

총 6개 키 블록(KEY0KEY5)이 있다. Flash Encryption(AES-256)에 2개, Secure Boot에 13개, NVS Encryption에 1개를 쓰면 금방 소진된다.

잘못된 키를 구우면:

  • 잘못된 Flash Encryption 키 → 플래시를 복호화할 수 없다 → 영구 브릭
  • 잘못된 Secure Boot 다이제스트 → 모든 서명 검증 실패 → 영구 브릭
  • Secure Boot 다이제스트에 read-protect → ROM이 다이제스트를 읽지 못한다 → 영구 브릭

sdkconfig 설정

# Secure Boot V2
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
CONFIG_SECURE_BOOT_SIGNING_KEY="../keys/secure_boot_signing_key.pem"

# Flash Encryption
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y   # 개발 중에는 Development
CONFIG_SECURE_FLASH_ENCRYPTION_AES256=y
CONFIG_SECURE_FLASH_ENCRYPTION_KEYFILE="../keys/flash_encryption_key.bin"

# Bootloader offset — 서명 블록 때문에 부트로더 크기가 커진다
CONFIG_PARTITION_TABLE_OFFSET=0x10000

# App Rollback — OTA 실패 시 이전 파티션으로 복귀
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y

키 생성과 프로비저닝

1. 키 생성

# RSA-3072 서명 키 생성
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=3072,
)

# Flash Encryption 키 — 32바이트 랜덤
flash_key = secrets.token_bytes(32)

2. eFuse 프로그래밍 (Development Mode)

# Secure Boot 키 다이제스트 굽기
espefuse.py burn_key BLOCK_KEY0 secure_boot_key_digest.bin SECURE_BOOT_DIGEST0

# Flash Encryption 키 굽기 (XTS-AES-256 = 2블록)
espefuse.py burn_key BLOCK_KEY1 flash_encryption_key.bin XTS_AES_256_KEY_1
espefuse.py burn_key BLOCK_KEY2 flash_encryption_key.bin XTS_AES_256_KEY_2

# 활성화
espefuse.py burn_efuse SECURE_BOOT_EN 1
espefuse.py burn_efuse SPI_BOOT_CRYPT_CNT 1

3. Production Mode 추가 작업

# 키 쓰기 보호 (변경 불가)
espefuse.py write-protect-efuse BLOCK_KEY0
espefuse.py write-protect-efuse BLOCK_KEY1
espefuse.py write-protect-efuse BLOCK_KEY2

# Flash Encryption 키 읽기 보호 (소프트웨어에서 접근 불가)
espefuse.py read-protect-efuse BLOCK_KEY1
espefuse.py read-protect-efuse BLOCK_KEY2

# SPI_BOOT_CRYPT_CNT를 0b111로 — 영구 암호화
espefuse.py burn_efuse SPI_BOOT_CRYPT_CNT 0x7

Development vs Release Mode

DevelopmentRelease
SPI_BOOT_CRYPT_CNT0b001 (쓰기 보호 안 됨)0b111 (쓰기 보호됨)
UART 평문 플래싱idf.py encrypted-flash로 가능불가. 사전 암호화된 바이너리만
토글 횟수최대 3회 (001→011→111)0회
용도개발, 디버깅양산 출하

양산 디바이스라도 현장 유지보수가 필요하면 Development Mode + 키 보호 조합을 쓸 수 있다. UART 리플래시가 가능하되, 키 자체는 write/read-protect으로 보호한다.

메모

적용 순서가 중요하다. Flash Encryption을 먼저 활성화하고, 그 다음 Secure Boot을 건다. Secure Boot 활성화 시 RD_DIS eFuse 레지스터가 쓰기 보호된다. 그 상태에서 Flash Encryption 키의 read-protect을 설정하면 실패한다. 키가 소프트웨어에서 읽히는 채로 남는다.

미사용 Secure Boot 키 슬롯을 폐기(revoke)해야 한다. 3개 슬롯 중 1개만 쓰고 나머지를 비워두면, 물리적 접근 가능한 공격자가 빈 슬롯에 자신의 키 다이제스트를 굽고 자기 서명 펌웨어를 실행할 수 있다. SECURE_BOOT_KEY_REVOKE1, SECURE_BOOT_KEY_REVOKE2를 1로 굽는다.

NVS는 Flash Encryption으로 암호화되지 않는다. NVS 파티션에 WiFi 비밀번호나 API 키를 저장한다면, 별도로 NVS Encryption(HMAC 기반 키 파생)을 활성화해야 한다.

첫 부팅 in-place 암호화 중 전원 차단 = 브릭. 프로비저닝 라인에서 안정적인 전원을 확보하거나, 호스트에서 사전 암호화한 바이너리를 플래시하는 방식으로 회피한다.

ECDSA 서명에서 다뤘듯 서명 알고리즘의 보안은 nonce 관리에 달려 있다. Secure Boot V2의 RSA-PSS도 마찬가지로 패딩에 랜덤 솔트를 사용하며, 이 랜덤성이 서명의 안전성을 결정한다. 디지털 키 시스템 설계에서 다룬 BLE+UWB 인증 시스템에서도 차량 ECU의 Secure Boot + Flash Encryption은 기본 전제다.