Skip to content

feat(h563-boot): secure boot (RSA-2048 + TRNG seed + anti-rollback), vendor LL drivers, robustness#93

Merged
w1ne merged 6 commits into
developfrom
feat/h563-secure-boot
Jun 22, 2026
Merged

feat(h563-boot): secure boot (RSA-2048 + TRNG seed + anti-rollback), vendor LL drivers, robustness#93
w1ne merged 6 commits into
developfrom
feat/h563-secure-boot

Conversation

@w1ne

@w1ne w1ne commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Modernizes and hardens the STM32H563 UDS OTA bootloader example (examples/h563_uds_bootloader/), addressing the authenticity, replay, and robustness gaps from an adversarial review and moving the whole bootloader onto ST vendor drivers. Each change is verified against a faithful simulator and re-checked on a real NUCLEO-H563ZI over SWD.

Secure boot (authenticity)

  • RSA-2048 PKCS#1 v1.5 + SHA-256 image signature, verified by the bootloader against a public key baked into the image, in addition to the existing CRC-32 (defense in depth). CRC alone was forgeable; now an image must be signed by the matching private key to validate, activate, or boot. Enforced in app_is_valid (boot), 0xFF01 CheckProgramming, and re-checked in 0xFF02 ActivateSoftware (closes the activate-without-revalidation hole). mkimage.py signs; demo keys are clearly marked DEMO (private key offline/HSM in production). RSA-2048 verify (~hundreds of k instructions) was chosen over ECDSA-P256 because EC verify does not fit the simulator's instruction budget and is far heavier on the MCU.
  • TRNG SecurityAccess seed. The 0x27 AES-CMAC challenge seed is now a fresh per-attempt nonce from the H563 hardware RNG, so a captured (seed, key) pair cannot be replayed. (Previously a fixed constant.)
  • Configurable anti-rollback. Activating an image whose version is older than the active app's is rejected when OTA_ANTIROLLBACK_ENFORCE is on (default on; overridable). The version-decision is a pure, host-tested function.

Test integrity

  • The in-simulator tester now computes the AES-CMAC key live from the received seed instead of shipping a baked constant, and the block-sequence-counter is enforced. The image-validation host test now exercises the real shared validator (including the live RSA path) instead of a reimplemented copy.

Vendor drivers (no hand-coded MMIO)

  • The whole bootloader now sources every peripheral register from ST's CMSIS device header stm32h563xx.h (FLASH->, FDCAN1->, RNG, SysTick) and the STM32H5 LL drivers — no hand-typed addresses remain. This eliminates the class of bug that hand-coded MMIO caused (a wrong RNG base, a wrong flash key-register offset). Vendor sources are referenced via Makefile variables under the same workspace already used for mbedTLS; the firmware keeps the HSI reset clock (no PLL bring-up).

Robustness

  • Flash driver checks NSSR error flags (clears via NSCCR) and returns failures; every BSY wait is bounded; RequestDownload rejects misaligned base addresses. Flash erase/program routines run from SRAM (read-while-write safe). Boot-state is torn-write safe (pre-erased sector + atomic single-quad-word program, no-erase attempt counter) and confirms the correct physical bank. Real SysTick 1 ms time base so the SecurityAccess lockout is real wall-clock time. Host Makefile -Werror; sane default paths.

Verified: both firmware ELFs build clean under -Werror; host suites (CMAC, RSA, image via the real validator, boot-state incl. torn-write, version) pass; the end-to-end simulation smoke reaches APP-B v2 (14/14) with the simulator's H5 flash program-error and read-while-write fidelity gates enabled; and on real silicon the bootloader boots, RSA-verifies a signed App-A, and jumps with no fault.

Relates to #64.

w1ne added 6 commits June 23, 2026 00:00
Gate the OTA app image by an ECDSA-P256 signature in addition to the
existing CRC-32, so only images signed with the matching private key boot.
The CRC is kept as a fast integrity pre-check (defense in depth).

Scheme: secp256r1, SHA-256 over the payload bytes, raw 64-byte r||s
appended after the payload (offset derived from image_size, no new header
field). The device verifies with mbedtls_ecdsa_verify on raw r,s MPIs
against a public key baked into the bootloader -- no ASN.1/DER on target.

- mkimage.py: sign the payload (Python cryptography, DER->raw r||s) and
  append the 64-byte signature; add --key and --tamper-signature flags.
- sec_ecdsa.c/.h: ecdsa_verify_p256() helper over mbedTLS.
- app_jump.c app_is_valid(): verify the signature after the CRC -- the
  single chokepoint gating boot, 0xFF01, 0xFF02 and 0xFF03.
- 0xFF01 CheckProgramming: require header+payload+signature transferred.
- 0xFF02 ActivateSoftware: re-validate the inactive image (CRC+signature)
  right before the bank swap, closing the activate-without-revalidation hole.
- bootloader build: add mbedTLS ecp/ecp_curves/ecdsa/bignum/bignum_core/
  sha256/asn1parse/asn1write and the matching config (ECP/ECDSA/SHA256/
  secp256r1). Both ELFs build clean under -Werror, --specs=nano.specs.
- Demo apps: re-sign A/B/Bbad; add a Bsigbad variant (CRC-good but
  signature-bad) plus an all-four target for negative secure-boot tests.
- sim_tester: regenerate app_b_image_blob.h from the signed App-B image.
- host ecdsa-test: verify the signed image and prove payload/signature
  tamper is rejected, independent of the simulator.

DEMO keys: app/signing_key_dev.pem (private) and image_pubkey.h (public)
are committed for the example only; a real product keeps the private key
in an HSM/offline and bakes only the public half.
Replace the ECDSA-P256 secure-boot scheme with RSA-2048 PKCS#1 v1.5 over
SHA-256 of the app payload. RSA verify (m^65537 mod n) is ~100x cheaper than
an ECDSA-P256 verify and completes well under the simulators 50M-instruction
cap, which the ECDSA path exceeded.

- sec_rsa.c/.h: rsa_verify_sha256() via mbedtls_rsa_pkcs1_verify against the
  baked public key (modulus n + e=65537); replaces sec_ecdsa.c/.h.
- Signature is 256 raw big-endian bytes appended after the payload; CRC-32 is
  kept as a fast integrity pre-check (defense in depth). OTA_IMAGE_SIG_SIZE=256.
- Enforcement unchanged: app_is_valid() gates boot, 0xFF01 CheckProgramming,
  0xFF02 ActivateSoftware re-validation, and 0xFF03 PerformRollback.
- mbedTLS module set switched from ECP/ECDSA to RSA: rsa, rsa_alt_helpers,
  bignum, bignum_core, sha256, md, oid (+ asn1parse/asn1write pulled in
  transitively by rsa.c key-parse/DigestInfo helpers); dropped ecp, ecp_curves,
  ecdsa. Config enables RSA_C, PKCS1_V15, OID_C, MD_C, SHA256_C.
- mkimage.py signs with the RSA-2048 dev key (PKCS#1 v1.5 + SHA-256).
- image_pubkey.h baked public key + signing_key_dev.pem regenerated as RSA-2048
  (DEMO keys; a real product keeps the private key offline/HSM).
- sim_tester/app_b_image_blob.h regenerated as the RSA-signed App-B image.
- Host rsa-test replaces ecdsa-test (verifies signed image; rejects payload
  tamper and a CRC-good bad-signature image).

Both firmware ELFs build clean under -Werror --specs=nano.specs; all host
suites (rsa, cmac, image, bootstate) pass.
…ounter

Sim tester no longer bakes the SecurityAccess key. It reads the seed
from the server's 0x67 01 response and computes AES-128-CMAC(secret,
seed) live via the same aes_cmac one-shot the bootloader verifies with.
sim_tester_init now stores the shared secret instead of discarding it.

bl_transfer_data enforces the ISO-14229 14.3 block-sequence-counter:
expected starts at 0x01 after RequestDownload, increments per accepted
block and wraps 0xFF -> 0x00. Matching counter is programmed, the
previous counter is ACKed without re-writing (retransmission), any
other value returns NRC 0x73 wrongBlockSequenceCounter.

flash_tool host BSC wrap corrected to 0xFF -> 0x00 (was 0x01).

sec_cmac_test gains a demo-credential vector asserting the live key
equals the known-good AES-CMAC(secret, seed).
Draw the 0x27 SecurityAccess seed from the H563 hardware TRNG (RNG block
@0x40C80800: enable RCC AHB2 RNGEN + RNG_CR.RNGEN, then poll RNG_SR.DRDY
and read RNG_DR for each 32-bit word) instead of a fixed DEMO_SEED, making
the AES-CMAC challenge a fresh per-attempt nonce that cannot be replayed.
The DRDY poll is bounded so a stuck/unclocked RNG returns conditionsNotCorrect
rather than hanging the bootloader. DEMO_SEED removed; the sim tester already
reads the seed live from the 0x67 01 response.

Add configurable monotonic anti-rollback: OTA_ANTIROLLBACK_ENFORCE (default 1,
override -DOTA_ANTIROLLBACK_ENFORCE=0). The floor is the currently-active
app's header version; activating an image with a lower version is rejected
(NRC 0x22, milestone "BL: rollback blocked") at 0xFF01 CheckProgramming and
re-checked at 0xFF02 ActivateSoftware. Recovery (no valid active image) skips
enforcement so it is never bricked. Comparison is a pure host-testable
function ota_version_allows(); adds ota_version_test.c (upgrade/equal allowed,
downgrade rejected when enforced, downgrade allowed when not) wired as the
otaversion-test make target.
Extract the pure OTA image validation core into image_validate.c/.h
(magic, size range, region-fit, CRC-32, RSA-2048 signature, initial-SP-in-RAM),
free of any MCU asm or flash I/O. app_is_valid() in app_jump.c is now a thin
wrapper that calls image_validate() over the bank's memory-mapped app region;
app_jump.c keeps only the SCB/MSP/asm jump. app_image_test.c drops its forked
image_buf_is_valid() and exercises the SAME image_validate() against real
signed image buffers (app_b_image.bin, app_bsigbad_image.bin), linking the
mbedTLS RSA/SHA modules so the signature path runs on the host too.

Minor hardening:
- host/Makefile: add -Werror; document that -lmbedcrypto comes from the
  libmbedtls-dev package; reconcile README wording.
- bootloader/Makefile: replace the machine-specific UDSLIB_DIR scratch default
  with $(abspath $(CURDIR)/../../..) so a fresh checkout builds with no
  override; check-udslib still validates the resolved path.
- main.c: document the deliberate no-security read of DID 0xF1A0 (active-bank
  indicator) as a diagnostic/orchestration aid that leaks no secret.
…coded MMIO

Replace hand-typed REG32 register addresses across the H563 UDS OTA
bootloader with ST CMSIS device-header instances (FLASH, FDCAN1, USART3,
RNG, SysTick) and STM32H5 LL helpers for RNG. Hand-typed addresses had
caused real bugs (wrong RNG base, wrong flash key offset); all bases and
bit fields now come from stm32h563xx.h and the LL headers.

- flash_h5.c/.h: FLASH_TypeDef + FLASH_CR_/SR_/CCR_/OPTCR_/OPTSR_ macros;
  device-header include guarded on __arm__ so host bootstate-test (stubbed
  flash I/O) stays register-header-free.
- fdcan.c: FDCAN1/USART3 instances + bit macros; message RAM via SRAMCAN_BASE.
- main.c: RNG via LL (clock enable, enable, DRDY poll, read); SysTick via
  CMSIS SysTick_Config. Same 1 ms tick, HSI reset clock unchanged.
- boot_confirm.c (app): FLASH_TypeDef + bit macros; rebuilt images identical.
- Makefiles: locate CMSIS/Cube under the zephyr workspace via CUBE_DIR/
  CMSIS_DIR (same pattern as MBEDTLS_DIR); -DSTM32H563xx, -DUSE_FULL_LL_DRIVER.

Behavior identical: flash sequences, UART milestones, and reset clock
unchanged. Both ELFs build clean under -Werror; all host tests pass; the
cross-repo sim smoke reaches APP-B v2 (14/14 assertions).
@w1ne w1ne merged commit 38fa275 into develop Jun 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant