From 2e16500b43390ea11476f8e130cb1661c168ff49 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 29 Sep 2026 14:47:26 -0700 Subject: [PATCH 1/9] PolarFire SoC: M-mode scratchpad, DDR training, SBI and CI fixes --- .github/workflows/test-configs.yml | 7 + Makefile | 2 + arch.mk | 13 + config/examples/polarfire_mpfs250_m.config | 23 +- docs/Targets.md | 18 +- hal/mpfs250-m.ld | 30 +- hal/mpfs250.c | 10 +- hal/mpfs250.h | 35 +- hal/mpfs250_ddr.c | 433 +++++++++++---------- options.mk | 8 + src/boot_riscv.c | 18 +- src/boot_riscv_start.S | 59 ++- src/riscv_sbi.c | 214 +++++++--- 13 files changed, 565 insertions(+), 305 deletions(-) diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 1c87e31f96..361a08e064 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -514,6 +514,13 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub + microchip_mpfs250_m_mldsa_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 IMAGE_HEADER_SIZE=12288 WOLFBOOT_SECTOR_SIZE=0x4000 microchip_mpfs250_m_qspi_test: uses: ./.github/workflows/test-build-riscv.yml with: diff --git a/Makefile b/Makefile index e7cc4cc424..6ea253e5a3 100644 --- a/Makefile +++ b/Makefile @@ -791,6 +791,8 @@ $(LSCRIPT): $(LSCRIPT_IN) FORCE sed -e "s/@WOLFBOOT_L2LIM_SIZE@/$(WOLFBOOT_L2LIM_SIZE)/g" | \ sed -e "s/@L2SRAM_ADDR@/$(L2SRAM_ADDR)/g" | \ sed -e "s/@STACK_SIZE_PER_HART@/$(STACK_SIZE_PER_HART)/g" | \ + sed -e "s/@WOLFBOOT_L2SCRATCH_SIZE@/$(WOLFBOOT_L2SCRATCH_SIZE)/g" | \ + sed -e "s/@STACK_SIZE@/$(STACK_SIZE)/g" | \ sed -e 's/@WOLFHAL_FLASH_EXCLUDE_TEXT@/$(WOLFHAL_FLASH_EXCLUDE_TEXT)/g' | \ sed -e 's/@WOLFHAL_FLASH_EXCLUDE_RODATA@/$(WOLFHAL_FLASH_EXCLUDE_RODATA)/g' | \ sed -e 's/@WOLFHAL_FLASH_RAM_SECTIONS@/$(WOLFHAL_FLASH_RAM_SECTIONS)/g' \ diff --git a/arch.mk b/arch.mk index c291fde827..672e6e06c3 100644 --- a/arch.mk +++ b/arch.mk @@ -1237,6 +1237,19 @@ ifeq ($(ARCH),RISCV64) # MPFS DDR init pulls LIBERO_SETTING_* values from a Libero/HSS-generated # fpga_design_config.h. Setting LIBERO_FPGA_CONFIG_DIR enables DDR init # and adds the directory to the include search path. + # + # Booting an S-mode OS means running from DDR, so an empty + # LIBERO_FPGA_CONFIG_DIR here would silently produce a bootloader with no + # DDR controller init at all. Fail the build instead. + ifeq ($(LIBERO_FPGA_CONFIG_DIR),) + ifneq (,$(findstring WOLFBOOT_MMODE_SMODE_BOOT,$(CFLAGS_EXTRA) $(CFLAGS))) + ifeq ($(filter clean keysclean distclean,$(MAKECMDGOALS)),) + $(error WOLFBOOT_MMODE_SMODE_BOOT requires LIBERO_FPGA_CONFIG_DIR: \ + point it at the board's fpga_design_config directory, e.g. \ + /build/boards/mpfs-video-kit/fpga_design_config) + endif + endif + endif ifneq ($(LIBERO_FPGA_CONFIG_DIR),) CFLAGS+=-DMPFS_DDR_INIT -I$(LIBERO_FPGA_CONFIG_DIR) # Generic Cadence DDR controller driver + the MPFS PHY/PLL/training diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index bc22c5f677..79658269c5 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -77,8 +77,14 @@ CFLAGS_EXTRA+=-DWOLFBOOT_MMODE_SMODE_BOOT DISK_SDCARD?=1 DISK_EMMC?=0 -# wolfBoot in L2 SRAM (256KB available) +# wolfBoot in L2 SRAM: all 4 scratchpad ways (0x0A000000 - 0x0A07FFFF) WOLFBOOT_ORIGIN?=0x0A000000 +WOLFBOOT_L2SCRATCH_SIZE?=512k + +# Boot-hart stack. Sized for ML-DSA-87, whose verify working set lands on +# top of the two IMAGE_HEADER_SIZE buffers src/update_disk.c puts on the +# stack; ECC384 needs far less. +STACK_SIZE?=64k # 4KB sector size (SD card flow is partition-based, not flash-erase-based) WOLFBOOT_SECTOR_SIZE?=0x1000 @@ -131,17 +137,10 @@ CFLAGS_EXTRA+=-DSDHCI_SDMA_DISABLED # race on Arasan/Cadence-family controllers; single-block avoids it. CFLAGS_EXTRA+=-DSDHCI_FORCE_SINGLE_BLOCK_READ -# Disk-load via PDMA staging. On this board, CPU AXI writes to DDR -# (cached or non-cached) do NOT reliably land at the address that -# subsequent cached reads will fetch from -- empirical alias probe -# showed CPU writes via the 0xC0000000 non-cached window are silently -# dropped, and cached PIO writes appear to allocate L2 lines that are -# never written back to DDR before the integrity-check read. -# -# Workaround: SDHCI PIO into a small L2 Scratch staging buffer, then -# mpfs_pdma_memcpy() copies the block into DDR via the PDMA master. -# PDMA-via-non-cached is the only AXI write path verified to land in -# DDR (the same path used by mpfs_clear_bootup_cache_ways pre-fill). +# Disk-load via PDMA staging: SDHCI PIO into an L2 Scratch staging buffer, +# then mpfs_pdma_memcpy() lands each block in DDR through the PDMA master. +# A CPU copy into DDR reads back wrong depending on access order and code +# layout (cause not identified); the PDMA path has never failed. CFLAGS_EXTRA+=-DSDHCI_BLOCK_VIA_PDMA # Video Kit routes the SD slot's Card Detect (CD#) signal through the FPGA diff --git a/docs/Targets.md b/docs/Targets.md index dd70453cfc..4fa3b7898c 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1606,7 +1606,7 @@ See the [Encrypted Partitions](encrypted_partitions.md) documentation for additi #### Configuration -Update your `.config` file with the following ML-DSA settings: +Update your `.config` file with the following ML-DSA settings, or pass them as `make` arguments on top of `config/examples/polarfire_mpfs250_m.config` (the standalone M-mode E51 target verifies ML-DSA-87 this way, with SHA-384 as the image hash): ```makefile # ML-DSA 87 (Category 5) @@ -1661,6 +1661,20 @@ Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB | ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | | ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | +Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit for the same 19.7 MB FIT, plaintext, ECC384/SHA384, with the DDR training and SD-card load included: + +| Phase | Software crypto | Athena offload | +|-----------------------------------------------|-----------------|----------------| +| DDR training, SD init, GPT read | ~1.5 s | ~1.5 s | +| SD load of the FIT (CMD17 single block + PDMA staging) | ~15.5 s | ~15.5 s | +| SHA384 integrity (reads through the non-cached alias) | ~4.1 s | ~4.8 s | +| ECC384 signature verify | ~0.7 s | ~0.7 s | +| FIT kernel copy to its load address (PDMA + read-back verify) | ~9.1 s | ~9.1 s | +| M-mode -> S-mode handoff | ~31 s | ~32 s | +| Linux login prompt | ~53-62 s | ~53 s | + +The offload does not shorten the integrity check on this path because the time is in reading the image through the non-cached DDR alias, not in the hashing; measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. The SD read and the PDMA copy with its byte-wise verify are the dominant costs; both exist because of the CPU-write-to-DDR coherence workaround (`SDHCI_BLOCK_VIA_PDMA`). ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. + ### PolarFire Soc Debugging Start GDB server: @@ -1693,7 +1707,7 @@ wolfBoot Version: 2.8.0 Running on E51 (hart 0) in M-mode Boot RESET_SR: 1FF (bit0=PERIPH bit1=MSS bit2=CPU bit3=DBG bit4=FABRIC bit5=WDOG bit6=GPIO bit7=BUS bit8=SOFT) ======================================== -DDR: Training+MTC PASS after 0 retries +DDR: Training+MTC PASS DDR: Initialization COMPLETE SDHCI: platform init Reading MBR... diff --git a/hal/mpfs250-m.ld b/hal/mpfs250-m.ld index d84aace4e1..bf012e9124 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -26,19 +26,27 @@ MEMORY * This offset is added by mpfsBootmodeProgrammer (bootmode 1) */ FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 - /* L2 Scratchpad SRAM - 256 KB used (2 of 4 scratchpad ways). - * Attempted 512 KB (all 4 ways) to match HSS layout, but ways 8-9 - * are not initialized by the bootmode programmer -- stack/HLS - * placed there hit a trap immediately after DDR init. Reverted - * until we add explicit scratchpad init for ways 8-9. - * Address range: 0x0A000000 - 0x0A03FFFF */ - L2_SCRATCH (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = 256k + /* L2 Scratchpad SRAM. Size is bounded by the number of scratchpad ways + * the startup asm populates one way at a time (see the pin loop in + * src/boot_riscv_start.S); MPFS250 hardwires ways 8-11 as scratchpad, so + * 4 x WAY_BYTE_LENGTH = 512 KB is the ceiling. Set per target with + * WOLFBOOT_L2SCRATCH_SIZE; the ASSERT below enforces the ceiling. */ + L2_SCRATCH (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @WOLFBOOT_L2SCRATCH_SIZE@ } -/* Stack size for the boot hart (E51 in M-mode) - * ECC384 + SHA384 + SPMATHALL + NO_ASM measured peak: ~6KB. - * 32KB provides 5x headroom. */ -PROVIDE(STACK_SIZE = 32k); +/* Stack size for the boot hart (E51 in M-mode). Set per target with + * STACK_SIZE: ECC384 + SHA384 needs only a few KB, but ML-DSA-87 adds its + * own working set plus two IMAGE_HEADER_SIZE buffers that src/update_disk.c + * places on the stack. */ +PROVIDE(STACK_SIZE = @STACK_SIZE@); + +/* 4 scratchpad ways x 128 KB (WAY_BYTE_LENGTH). Going past this hands out + * addresses the startup pin loop never made resident. */ +ASSERT(LENGTH(L2_SCRATCH) <= 4 * 0x20000, + "L2_SCRATCH exceeds the 4 scratchpad ways (512 KB)") +ASSERT(ORIGIN(L2_SCRATCH) >= 0x0A000000 && + ORIGIN(L2_SCRATCH) + LENGTH(L2_SCRATCH) <= 0x0A080000, + "L2_SCRATCH must lie inside the pinned scratchpad 0x0A000000..0x0A07FFFF") SECTIONS { diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 56a2c25db4..fbd6021018 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -399,7 +399,7 @@ void hal_init(void) #if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) /* Clear the DTIM-resident cross-hart state (start mailboxes + SBI * shared block): DTIM content is undefined at power-on. */ - for (k = 0; k < (0x200U / sizeof(uint32_t)); k++) { + for (k = 0; k < (MPFS_DTIM_BOOT_CLEAR_SIZE / sizeof(uint32_t)); k++) { dtim[k] = 0; } __asm__ volatile("fence iorw, iorw" ::: "memory"); @@ -472,12 +472,8 @@ void hal_init(void) #ifdef MPFS_DDR_INIT /* Bring up LPDDR4 before any DDR-resident operations. * - * Outer retry loop: each call to mpfs_ddr_init() does a SYSREG DDRC - * soft-reset pulse, which clears the MTC engine state. If the - * inner retry inside mpfs_ddr_init() exhausts (typically because - * MTC wedged after the first failure), come back here for a full - * controller re-init. Empirical: per-attempt failure rate ~30%, so - * MPFS_DDR_MAX_OUTER_RETRY (6) outer attempts cover ~99.9% of boots. */ + * Each mpfs_ddr_init() call pulses the SYSREG DDRC soft reset; a rejected + * training only recovers through this full controller re-init. */ for (outer_retry = 0; outer_retry < MPFS_DDR_MAX_OUTER_RETRY; outer_retry++) { if (outer_retry > 0) { diff --git a/hal/mpfs250.h b/hal/mpfs250.h index a3de52b817..e203c13b09 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -56,7 +56,7 @@ #define MPFS_CPU_FREQ_RESET_MHZ 80U #endif -/* Full-DDRC-reinit attempts in hal_init() (per-attempt failure rate ~30%). */ +/* Full-DDRC-reinit attempts in hal_init(). */ #ifndef MPFS_DDR_MAX_OUTER_RETRY #define MPFS_DDR_MAX_OUTER_RETRY 6U #endif @@ -351,6 +351,19 @@ typedef struct { * No UL suffix: also used from assembly (boot_riscv_start.S). */ #define MPFS_DTIM_MAIN_STARTED_ADDR 0x010000F0 +/* DTIM bytes hal_init() zeroes at boot (SBI shared block + hart mailboxes). */ +#define MPFS_DTIM_BOOT_CLEAR_SIZE 0x200U +/* DTIM pair (count, ~count) just above the cleared block, so it survives an + * MSS reset: DDR training restarts by reset since the last accepted training. */ +#define MPFS_DTIM_DDR_RESET_CNT_ADDR (0x01000000UL + MPFS_DTIM_BOOT_CLEAR_SIZE) +#ifndef MPFS_DDR_EYE_RESET_MAX +#define MPFS_DDR_EYE_RESET_MAX 5U +#endif +/* Minimum per-lane DQ/DQS window, HSS DQ_DQS_NUM_TAPS. */ +#ifndef MPFS_DDR_EYE_MIN_TAPS +#define MPFS_DDR_EYE_MIN_TAPS 5U +#endif + /* Number of harts on MPFS */ #define MPFS_NUM_HARTS 5 #define MPFS_FIRST_HART 0 /* E51 is hart 0 */ @@ -509,7 +522,25 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define PHY_TRAINING_START 0x810 #define PHY_TRAINING_STATUS 0x814 #define PHY_TRAINING_RESET 0x818 -#define PHY_TIP_CFG 0x828 +/* Per-lane TIP status, selected via PHY_LANE_SELECT. Offsets taken from + * CFG_DDR_SGMII_PHY_TypeDef in mss_ddr_sgmii_phy_defs.h. */ +#define PHY_GT_ERR_COMB 0x81C +#define PHY_GT_CLK_SEL 0x820 +#define PHY_GT_TXDLY 0x824 +#define PHY_GT_STEPS_180 0x828 +#define PHY_GT_STATE 0x82C +#define PHY_WL_DELAY_0 0x830 +#define PHY_DQ_DQS_ERR_DONE 0x834 +#define PHY_DQDQS_WINDOW 0x838 /* [7:0] left, [15:8] right edge */ +#define PHY_DQDQS_STATE 0x83C +#define PHY_DELTA0 0x840 +#define PHY_DELTA1 0x844 +#define PHY_DQDQS_STATUS1 0x84C +#define PHY_DQDQS_STATUS2 0x850 +#define PHY_ADDCMD_STATUS0 0x864 +#define PHY_ADDCMD_STATUS1 0x868 +#define PHY_ADDCMD_ANSWER 0x86C +#define PHY_IOC_REG5 0x218 /* SRO slew readback */ #define PHY_TIP_CFG_PARAMS 0x8D0 #define PHY_EXPERT_MODE_EN 0x878 #define PHY_EXPERT_DLYCNT_MOVE0 0x87C diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index 551c82f2b6..cf990986b6 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -36,30 +36,13 @@ #ifdef MPFS_DDR_INIT -/* DQ/DQS init offset (HSS rpc_156). Default 6 (Libero Video Kit value), - * tunable 1..9 per HSS TUNE_RPC_156_DQDQS_INIT_VALUE. Bumped between outer - * retries when training verify reports dq_dqs_err_done != 8 or - * dqdqs_status2 == 0 (data eye closed). */ +/* DQ/DQS init offset (HSS rpc_156). 6 is the Libero Video Kit value; HSS + * allows 1..9 (TUNE_RPC_156_DQDQS_INIT_VALUE) but it only shifts the window. */ static uint32_t mpfs_phy_rpc156_val = 6U; #if defined(WOLFBOOT_RISCV_MMODE) && defined(MPFS_DDR_INIT) -/* DDR-init busy-loop delay. The argument is NOT a real microsecond -- - * it is whatever the legacy busy-loop produces at the current CPU - * clock. Empirically reaches train_stat=0x1D on the first attempt with - * the same per-attempt rate as forwarding to udelay(), and is much - * faster (~4 s vs ~50 s) for the TIP-wait timeout, which dominates - * retry-loop time when training fails. - * - * Do NOT replace with udelay(us) without re-timing every call site - * below: at 600 MHz the busy-loop delivers roughly us/20 of a real us, - * so udelay(us) makes every post-PLL delay ~20x longer. In addition - * to slowing retries, this can shift LPDDR4 / PHY timing windows -- - * earlier observed empirical data showed an isolated additional - * regression beyond the pre-existing ~30% per-attempt failure rate. - * - * The "5us" / "250us" / "2ms" comments at the call sites are LEGACY - * and do not reflect the actual delay; preserved for git blame, not - * as timing references. */ +/* DDR-init busy-loop delay. The argument is NOT a real microsecond: at 600 MHz + * it delivers roughly us/20. udelay() trains identically but is ~20x slower. */ static void ddr_delay(uint32_t us) { volatile uint32_t i; @@ -535,21 +518,16 @@ static void setup_segments(void) mb(); } -/* DDR Controller Configuration +/* DDR controller configuration, in HSS init_ddrc() order. * - * Phase 3.6 rewrite: full bulk import of MC_BASE2 register configuration - * matching HSS setup_ddrc() at mss_ddr.c:3940-4225. All values come from - * the Video Kit Libero header + * Values come from the Video Kit Libero header * hart-software-services/build/boards/mpfs-video-kit/fpga_design_config/ * ddr/hw_ddrc.h * - * The previous version configured only ~30 of these registers AND used - * several wrong register offsets (e.g. MC_CFG_CL was at 0x74 -- which is - * actually CFG_XP -- so the CL value never reached the CL register). - * That left the IP in an under/mis-configured state that prevented TIP - * from progressing past BCLK_SCLK during training. - * - * This function configures the full ~155 MC_BASE2 registers in HSS order. + * ddr_cadence_controller_setup() writes the table in array order, so row + * order is write order. That matters at CTRLR_SOFT_RESET_N below: the PHY + * training, MTC and AXI_IF blocks are programmed only after the controller + * is released from soft reset. */ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x2400, LIBERO_SETTING_CFG_MANUAL_ADDRESS_MAP }, @@ -659,95 +637,13 @@ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x3D70, LIBERO_SETTING_CFG_RRD_DLR }, { 0x3D74, LIBERO_SETTING_CFG_FAW_DLR }, { 0x3D98, LIBERO_SETTING_CFG_ADVANCE_ACTIVATE_READY }, - { 0x4C00, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P0 }, - { 0x4C04, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P1 }, - { 0x4C08, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P2 }, - { 0x4C0C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P3 }, - { 0x4C10, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P4 }, - { 0x4C14, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P5 }, - { 0x4C18, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P6 }, - { 0x4C1C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P7 }, - { 0x5000, LIBERO_SETTING_CFG_REORDER_EN }, - { 0x5004, LIBERO_SETTING_CFG_REORDER_QUEUE_EN }, - { 0x5008, LIBERO_SETTING_CFG_INTRAPORT_REORDER_EN }, - { 0x500C, LIBERO_SETTING_CFG_MAINTAIN_COHERENCY }, - { 0x5010, LIBERO_SETTING_CFG_Q_AGE_LIMIT }, - { 0x5018, LIBERO_SETTING_CFG_RO_CLOSED_PAGE_POLICY }, - { 0x501C, LIBERO_SETTING_CFG_REORDER_RW_ONLY }, - { 0x5020, LIBERO_SETTING_CFG_RO_PRIORITY_EN }, - { 0x5400, LIBERO_SETTING_CFG_DM_EN }, - { 0x5404, LIBERO_SETTING_CFG_RMW_EN }, - { 0x5800, LIBERO_SETTING_CFG_ECC_CORRECTION_EN }, - { 0x5840, LIBERO_SETTING_CFG_ECC_BYPASS }, - { 0x5844, LIBERO_SETTING_INIT_WRITE_DATA_1B_ECC_ERROR_GEN }, - { 0x5848, LIBERO_SETTING_INIT_WRITE_DATA_2B_ECC_ERROR_GEN }, - { 0x585C, LIBERO_SETTING_CFG_ECC_1BIT_INT_THRESH }, - { 0x5C00, LIBERO_SETTING_INIT_READ_CAPTURE_ADDR }, - { 0x6400, LIBERO_SETTING_CFG_ERROR_GROUP_SEL }, - { 0x6404, LIBERO_SETTING_CFG_DATA_SEL }, - { 0x6408, LIBERO_SETTING_CFG_TRIG_MODE }, - { 0x640C, LIBERO_SETTING_CFG_POST_TRIG_CYCS }, - { 0x6410, LIBERO_SETTING_CFG_TRIG_MASK }, - { 0x6414, LIBERO_SETTING_CFG_EN_MASK }, - { 0x6418, LIBERO_SETTING_MTC_ACQ_ADDR }, - { 0x6430, LIBERO_SETTING_CFG_TRIG_MT_ADDR_0 }, - { 0x6434, LIBERO_SETTING_CFG_TRIG_MT_ADDR_1 }, - { 0x6438, LIBERO_SETTING_CFG_TRIG_ERR_MASK_0 }, - { 0x643C, LIBERO_SETTING_CFG_TRIG_ERR_MASK_1 }, - { 0x6440, LIBERO_SETTING_CFG_TRIG_ERR_MASK_2 }, - { 0x6444, LIBERO_SETTING_CFG_TRIG_ERR_MASK_3 }, - { 0x6448, LIBERO_SETTING_CFG_TRIG_ERR_MASK_4 }, - { 0x644C, LIBERO_SETTING_MTC_ACQ_WR_DATA_0 }, - { 0x6450, LIBERO_SETTING_MTC_ACQ_WR_DATA_1 }, - { 0x6454, LIBERO_SETTING_MTC_ACQ_WR_DATA_2 }, - { 0x652C, LIBERO_SETTING_CFG_PRE_TRIG_CYCS }, - { 0x6550, LIBERO_SETTING_CFG_DATA_SEL_FIRST_ERROR }, - { 0x7C00, LIBERO_SETTING_CFG_DQ_WIDTH }, - { 0x7C04, LIBERO_SETTING_CFG_ACTIVE_DQ_SEL }, - { 0x800C, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_REQ }, - { 0x8010, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_CMD }, - { 0x10010, LIBERO_SETTING_INIT_DFI_LP_DATA_REQ }, - { 0x10014, LIBERO_SETTING_INIT_DFI_LP_CTRL_REQ }, - { 0x1001C, LIBERO_SETTING_INIT_DFI_LP_WAKEUP }, - { 0x10020, LIBERO_SETTING_INIT_DFI_DRAM_CLK_DISABLE }, - { 0x10030, LIBERO_SETTING_CFG_DFI_DATA_BYTE_DISABLE }, - { 0x1003C, LIBERO_SETTING_CFG_DFI_LVL_SEL }, - { 0x10040, LIBERO_SETTING_CFG_DFI_LVL_PERIODIC }, - { 0x10044, LIBERO_SETTING_CFG_DFI_LVL_PATTERN }, - { 0x10050, LIBERO_SETTING_PHY_DFI_INIT_START }, - { 0x12C18, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_0 }, - { 0x12C1C, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_1 }, - { 0x12C20, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_0 }, - { 0x12C24, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_1 }, - { 0x12F18, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_0 }, - { 0x12F1C, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_1 }, - { 0x12F20, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_0 }, - { 0x12F24, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_1 }, - { 0x13218, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_0 }, - { 0x1321C, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_1 }, - { 0x13220, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_0 }, - { 0x13224, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_1 }, - { 0x13514, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI1 }, - { 0x13518, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI2 }, - { 0x13690, LIBERO_SETTING_CFG_AXI_AUTO_PCH }, - { 0x3C000, LIBERO_SETTING_PHY_RESET_CONTROL }, - { 0x3C000, (LIBERO_SETTING_PHY_RESET_CONTROL & ~0x8000UL) }, - { 0x3C004, LIBERO_SETTING_PHY_PC_RANK }, - { 0x3C008, LIBERO_SETTING_PHY_RANKS_TO_TRAIN }, - { 0x3C00C, LIBERO_SETTING_PHY_WRITE_REQUEST }, - { 0x3C014, LIBERO_SETTING_PHY_READ_REQUEST }, - { 0x3C01C, LIBERO_SETTING_PHY_WRITE_LEVEL_DELAY }, - { 0x3C020, LIBERO_SETTING_PHY_GATE_TRAIN_DELAY }, - { 0x3C024, LIBERO_SETTING_PHY_EYE_TRAIN_DELAY }, - { 0x3C028, LIBERO_SETTING_PHY_EYE_PAT }, - { 0x3C02C, LIBERO_SETTING_PHY_START_RECAL }, - { 0x3C030, LIBERO_SETTING_PHY_CLR_DFI_LVL_PERIODIC }, - { 0x3C034, LIBERO_SETTING_PHY_TRAIN_STEP_ENABLE }, - { 0x3C038, LIBERO_SETTING_PHY_LPDDR_DQ_CAL_PAT }, - { 0x3C03C, LIBERO_SETTING_PHY_INDPNDT_TRAINING }, - { 0x3C040, LIBERO_SETTING_PHY_ENCODED_QUAD_CS }, - { 0x3C044, LIBERO_SETTING_PHY_HALF_CLK_DLY_ENABLE }, + + /* Everything above is programmed with the controller still in soft + * reset. This write releases it; HSS init_ddrc() programs the PHY + * training, MTC and AXI_IF blocks only after this point, so the order + * of the rows below is a sequencing requirement, not a preference. */ { MC_CTRLR_SOFT_RESET_N, LIBERO_SETTING_CTRLR_SOFT_RESET_N }, + { MC_CFG_LOOKAHEAD_PCH, LIBERO_SETTING_CFG_LOOKAHEAD_PCH }, { MC_CFG_LOOKAHEAD_ACT, LIBERO_SETTING_CFG_LOOKAHEAD_ACT }, { MC_INIT_AUTOINIT_DISABLE, LIBERO_SETTING_INIT_AUTOINIT_DISABLE }, @@ -917,10 +813,98 @@ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { MC_CFG_BURST_RW_REFRESH_HOLDOFF, LIBERO_SETTING_CFG_BURST_RW_REFRESH_HOLDOFF }, { MC_CFG_BG_INTERLEAVE, LIBERO_SETTING_CFG_BG_INTERLEAVE }, { MC_CFG_REFRESH_DURING_PHY_TRAINING, LIBERO_SETTING_CFG_REFRESH_DURING_PHY_TRAINING }, + { 0x4C00, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P0 }, + { 0x4C04, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P1 }, + { 0x4C08, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P2 }, + { 0x4C0C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P3 }, + { 0x4C10, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P4 }, + { 0x4C14, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P5 }, + { 0x4C18, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P6 }, + { 0x4C1C, LIBERO_SETTING_CFG_STARVE_TIMEOUT_P7 }, + { 0x5000, LIBERO_SETTING_CFG_REORDER_EN }, + { 0x5004, LIBERO_SETTING_CFG_REORDER_QUEUE_EN }, + { 0x5008, LIBERO_SETTING_CFG_INTRAPORT_REORDER_EN }, + { 0x500C, LIBERO_SETTING_CFG_MAINTAIN_COHERENCY }, + { 0x5010, LIBERO_SETTING_CFG_Q_AGE_LIMIT }, + { 0x5018, LIBERO_SETTING_CFG_RO_CLOSED_PAGE_POLICY }, + { 0x501C, LIBERO_SETTING_CFG_REORDER_RW_ONLY }, + { 0x5020, LIBERO_SETTING_CFG_RO_PRIORITY_EN }, + { 0x5400, LIBERO_SETTING_CFG_DM_EN }, + { 0x5404, LIBERO_SETTING_CFG_RMW_EN }, + { 0x5800, LIBERO_SETTING_CFG_ECC_CORRECTION_EN }, + { 0x5840, LIBERO_SETTING_CFG_ECC_BYPASS }, + { 0x5844, LIBERO_SETTING_INIT_WRITE_DATA_1B_ECC_ERROR_GEN }, + { 0x5848, LIBERO_SETTING_INIT_WRITE_DATA_2B_ECC_ERROR_GEN }, + { 0x585C, LIBERO_SETTING_CFG_ECC_1BIT_INT_THRESH }, + { 0x5C00, LIBERO_SETTING_INIT_READ_CAPTURE_ADDR }, + { 0x6400, LIBERO_SETTING_CFG_ERROR_GROUP_SEL }, + { 0x6404, LIBERO_SETTING_CFG_DATA_SEL }, + { 0x6408, LIBERO_SETTING_CFG_TRIG_MODE }, + { 0x640C, LIBERO_SETTING_CFG_POST_TRIG_CYCS }, + { 0x6410, LIBERO_SETTING_CFG_TRIG_MASK }, + { 0x6414, LIBERO_SETTING_CFG_EN_MASK }, + { 0x6418, LIBERO_SETTING_MTC_ACQ_ADDR }, + { 0x6430, LIBERO_SETTING_CFG_TRIG_MT_ADDR_0 }, + { 0x6434, LIBERO_SETTING_CFG_TRIG_MT_ADDR_1 }, + { 0x6438, LIBERO_SETTING_CFG_TRIG_ERR_MASK_0 }, + { 0x643C, LIBERO_SETTING_CFG_TRIG_ERR_MASK_1 }, + { 0x6440, LIBERO_SETTING_CFG_TRIG_ERR_MASK_2 }, + { 0x6444, LIBERO_SETTING_CFG_TRIG_ERR_MASK_3 }, + { 0x6448, LIBERO_SETTING_CFG_TRIG_ERR_MASK_4 }, + { 0x644C, LIBERO_SETTING_MTC_ACQ_WR_DATA_0 }, + { 0x6450, LIBERO_SETTING_MTC_ACQ_WR_DATA_1 }, + { 0x6454, LIBERO_SETTING_MTC_ACQ_WR_DATA_2 }, + { 0x652C, LIBERO_SETTING_CFG_PRE_TRIG_CYCS }, + { 0x6550, LIBERO_SETTING_CFG_DATA_SEL_FIRST_ERROR }, + { 0x7C00, LIBERO_SETTING_CFG_DQ_WIDTH }, + { 0x7C04, LIBERO_SETTING_CFG_ACTIVE_DQ_SEL }, + { 0x800C, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_REQ }, + { 0x8010, LIBERO_SETTING_INIT_CA_PARITY_ERROR_GEN_CMD }, { MC_DFI_RDDATA_EN, LIBERO_SETTING_CFG_DFI_T_RDDATA_EN }, { MC_DFI_PHY_RDLAT, LIBERO_SETTING_CFG_DFI_T_PHY_RDLAT }, { MC_DFI_PHY_WRLAT, LIBERO_SETTING_CFG_DFI_T_PHY_WRLAT }, { MC_DFI_PHYUPD_EN, LIBERO_SETTING_CFG_DFI_PHYUPD_EN }, + { 0x10010, LIBERO_SETTING_INIT_DFI_LP_DATA_REQ }, + { 0x10014, LIBERO_SETTING_INIT_DFI_LP_CTRL_REQ }, + { 0x1001C, LIBERO_SETTING_INIT_DFI_LP_WAKEUP }, + { 0x10020, LIBERO_SETTING_INIT_DFI_DRAM_CLK_DISABLE }, + { 0x10030, LIBERO_SETTING_CFG_DFI_DATA_BYTE_DISABLE }, + { 0x1003C, LIBERO_SETTING_CFG_DFI_LVL_SEL }, + { 0x10040, LIBERO_SETTING_CFG_DFI_LVL_PERIODIC }, + { 0x10044, LIBERO_SETTING_CFG_DFI_LVL_PATTERN }, + { 0x10050, LIBERO_SETTING_PHY_DFI_INIT_START }, + { 0x12C18, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_0 }, + { 0x12C1C, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI1_1 }, + { 0x12C20, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_0 }, + { 0x12C24, LIBERO_SETTING_CFG_AXI_START_ADDRESS_AXI2_1 }, + { 0x12F18, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_0 }, + { 0x12F1C, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI1_1 }, + { 0x12F20, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_0 }, + { 0x12F24, LIBERO_SETTING_CFG_AXI_END_ADDRESS_AXI2_1 }, + { 0x13218, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_0 }, + { 0x1321C, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI1_1 }, + { 0x13220, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_0 }, + { 0x13224, LIBERO_SETTING_CFG_MEM_START_ADDRESS_AXI2_1 }, + { 0x13514, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI1 }, + { 0x13518, LIBERO_SETTING_CFG_ENABLE_BUS_HOLD_AXI2 }, + { 0x13690, LIBERO_SETTING_CFG_AXI_AUTO_PCH }, + { 0x3C000, LIBERO_SETTING_PHY_RESET_CONTROL }, + { 0x3C000, (LIBERO_SETTING_PHY_RESET_CONTROL & ~0x8000UL) }, + { 0x3C004, LIBERO_SETTING_PHY_PC_RANK }, + { 0x3C008, LIBERO_SETTING_PHY_RANKS_TO_TRAIN }, + { 0x3C00C, LIBERO_SETTING_PHY_WRITE_REQUEST }, + { 0x3C014, LIBERO_SETTING_PHY_READ_REQUEST }, + { 0x3C01C, LIBERO_SETTING_PHY_WRITE_LEVEL_DELAY }, + { 0x3C020, LIBERO_SETTING_PHY_GATE_TRAIN_DELAY }, + { 0x3C024, LIBERO_SETTING_PHY_EYE_TRAIN_DELAY }, + { 0x3C028, LIBERO_SETTING_PHY_EYE_PAT }, + { 0x3C02C, LIBERO_SETTING_PHY_START_RECAL }, + { 0x3C030, LIBERO_SETTING_PHY_CLR_DFI_LVL_PERIODIC }, + { 0x3C034, LIBERO_SETTING_PHY_TRAIN_STEP_ENABLE }, + { 0x3C038, LIBERO_SETTING_PHY_LPDDR_DQ_CAL_PAT }, + { 0x3C03C, LIBERO_SETTING_PHY_INDPNDT_TRAINING }, + { 0x3C040, LIBERO_SETTING_PHY_ENCODED_QUAD_CS }, + { 0x3C044, LIBERO_SETTING_PHY_HALF_CLK_DLY_ENABLE }, }; /* Program the full MC_BASE2/ADDR_MAP/MC_BASE1/MPFE/.../AXI_IF controller @@ -1134,14 +1118,9 @@ static int setup_phy(void) DDRPHY_REG(PHY_RPC156) = mpfs_phy_rpc156_val; /* DQ/DQS init offset (1..9) */ DDRPHY_REG(PHY_RPC166) = 0x00000002UL; /* Trained: 0x02 */ DDRPHY_REG(PHY_RPC168) = 0x00000000UL; /* Trained: 0x00 */ - /* rpc220 (DQ load delay). Full CFG_DDR_SGMII_PHY diff vs HSS - * (2026-06-01) shows HSS runs rpc220=0x1 during WRLVL and only - * raises it to 0xC inside write_calibration (mss_ddr.c:1744). - * Tested matching HSS (0x1 here): wolfBoot's AXI reads HANG (naked - * read @ 0xC0000000 stalls, WRCALIB times out). wolfBoot needs 0xC - * for the read path to function -- another HSS value that does not - * transfer to wolfBoot's PHY operating point. Keep 0xC. */ - DDRPHY_REG(PHY_RPC220) = 0x0000000CUL; /* wolfBoot-needed (HSS=0x1 hangs reads) */ + /* rpc220 (DQ load delay). HSS leaves the reset value (0x1) until write + * calibration; either value trains the same here. */ + DDRPHY_REG(PHY_RPC220) = 0x0000000CUL; /* rpc226 at offset 0x788: HSS-captured value 0x14. The * DDRPHY_MODE-driven preload normally populates this, but on this * board wolfBoot's preload ended up with 0x01 -- write it @@ -1450,8 +1429,8 @@ static uint8_t mpfs_write_calibration_using_mtc(uint8_t num_lanes) return 0U; } -/* DDR Training. retry_count = combined outer*MAX_TRAIN_RETRY + inner - * retry count. Used for HSS-style MOVE_CK ADDCMD cycling (mss_ddr.c: +/* DDR Training. retry_count = the caller's re-init attempt number, + * used for HSS-style MOVE_CK ADDCMD cycling (mss_ddr.c: * 6101-6128) which rotates the picked refclk index (0/45/90 deg -> * k / k+1 / k+2) across retries to converge when the first pick is * marginal. */ @@ -2247,6 +2226,49 @@ static void training_tip_wait(void) } +#ifdef DEBUG_DDR +/* Post-training dump in the column order of the Microchip HAL DDR demo + * (mss_ddr_debug.c), settling 50 us after each lane_select as HSS does. */ +static void training_posttip_dump(void) +{ + uint32_t lane; + uint32_t ioc2 = DDRPHY_REG(PHY_IOC_REG2); + uint32_t ioc5 = DDRPHY_REG(PHY_IOC_REG5); + + DBG_DDR("REFDUMP train_stat=0x%x PCODE=0x%x NCODE=0x%x WRCALIB=0x%x\n", + DDRPHY_REG(PHY_TRAINING_STATUS), ioc2 & 0x7FU, + (ioc2 >> 7) & 0x7FU, DDRPHY_REG(PHY_EXPERT_WRCALIB)); + DBG_DDR("REFDUMP sro_ref_slewr=0x%x sro_ref_slewf=0x%x sro_slewr=0x%x " + "sro_slewf=0x%x\n", ioc5 & 0x3FU, (ioc5 >> 6) & 0xFFFU, + (ioc5 >> 18) & 0x3FU, (ioc5 >> 24) & 0x3FU); + + for (lane = 0; lane < 4U; lane++) { + DDRPHY_REG(PHY_LANE_SELECT) = lane; + udelay(50); + DBG_DDR("REFDUMP L%u gt_err_comb=0x%x gt_txdly=0x%x gt_steps_180=0x%x " + "gt_state=0x%x gt_clk_sel=0x%x wl_delay_0=0x%x\n", + lane, DDRPHY_REG(PHY_GT_ERR_COMB), DDRPHY_REG(PHY_GT_TXDLY), + DDRPHY_REG(PHY_GT_STEPS_180), DDRPHY_REG(PHY_GT_STATE), + DDRPHY_REG(PHY_GT_CLK_SEL), DDRPHY_REG(PHY_WL_DELAY_0)); + DBG_DDR("REFDUMP L%u dqdqs_err_done=0x%x dqdqs_state=0x%x delta0=0x%x " + "delta1=0x%x dqdqs_window=0x%x\n", lane, + DDRPHY_REG(PHY_DQ_DQS_ERR_DONE), DDRPHY_REG(PHY_DQDQS_STATE), + DDRPHY_REG(PHY_DELTA0), DDRPHY_REG(PHY_DELTA1), + DDRPHY_REG(PHY_DQDQS_WINDOW)); + DBG_DDR("REFDUMP L%u rdqdqs_status2=0x%x addcmd_status0=0x%x " + "addcmd_status1=0x%x addcmd_answer=0x%x dqdqs_status1=0x%x\n", + lane, DDRPHY_REG(PHY_DQDQS_STATUS2), + DDRPHY_REG(PHY_ADDCMD_STATUS0), DDRPHY_REG(PHY_ADDCMD_STATUS1), + DDRPHY_REG(PHY_ADDCMD_ANSWER), DDRPHY_REG(PHY_DQDQS_STATUS1)); + } + /* TIP relies on lane_select being non-zero between iterations (see + * training_pretip_lane_snapshot), so leave it where the loop ended. */ +} +#else +#define training_posttip_dump() do { } while (0) +#endif + + static int run_training(uint32_t retry_count) { uint32_t timeout, dfi_stat, train_stat; @@ -2255,20 +2277,8 @@ static int run_training(uint32_t retry_count) uint32_t l; /* per-lane eye-width index */ uint32_t eye[4]; /* per-lane data-eye widths */ - /* TRAINING_SKIP = 0x02 to skip TIP's ADDCMD phase (we run our own - * manual ADDCMD via lpddr4_manual_training above). Matches HSS - * captured value 0x02 at PHY 0x80C (2026-05-15 DEBUG HEXDUMP). - * - * Previously experimented with 0x00 (full TIP training) under the - * theory that train_stat=0x1F (vs 0x1D) and DFI training_complete - * would help. That assumption was wrong: full TIP training picks - * different per-lane wl_dly values from the HSS-trained ones, and - * those wl_dly values combined with our other PHY config left the - * write-data path mistrained for lanes 2/3. */ - /* Tested TRAINING_SKIP=0x02 twice (with and without rpc220=0xC - * rpc226=0x14 alignment) -- regresses wl_dly to 0x56-0x7F across - * lanes (vs 0x24-0x2C with skip=0). HSS's TIP-skip approach - * requires pre-WRLVL PHY state we don't yet match. Keep skip=0. */ + /* Run every TIP step. The Libero default (0x02, skip ADDCMD) collapses + * lane 0's DQ/DQS window to one tap with this driver's manual ADDCMD. */ DDRPHY_REG(PHY_TRAINING_SKIP) = 0x00U; mb(); @@ -2691,16 +2701,16 @@ static int run_training(uint32_t retry_count) /* HSS DDR_TRAINING_VERIFY checks (mss_ddr.c:1488-1522): if any of * these are non-canonical, training had problems even though - * train_stat reads 0x1D. dqdqs_status2 is per-lane (selected via + * train_stat looks complete. dqdqs_status2 is per-lane (selected via * PHY_LANE_SELECT) -- dump all 4 to see per-lane data-eye width. */ for (l = 0; l < 4U; l++) { DDRPHY_REG(PHY_LANE_SELECT) = l; udelay(2); - eye[l] = DDRPHY_REG(0x850U); + eye[l] = DDRPHY_REG(PHY_DQDQS_STATUS2); } DBG_DDR( " gt_err_comb=0x%x dq_dqs_err_done=0x%x (need 8) eye[0..3]=%u/%u/%u/%u\n", - DDRPHY_REG(0x81CU), DDRPHY_REG(0x834U), + DDRPHY_REG(PHY_GT_ERR_COMB), DDRPHY_REG(PHY_DQ_DQS_ERR_DONE), eye[0], eye[1], eye[2], eye[3]); (void)eye; @@ -2787,17 +2797,14 @@ static int run_training(uint32_t retry_count) DDRCFG_REG(MC_CFG_AUTO_REF_EN) = 0x01; mb(); - /* HSS DDR_TRAINING_VERIFY (mss_ddr.c:1488-1504) reads: - * dq_dqs_err_done (need 8): DQ/DQS phase completion flag - * dqdqs_status2 (need >= 5 taps): data eye window width - * On this board both report bad values (0x4 / 0x0) yet train_stat - * reads 0x1D and lanes 2&3 still write correctly via the lanes-2&3 - * calibration committed by set_write_calib. Returning failure - * here triggers inner retries that empirically make PHY state - * WORSE (dq_dqs_err_done -> 0x0, all lanes fail WRCALIB), because - * back-to-back training without a power cycle accumulates errors. - * Accept training as-is; the calibration committed by WRCALIB is - * what we get. */ + /* HSS DDR_TRAINING_VERIFY (mss_ddr.c:1488-1504) reads dq_dqs_err_done + * (need 8) and dqdqs_status2 (need >= DQ_DQS_NUM_TAPS, which HSS sets + * to 5). Both are satisfied here. Returning failure would trigger + * inner retries, and back-to-back training without a power cycle + * accumulates errors rather than converging, so accept training as-is + * once those two checks pass. */ + + training_posttip_dump(); return 0; } @@ -3057,13 +3064,6 @@ int mpfs_ddr_init(unsigned int outer_retry) wolfBoot_printf("\n========================================\n"); - /* rpc_156 DQ/DQS init offset. Libero default 6 leaves the data eye - * closed (dqdqs_status2=0) on the Video Kit. HSS allows 1..9 via - * TUNE_RPC_156_DQDQS_INIT_VALUE. Empirically each fresh boot's - * training state degrades on subsequent attempts within the same - * power cycle, so we use a SINGLE value (no sweep): bump to 3 to - * push past the bad starting edge. Change in code if 3 doesn't - * give dqdqs_status2 >= 5 on cold boot. */ mpfs_phy_rpc156_val = 6U; (void)outer_retry; /* TUNE sweep removed; outer_retry kept for future use */ @@ -3140,21 +3140,16 @@ int mpfs_ddr_init(unsigned int outer_retry) * the MTC engine has a separate DDRC-internal access issue and would just * burn all retries and end in a WDT reset. * - * Empirical baseline: ~30% per-attempt training failure rate; 3 inner x - * 6 outer retries (up to 18 attempts) covers it with margin. + * A failed attempt is not retried in place: after the controller's + * training-fail reset the DFI init never completes again until the + * whole controller is re-initialised, which is also where HSS goes + * (DDR_TRAINING_FAIL -> DDR_TRAINING_CHECK_FOR_OFFMODE). The caller's + * outer loop does that. Every attempt uses the best-margin refclk + * pick: a write-calibration miss on this board recovers with a plain + * re-init, while the HSS-style MOVE_CK push to the second and third + * picks only produced write-calibration timeouts. */ { - uint32_t train_retry = 0; - /* 3 inner attempts so the MOVE_CK rotation (0deg/45deg/90deg) - * cycles through all three alternative refclk picks (k / k+1 / - * k+2) in one DDRC-init pass. Combined with the 6 outer - * retries gives 18 chances for ADDCMD + WRLVL convergence. */ - /* Keep at 3 so MOVE_CK rotation cycles through 0deg/45deg/90deg - * within one outer DDRC-init pass. We don't force WRLVL retry - * (regression made all 18 attempts MTC-timeout) so most boots - * succeed on first attempt; the inner retries only matter when - * MTC sanity actually fails. */ - const uint32_t MAX_TRAIN_RETRY = 3; uint32_t lane; uint32_t mtc_to; uint32_t train_stat; @@ -3162,30 +3157,8 @@ int mpfs_ddr_init(unsigned int outer_retry) uint32_t dfi_to; int mtc_pass = 0; - while (train_retry < MAX_TRAIN_RETRY) { - if (train_retry > 0) { - wolfBoot_printf( - "DDR: Retry %u/%u after MTC sanity FAIL\n", - (unsigned)train_retry, (unsigned)MAX_TRAIN_RETRY); - /* HSS DDR_TRAINING_FAIL reset sequence (mss_ddr.c:519-538) */ - DDRCFG_REG(MC_INIT_CS) = 0x1; - DDRCFG_REG(MC_INIT_DISABLE_CKE) = 0x1; - ddr_delay(500); - DDRCFG_REG(MC_INIT_FORCE_RESET) = 0x1; - ddr_delay(200000); - DDRCFG_REG(MC_DFI_INIT_START) = 0x0; - DDRCFG_REG(MC_CTRLR_INIT) = 0x0; - DDRPHY_REG(PHY_TRAINING_START) = 0x0; - mb(); - } - train_retry++; - - /* Combined retry count for HSS-style MOVE_CK ADDCMD - * rotation: each outer DDR re-init contributes - * MAX_TRAIN_RETRY worth of count, so retry%3 cycles - * through all three move-CK pairs across all attempts. */ - ret = run_training(outer_retry * MAX_TRAIN_RETRY + - (train_retry - 1)); + do { + ret = run_training(0); if (ret != 0) { continue; } @@ -3244,6 +3217,53 @@ int mpfs_ddr_init(unsigned int outer_retry) (unsigned)wrcal); continue; } + /* HSS DDR_TRAINING_IP_SM_VERIFY: a DQ/DQS window under + * DQ_DQS_NUM_TAPS (5) on any lane is a failed training. */ + { + volatile uint32_t *rst_cnt = + (volatile uint32_t *)MPFS_DTIM_DDR_RESET_CNT_ADDR; + uint32_t eye_min = 0xFFU; + uint32_t n = 0; + for (lane = 0; lane < 4; lane++) { + uint32_t w; + DDRPHY_REG(PHY_LANE_SELECT) = lane; + udelay(50); + w = DDRPHY_REG(PHY_DQDQS_STATUS2); + if (w < eye_min) { + eye_min = w; + } + } + if ((rst_cnt[0] ^ rst_cnt[1]) == 0xFFFFFFFFUL) { + n = rst_cnt[0]; + } + if (eye_min < MPFS_DDR_EYE_MIN_TAPS && + n < MPFS_DDR_EYE_RESET_MAX) { + /* A controller re-init tends to repeat the narrow window; + * only a full MSS reset re-rolls it. */ + wolfBoot_printf("DDR: DQ/DQS window %u taps, MSS reset " + "%u/%u\n", (unsigned)eye_min, (unsigned)(n + 1U), + (unsigned)MPFS_DDR_EYE_RESET_MAX); + rst_cnt[0] = n + 1U; + rst_cnt[1] = ~(n + 1U); + mb(); + udelay(20000); + SYSREG_MSS_RESET_CR = 0xDEAD; + while (1) { + __asm__ volatile("wfi"); + } + } + if (eye_min < MPFS_DDR_EYE_MIN_TAPS) { + /* The window never widened: reject the training rather + * than run DDR on a one-tap eye. The reset count stays + * spent, so the outer retries cannot start a new series. */ + wolfBoot_printf("DDR: DQ/DQS window %u taps after %u " + "resets, training rejected\n", (unsigned)eye_min, + (unsigned)n); + return -1; + } + rst_cnt[0] = 0; + rst_cnt[1] = 0; + } train_stat = DDRPHY_REG(PHY_TRAINING_STATUS); /* Fast path: if TIP completed full training (train_stat @@ -3325,16 +3345,13 @@ int mpfs_ddr_init(unsigned int outer_retry) train_stat); mtc_pass = 1; - break; - } + } while (0); if (!mtc_pass) { - wolfBoot_printf("DDR: Training/MTC failed after %u retries\n", - (unsigned)MAX_TRAIN_RETRY); + wolfBoot_printf("DDR: training attempt failed\n"); return -2; } - wolfBoot_printf("DDR: Training+MTC PASS after %u retries\n", - (unsigned)(train_retry - 1)); + wolfBoot_printf("DDR: Training+MTC PASS\n"); } /* HSS LPDDR4 POST_INITIALIZATION (mss_ddr.c:5597-5646). */ diff --git a/options.mk b/options.mk index 77c0af941c..f268203909 100644 --- a/options.mk +++ b/options.mk @@ -1607,6 +1607,14 @@ ifneq (,$(filter RISCV RISCV64,$(ARCH))) CFLAGS+=-DSTACK_SIZE_PER_HART=$(STACK_SIZE_PER_HART) endif +# L2 scratchpad region size and boot-hart stack size, substituted into the +# linker script. The scratchpad is capped by the number of scratchpad ways +# the startup asm populates (4 ways x 128 KB on MPFS250); the linker script +# asserts the limit. Defaults keep the pre-existing layout for targets that +# do not override them. +WOLFBOOT_L2SCRATCH_SIZE ?= 256k +STACK_SIZE ?= 32k + CFLAGS+=$(CFLAGS_EXTRA) LDFLAGS+=$(LDFLAGS_EXTRA) OBJS+=$(OBJS_EXTRA) diff --git a/src/boot_riscv.c b/src/boot_riscv.c index 5de8f69719..4a03fbe5f1 100644 --- a/src/boot_riscv.c +++ b/src/boot_riscv.c @@ -60,6 +60,8 @@ extern void reloc_trap_vector(const uint32_t *address); /* Minimal SBI runtime (src/riscv_sbi.c): services S-mode ecalls and the * M-mode timer/software interrupts that back the S-mode timer and IPIs. */ extern unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc); +extern unsigned long sbi_redirect_trap(unsigned long cause, unsigned long epc, + unsigned long tval); extern void sbi_timer_irq(void); extern void sbi_ipi_irq(unsigned long hartid); extern unsigned long sbi_illegal_insn(unsigned long *regs, unsigned long epc, @@ -224,6 +226,17 @@ unsigned long WEAKFUNCTION handle_trap_ex(unsigned long cause, unsigned long epc } /* not handled: fall through to the fatal dump below */ } + + /* These harts cannot delegate access faults (medeleg reads back 0xB109): + * a supervisor exception M-mode has nothing to do with is handed to the + * supervisor's own vector, so the OS gets its fault instead of a parked + * hart. Environment calls from S-mode were serviced above. */ + if ((cause & MCAUSE_INT) == 0UL && ec != 9UL) { + unsigned long nepc = sbi_redirect_trap(cause, epc, tval); + if (nepc != 0UL) { + return nepc; + } + } #endif /* Halt on synchronous exceptions to prevent infinite trap-mret loops @@ -245,8 +258,9 @@ unsigned long WEAKFUNCTION handle_trap_ex(unsigned long cause, unsigned long epc } #if defined(DEBUG_BOOT) unsigned long sp_now; - wolfBoot_printf("TRAP: cause=%lx epc=%lx tval=%lx mstatus=%lx\n", - cause, epc, tval, csr_read(mstatus)); + wolfBoot_printf("TRAP: cause=%lx epc=%lx tval=%lx mstatus=%lx " + "medeleg=%lx\n", cause, epc, tval, csr_read(mstatus), + csr_read(medeleg)); __asm__ volatile("mv %0, sp" : "=r"(sp_now)); wolfBoot_printf(" sp=%lx\n", sp_now); #if defined(WOLFBOOT_RISCV_MMODE) && defined(TARGET_mpfs250) diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 5177180389..0093aa1f98 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -67,16 +67,50 @@ _reset: sw zero, 0x174(t1) /* SYSREG_L2_SHUTDOWN_CR = 0 */ fence - /* Route E51 D-cache to scratchpad ways (8-11) for the copy. - * With the default mask (0xFF = cache ways 0-7), stores to the - * Zero Device (0x0A000000) land in cache and never reach the - * scratchpad SRAM. The I-cache later fetches from scratchpad and - * gets uninitialized data (zeros), causing illegal-instruction - * traps. Setting the mask to 0xF00 forces stores into the actual - * scratchpad SRAM. See HSS mss_l2_cache.c config_l2_cache(). */ - li t4, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ - li t5, 0xF00 /* scratchpad ways 8-11 */ - sd t5, 0(t4) + /* Populate the scratchpad ways one way at a time, so every line of + * the Zero Device (0x0A000000) is resident before anything else runs. + * + * With the default mask (0xFF = cache ways 0-7), a store to the Zero + * Device allocates in an ordinary cache way; that line is evictable and + * its writeback is discarded, so the store is silently lost. Selecting + * all four scratchpad ways at once (0xF00) is not enough either: it only + * covers the stores made while the mask is set, which used to be the + * .text copy alone. Everything after the mask was restored -- .data, + * .bss (including the SDHCI PDMA staging buffer), the heap and the E51 + * stack -- still allocated in evictable ways. + * + * Walking one way at a time is what makes the whole 512 KB resident: a + * store can only allocate in the single enabled way, so 2048 blocks x 4 + * ways fills each way exactly once. Once resident, later stores hit and + * the mask no longer matters. This is HSS config_l2_cache(), see + * mss_l2_cache.c; the marker value matches its INIT_MARKER. + * + * Constants for WAY_ENABLE=0x0B, NUM_SCRATCH_PAD_WAYS=4: + * first way = 1 << ((WAY_ENABLE + 1) - NUM_SCRATCH_PAD_WAYS) = 1 << 8 + * blocks per way = WAY_BYTE_LENGTH / CACHE_BLOCK_BYTE_LENGTH + * = 0x20000 / 64 = 2048 */ + li t0, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ + li t1, 0x100 /* scratchpad way 8 */ + li t2, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ + li t3, 0xC0FFEEBEC0010000 /* INIT_MARKER */ + li t4, 4 /* NUM_SCRATCH_PAD_WAYS */ +.L_pin_way: + sd t1, 0(t0) /* evictions allowed from this way only */ + fence + li t5, 2048 /* blocks per way */ +.L_pin_block: + sd t3, 0(t2) /* first 64-bit word of each block */ + addi t2, t2, 64 + addi t5, t5, -1 + bnez t5, .L_pin_block + slli t1, t1, 1 + fence + addi t4, t4, -1 + bnez t4, .L_pin_way + + /* Prevent the E51 from evicting from the scratchpad ways. */ + li t1, 0xFF /* cache ways 0-7 */ + sd t1, 0(t0) fence #endif @@ -97,11 +131,6 @@ _reset: addi t1, t1, 8 j .L_copy_text .L_copy_text_done: -#ifdef TARGET_mpfs250 - /* Restore D-cache to cache-only ways before normal execution */ - li t5, 0xFF /* cache ways 0-7 */ - sd t5, 0(t4) /* WAY_MASK_E51_DCACHE = cache-only */ -#endif fence rw, rw fence.i /* flush icache before jumping to SRAM */ diff --git a/src/riscv_sbi.c b/src/riscv_sbi.c index 734f475937..6e9be57925 100644 --- a/src/riscv_sbi.c +++ b/src/riscv_sbi.c @@ -114,7 +114,8 @@ #define SBI_BASE_GET_MARCHID 5 #define SBI_BASE_GET_MIMPID 6 -#define SBI_SPEC_VERSION ((0UL << 24) | 2UL) /* v0.2 */ +/* v0.3: the OS only uses SRST (reboot and power-off) from 0.3 up. */ +#define SBI_SPEC_VERSION ((0UL << 24) | 3UL) /* No registry ID is assigned to wolfBoot's SBI; report a custom value that * cannot collide with the small spec-registry IDs (0=BBL, 1=OpenSBI, 3=KVM, * 8=PolarFire HSS, ...). 0x776F6C66 = ASCII "wolf". */ @@ -157,24 +158,28 @@ typedef struct { volatile uint32_t init_magic; volatile int hart_state[MPFS_NUM_HARTS]; volatile uint32_t ipi_ops[MPFS_NUM_HARTS]; - /* Fence-completion protocol: ipi_done[h] is incremented by hart h - * only after it has executed the fence ops it consumed; a requester - * snapshots it into ipi_wait_gen[h] before posting and waits until - * ipi_done[h] passes the snapshot. */ + /* Fence-completion protocol: a requester ORs its op into ipi_ops[h] + * and then takes a ticket from ipi_posted[h]; hart h reads ipi_posted + * before it consumes ipi_ops, executes the fences, and publishes what + * it read as ipi_done[h]. A ticket is complete once ipi_done[h] has + * reached it: the fence that ran consumed an ops word that already + * held the request. A later post keeps its own ticket and is served + * by the next interrupt. */ + volatile uint32_t ipi_posted[MPFS_NUM_HARTS]; volatile uint32_t ipi_done[MPFS_NUM_HARTS]; - volatile uint32_t ipi_wait_gen[MPFS_NUM_HARTS]; } sbi_shared_state_t; #define SBI_SHARED ((sbi_shared_state_t *)SBI_SHARED_DTIM_ADDR) #define sbi_hart_state (SBI_SHARED->hart_state) #define sbi_ipi_ops (SBI_SHARED->ipi_ops) +#define sbi_ipi_posted (SBI_SHARED->ipi_posted) #define sbi_ipi_done (SBI_SHARED->ipi_done) -#define sbi_ipi_wait_gen (SBI_SHARED->ipi_wait_gen) /* Per-hart IPI work flags, set by a requesting hart and consumed in the * target hart's M-mode software-interrupt handler. */ #define SBI_IPI_OP_SSIP 1U /* inject a supervisor software interrupt */ #define SBI_IPI_OP_FENCE_I 2U /* remote instruction-stream sync */ #define SBI_IPI_OP_SFENCE 4U /* remote sfence.vma (full flush) */ +#define SBI_IPI_OP_HALT 8U /* park the hart in M-mode (system shutdown) */ /* Platform hook: release a parked hart into S-mode at saddr with a1=opaque * (SBI HSM hart_start backend). Weak default: unsupported. */ @@ -197,8 +202,8 @@ void sbi_hart_mark_started(unsigned long hartid) for (k = 0; k < (unsigned int)MPFS_NUM_HARTS; k++) { sbi_hart_state[k] = SBI_HSM_STOPPED; sbi_ipi_ops[k] = 0; + sbi_ipi_posted[k] = 0; sbi_ipi_done[k] = 0; - sbi_ipi_wait_gen[k] = 0; } __asm__ volatile("fence rw, rw" ::: "memory"); SBI_SHARED->init_magic = SBI_SHARED_MAGIC; @@ -422,14 +427,43 @@ unsigned long sbi_illegal_insn(unsigned long *regs, unsigned long epc, return 0UL; } -static void sbi_putc(char c) +/* The OS owns the UART by now and may have reconfigured or stopped it, so + * never wait on it for long with M-mode interrupts masked. Returns 1 when + * the byte was written. */ +#define SBI_UART_SPIN 200000U + +static int sbi_uart_ready(void) +{ + uint32_t n = SBI_UART_SPIN; + + while ((n > 0U) && ((SBI_UART_LSR & SBI_UART_THRE) == 0U)) { + n--; + } + return (n > 0U) ? 1 : 0; +} + +static int sbi_putc(char c) { if (c == '\n') { - while ((SBI_UART_LSR & SBI_UART_THRE) == 0U) { } + if (!sbi_uart_ready()) { + return 0; + } SBI_UART_THR = (uint8_t)'\r'; } - while ((SBI_UART_LSR & SBI_UART_THRE) == 0U) { } + if (!sbi_uart_ready()) { + return 0; + } SBI_UART_THR = (uint8_t)c; + return 1; +} + +/* Park this hart in M-mode for good. */ +static void __attribute__((noreturn)) sbi_system_halt(void) +{ + csr_clr_bits(mie, MIE_MTIP); + while (1) { + __asm__ volatile("wfi"); + } } /* Set a hart's S-mode timer. M-mode owns the timer; we program the CLINT @@ -457,15 +491,22 @@ void sbi_timer_irq(void) void sbi_ipi_irq(unsigned long hartid) { uint32_t ops = 0; + uint32_t posted = 0; CLINT_MSIP(hartid) = 0U; __asm__ volatile("fence iorw, iorw" ::: "memory"); if (hartid < (unsigned long)MPFS_NUM_HARTS) { + /* Read the ticket counter before consuming the ops: every request + * posted under a ticket acknowledged below is then already in it. */ + posted = __atomic_load_n(&sbi_ipi_posted[hartid], __ATOMIC_ACQUIRE); /* Atomically read-and-clear: a remote hart may be OR-ing a new op in * (sbi_post_ipi) concurrently with this consume. amoswap.w (rv64a) * makes the read+clear a single operation so no posted op is lost. */ ops = __atomic_exchange_n(&sbi_ipi_ops[hartid], 0U, __ATOMIC_ACQ_REL); } + if ((ops & SBI_IPI_OP_HALT) != 0U) { + sbi_system_halt(); + } if ((ops & SBI_IPI_OP_FENCE_I) != 0U) { __asm__ volatile("fence.i" ::: "memory"); } @@ -475,11 +516,9 @@ void sbi_ipi_irq(unsigned long hartid) if ((ops & SBI_IPI_OP_SSIP) != 0U || ops == 0U) { csr_set_bits(mip, MIP_SSIP); } - /* Publish completion only after the requested fences have executed: - * a requester treats the increment as this hart being done. */ - if ((ops & (SBI_IPI_OP_FENCE_I | SBI_IPI_OP_SFENCE)) != 0U) { - (void)__atomic_add_fetch(&sbi_ipi_done[hartid], 1U, - __ATOMIC_ACQ_REL); + /* Publish completion only after the requested fences have executed. */ + if (hartid < (unsigned long)MPFS_NUM_HARTS) { + __atomic_store_n(&sbi_ipi_done[hartid], posted, __ATOMIC_RELEASE); } } @@ -489,10 +528,14 @@ void sbi_ipi_irq(unsigned long hartid) * that include self rely on it), while for an RFENCE the caller has already * performed the fence locally, so self is skipped there. */ static void sbi_post_ipi(unsigned long mask, unsigned long base, - uint32_t op, unsigned long self) + uint32_t op, unsigned long self, uint32_t *ticket) { unsigned long i; unsigned long h; + + for (i = 0; i < (unsigned long)MPFS_NUM_HARTS; i++) { + ticket[i] = 0; + } /* SBI v0.2: hart_mask_base == -1 selects all available harts and * hart_mask is ignored. Normalize to (base 0, all-harts mask) so the * loop below does not compute h = (-1)+i and skip every target. */ @@ -517,30 +560,47 @@ static void sbi_post_ipi(unsigned long mask, unsigned long base, if (sbi_hart_state[h] != SBI_HSM_STARTED) { continue; /* parked harts consume MSIP in their wake loop */ } - /* Snapshot the completion counter before posting: the wait below - * completes when the target increments past this value. */ - if ((op & (SBI_IPI_OP_FENCE_I | SBI_IPI_OP_SFENCE)) != 0U) { - sbi_ipi_wait_gen[h] = sbi_ipi_done[h]; - } /* Atomic OR (amoor.w, rv64a): two harts may post to the same target * concurrently, and the target may be consuming (sbi_ipi_irq) at the * same time - a plain |= read-modify-write could drop an op. */ (void)__atomic_fetch_or(&sbi_ipi_ops[h], op, __ATOMIC_ACQ_REL); + /* Take the ticket after the op is in: the target acknowledges a + * ticket only with an ops word that already held this request. */ + if ((op & (SBI_IPI_OP_FENCE_I | SBI_IPI_OP_SFENCE)) != 0U) { + ticket[h] = __atomic_add_fetch(&sbi_ipi_posted[h], 1U, + __ATOMIC_ACQ_REL); + if (ticket[h] == 0U) { /* 0 means "nothing posted" below */ + ticket[h] = __atomic_add_fetch(&sbi_ipi_posted[h], 1U, + __ATOMIC_ACQ_REL); + } + } __asm__ volatile("fence rw, rw" ::: "memory"); CLINT_MSIP(h) = 1U; } __asm__ volatile("fence iorw, iorw" ::: "memory"); } +/* Shutdown has nowhere to go on this board: park every started hart. The + * OS has stopped the others already, but they still take timer interrupts + * and would keep running the supervisor's stall detectors. */ +static void __attribute__((noreturn)) sbi_system_shutdown(unsigned long self) +{ + uint32_t ticket[MPFS_NUM_HARTS]; + + sbi_post_ipi((1UL << MPFS_NUM_HARTS) - 1UL, 0, SBI_IPI_OP_HALT, self, + ticket); + sbi_system_halt(); +} + /* Wait (bounded) for the posted fence ops to be consumed by the targets: - * the targets increment ipi_done after executing the fences, so this - * completes only when every target has finished its fence. The SBI + * a target publishes ipi_done after executing the fences, so this + * completes only when every ticket has been reached. The SBI * remote-fence calls are synchronous; the bound guards against a wedged * target turning into a wedged caller. Returns SBI_SUCCESS when every * target completed, SBI_ERR_FAILED when any target did not complete * within the bound. */ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, - unsigned long self) + unsigned long self, const uint32_t *ticket) { unsigned long i; unsigned long h; @@ -557,20 +617,64 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, } h = base + i; if (h == self || h >= (unsigned long)MPFS_NUM_HARTS || - sbi_hart_state[h] != SBI_HSM_STARTED) { + ticket[h] == 0U) { continue; } - spin = 10000000U; - while (sbi_ipi_done[h] <= sbi_ipi_wait_gen[h] && spin > 0U) { + /* A target can be inside a console write or another ecall for tens + * of milliseconds; a fence that gives up early leaves the OS with + * stale TLBs on that hart, so wait well past that (about 1 s). + * M-mode interrupts are masked in here, so service any fence the + * target (or a third hart) posts to us meanwhile: two harts fencing + * each other would otherwise each wait for the other and time out. */ + spin = 400000000U; + while ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - + ticket[h]) < 0 && spin > 0U) { + if (sbi_ipi_ops[self] != 0U) { + sbi_ipi_irq(self); + } spin--; } - if (sbi_ipi_done[h] <= sbi_ipi_wait_gen[h]) { + if ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - + ticket[h]) < 0) { err = SBI_ERR_FAILED; } } return err; } +/* Deliver a trap that arrived in M-mode from S/U mode to the supervisor's + * trap vector, the way delegation would have: sepc/scause/stval describe it, + * sstatus records the interrupted privilege and interrupt enable, and the + * mret lands on stvec in S-mode. Returns the new epc, or 0 when the trap + * came from M-mode or the supervisor has no vector installed yet. */ +unsigned long sbi_redirect_trap(unsigned long cause, unsigned long epc, + unsigned long tval) +{ + unsigned long mpp = (csr_read(mstatus) & MSTATUS_MPP_MASK) >> + MSTATUS_MPP_SHIFT; + unsigned long stvec = csr_read(stvec) & ~3UL; + unsigned long sstatus; + + if ((mpp == (unsigned long)PRV_M) || (stvec == 0UL)) { + return 0UL; + } + csr_write(sepc, epc); + csr_write(scause, cause); + csr_write(stval, tval); + sstatus = csr_read(sstatus) & ~(MSTATUS_SPP | MSTATUS_SPIE); + if (mpp == (unsigned long)PRV_S) { + sstatus |= MSTATUS_SPP; + } + if ((sstatus & MSTATUS_SIE) != 0UL) { + sstatus |= MSTATUS_SPIE; + } + sstatus &= ~MSTATUS_SIE; + csr_write(sstatus, sstatus); + csr_clr_bits(mstatus, MSTATUS_MPP_MASK); + csr_set_bits(mstatus, MSTATUS_MPP_S); + return stvec; +} + /* Returns the (possibly advanced) PC to resume at. For ecall we skip the * 4-byte ecall instruction. */ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) @@ -580,8 +684,9 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) long err = SBI_SUCCESS; unsigned long val = 0; unsigned long hartid; - volatile uint32_t spin; /* UART-drain delay before SRST/SHUTDOWN reset */ + uint32_t ticket[MPFS_NUM_HARTS]; #ifdef DEBUG_SBI + volatile uint32_t spin; /* UART-drain delay before SRST/SHUTDOWN reset */ static uint32_t sbi_dbg_calls = 0; #endif @@ -643,7 +748,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) case SBI_EXT_IPI: if (fid == 0) { /* send_ipi(hart_mask, hart_mask_base) */ - sbi_post_ipi(regs[A0], regs[A1], SBI_IPI_OP_SSIP, hartid); + sbi_post_ipi(regs[A0], regs[A1], SBI_IPI_OP_SSIP, hartid, ticket); } else { err = SBI_ERR_NOT_SUPPORTED; } @@ -664,8 +769,8 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) err = SBI_ERR_NOT_SUPPORTED; break; } - sbi_post_ipi(regs[A0], regs[A1], op, hartid); - err = sbi_wait_ipi_done(regs[A0], regs[A1], hartid); + sbi_post_ipi(regs[A0], regs[A1], op, hartid, ticket); + err = sbi_wait_ipi_done(regs[A0], regs[A1], hartid, ticket); break; } @@ -717,9 +822,13 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) unsigned long c; unsigned long j; uint8_t cbuf[64]; - if (n > 4096UL) { - n = 4096UL; /* bound a single call; kernel loops on val */ + /* Bound a single call (the kernel loops on val): this runs in + * M-mode with interrupts off, so a long write holds up fence + * IPIs from the other harts. 256 bytes is ~25 ms at 115200. */ + if (n > 256UL) { + n = 256UL; } + val = 0; for (k = 0; k < n; k += c) { c = n - k; if (c > sizeof(cbuf)) { @@ -727,14 +836,17 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) } sbi_copy_from_smode(cbuf, gp + k, c); for (j = 0; j < c; j++) { - sbi_putc((char)cbuf[j]); + if (!sbi_putc((char)cbuf[j])) { + k = n; + break; + } + val++; } } - val = n; break; } case 2: /* console_write_byte(byte) */ - sbi_putc((char)regs[A0]); + (void)sbi_putc((char)regs[A0]); break; case 1: /* console_read -- no input wired yet */ val = 0; @@ -746,10 +858,21 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) break; case SBI_EXT_SRST: - /* system_reset(type, reason): announce, drain UART, then reset. */ + /* system_reset(type, reason). No console output here: by now the OS + * owns the UARTs and a blocked printf leaves the hart stuck in the + * handler with the reset never issued. */ +#ifdef DEBUG_SBI wolfBoot_printf("[SBI] SYSTEM RESET requested: type=0x%lx " "reason=0x%lx\n", regs[A0], regs[A1]); for (spin = 0; spin < 20000000UL; spin++) { } +#endif + if (regs[A0] == 0UL) { + sbi_system_shutdown(hartid); + } + if (regs[A0] > 2UL) { + err = SBI_ERR_INVALID_PARAM; + break; + } #ifdef TARGET_mpfs250 SYSREG_MSS_RESET_CR = 0xDEAD; #endif @@ -761,7 +884,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) regs[A0] = 0; return epc + 4; case SBI_EXT_0_1_CONSOLE_PUTCHAR: - sbi_putc((char)regs[A0]); + (void)sbi_putc((char)regs[A0]); regs[A0] = 0; return epc + 4; case SBI_EXT_0_1_CONSOLE_GETCHAR: @@ -777,7 +900,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) unsigned long lmask = 0; sbi_copy_from_smode((uint8_t *)&lmask, regs[A0], sizeof(unsigned long)); - sbi_post_ipi(lmask, 0, SBI_IPI_OP_SSIP, hartid); + sbi_post_ipi(lmask, 0, SBI_IPI_OP_SSIP, hartid, ticket); } regs[A0] = 0; return epc + 4; @@ -792,18 +915,17 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) sizeof(unsigned long)); sbi_post_ipi(fmask, 0, (eid == SBI_EXT_0_1_REMOTE_FENCE_I) ? - SBI_IPI_OP_FENCE_I : SBI_IPI_OP_SFENCE, hartid); - err = sbi_wait_ipi_done(fmask, 0, hartid); + SBI_IPI_OP_FENCE_I : SBI_IPI_OP_SFENCE, hartid, ticket); + err = sbi_wait_ipi_done(fmask, 0, hartid, ticket); } regs[A0] = (unsigned long)err; return epc + 4; case SBI_EXT_0_1_SHUTDOWN: +#ifdef DEBUG_SBI wolfBoot_printf("[SBI] legacy SHUTDOWN requested\n"); for (spin = 0; spin < 20000000UL; spin++) { } -#ifdef TARGET_mpfs250 - SYSREG_MSS_RESET_CR = 0xDEAD; #endif - while (1) { } + sbi_system_shutdown(hartid); default: err = SBI_ERR_NOT_SUPPORTED; From 790dad3a46f475f03724099a7dc0249799e2b921 Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 28 Sep 2026 14:05:08 -0700 Subject: [PATCH 2/9] PolarFire SoC: SHA-384 and AES-256-CTR offload to the Athena F5200 --- arch.mk | 16 +- hal/mpfs250.c | 588 +++++++++++++++++++++++++++++++++++++++++++++++++- hal/mpfs250.h | 34 ++- options.mk | 34 +++ 4 files changed, 656 insertions(+), 16 deletions(-) diff --git a/arch.mk b/arch.mk index 672e6e06c3..571d8670d4 100644 --- a/arch.mk +++ b/arch.mk @@ -1302,9 +1302,19 @@ ifeq ($(ARCH),RISCV64) CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany else - # U54 cores: rv64gc (with FPU) - CFLAGS+=-march=rv64imafd$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64d -mcmodel=medany - LDFLAGS+=-march=rv64imafd -mabi=lp64d -mcmodel=medany + # U54 cores: soft-float lp64, matching Microchip's published user-crypto + # (CAL) archives - both are lp64 (bare-metal-examples is rv64imafd/lp64, + # hart-software-services is rv64imac/lp64). An lp64d build cannot link + # against either. wolfBoot does no floating point, so nothing is lost. + # + # rv64imac rather than rv64imafd because this toolchain ships no + # F/D-ISA + soft-float-ABI multilib: every rv64i*f*d* variant is paired + # with lp64f/lp64d, so -march=rv64imafd -mabi=lp64 compiles but fails to + # link ("can\'t link double-float modules with soft-float modules" against + # libgcc). The U54 keeps its FPU; wolfBoot simply does not emit FP + # instructions, and CAL\'s own F/D instructions still execute fine. + CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany + LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany # FDT support for DDR S-mode (not needed for L2-LIM bare-metal boot) ifneq ($(MPFS_L2LIM),1) diff --git a/hal/mpfs250.c b/hal/mpfs250.c index fbd6021018..1079d5dc24 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -46,6 +46,26 @@ #include "gpt.h" #include "fdt.h" +#ifdef MPFS_ATHENA +#include +/* Microchip's CAL library drives the Athena F5200. Its caltypes.h and + * wolfSSL's types.h both define a type named uint128_t (a union of words there, + * __uint128_t here), so CAL's is renamed for the span of its headers. CAL is + * referenced, never vendored: it carries a Mercury Systems notice. + * INC_STDINT_H makes caltypes.h use ; both are guarded because the + * HSS copy of calpolicy.h already defines them. */ +#ifndef CALCONFIGH +#define CALCONFIGH "config_user.h" +#endif +#ifndef INC_STDINT_H +#define INC_STDINT_H +#endif +#define uint128_t cal_uint128_t +#include "calini.h" +#include "hash.h" +#include "sym.h" +#undef uint128_t +#endif #if defined(DISK_SDCARD) || defined(DISK_EMMC) #include "sdhci.h" @@ -120,7 +140,6 @@ __attribute__((noinline)) void udelay(uint32_t us) #endif /* WOLFBOOT_RISCV_MMODE */ - /* Multi-Hart Support */ #ifdef WOLFBOOT_RISCV_MMODE @@ -362,6 +381,566 @@ static int test_ext_flash(void); static void qspi_uart_program(void); #endif +#ifdef MPFS_ATHENA +/* Ungate and release the Athena F5200 (sequence per HSS opensbi_crypto_ecall.c; + * CRYPTO_CR_INFO.MSS_MODE is informational, software does the un-reset). + * Idempotent: ATHENA_CR_RESET after CALIni() silently kills AES, not hashing. + * The SCA stall countermeasure (Security UG Table 7-7/7-8) is always on at + * rate 0 (1 in 8); DPA resistance is what the "S" part is for. rdcycle, not + * mcycle: an mcycle read traps in the S-mode builds. */ +static void mpfs_athena_enable(void) +{ + static int athena_enabled; + uint64_t cyc; + + if (athena_enabled) { + return; + } + + SYSREG_SUBBLK_CLOCK_CR |= MSS_PERIPH_ATHENA; + SYSREG_SOFT_RESET_CR &= ~MSS_PERIPH_ATHENA; + __asm__ volatile("rdcycle %0" : "=r"(cyc)); + ATHENA_STALL_CR = (uint32_t)(cyc ^ (cyc >> 32)); + ATHENA_CR = ATHENA_CR_RESET | ATHENA_CR_RINGOSCON; + ATHENA_CR = ATHENA_CR_RINGOSCON | ATHENA_CR_STALL_EN; + athena_enabled = 1; +} + +/* CAL finds the engine through this global: the User Crypto base in the Libero + * design. The HSS build has it compiled in and ignores this symbol. */ +uint32_t g_user_crypto_base_addr = 0x22000000UL; + +/* Hash context handed to CAL; sized for SATRESCONTEXT and checked below so a + * CAL update cannot silently overflow it. */ +#define MPFS_ATHENA_CTX_SIZE 256 +typedef char athena_ctx_size_check[ + (MPFS_ATHENA_CTX_SIZE >= (int)sizeof(SATRESCONTEXT)) ? 1 : -1]; + +static int mpfs_athena_engine_init(void) +{ + /* CALIni() must follow the un-reset, and the core must not be reset after + * it: that silently kills AES (writes nothing, returns success). */ + mpfs_athena_enable(); + if (CALIni() != SATR_SUCCESS) { + return -1; + } + return 0; +} + +/* CAL status of the last call that failed, reported by the self-check. */ +static int athena_last_cal_rc; + +static int mpfs_athena_sha384_init(void *ctx) +{ + SATR rc = CALHashCtxIni((SATRESCONTEXTPTR)ctx, SATHASHTYPE_SHA384); + + if (rc != SATR_SUCCESS) { + athena_last_cal_rc = (int)rc; + return -1; + } + return 0; +} + +/* len must be a whole number of blocks; a partial non-final chunk is rejected + * by the engine with SATR_BADHASHLEN. */ +static int mpfs_athena_sha384_update(void *ctx, const void *in, uint32_t len) +{ + SATR rc = CALHashCtx((SATRESHANDLE)0, (SATRESCONTEXTPTR)ctx, in, + (SATUINT32_t)len, NULL, SAT_FALSE); + + if (rc != SATR_SUCCESS) { + athena_last_cal_rc = (int)rc; + return -1; + } + return 0; +} + +/* The final call may carry any remaining length, including zero. */ +static int mpfs_athena_sha384_final(void *ctx, const void *in, uint32_t len, + void *digest) +{ + SATR rc = CALHashCtx((SATRESHANDLE)0, (SATRESCONTEXTPTR)ctx, in, + (SATUINT32_t)len, digest, SAT_TRUE); + + if (rc != SATR_SUCCESS) { + athena_last_cal_rc = (int)rc; + return -1; + } + return 0; +} + +#ifdef MPFS_ATHENA_AES +/* AES-256-CTR. CTR is symmetric, so this serves encrypt and decrypt alike. + * iv is advanced in place by the engine, so a caller may chain calls. */ +static int mpfs_athena_aes256_ctr(const void *key, void *iv, const void *in, + void *out, uint32_t len) +{ + if (CALSymEncrypt(SATSYMTYPE_AES256, (const SATUINT32_t *)key, + SATSYMMODE_CTR, iv, SAT_TRUE, in, out, (SATUINT32_t)len) + != SATR_SUCCESS) { + return -1; + } + /* CALSymEncrypt only starts the transfer. Without this wait the output + * buffer is never written while both calls still report SATR_SUCCESS. */ + if (CALSymTrfRes(SAT_TRUE) != SATR_SUCCESS) { + return -1; + } + return 0; +} +#endif /* MPFS_ATHENA_AES */ +#endif /* MPFS_ATHENA */ + +#ifdef MPFS_ATHENA +/* wolfCrypt crypto callback backed by the Athena F5200: SHA-384 and + * AES-256-CTR, which together are most of wolfBoot's per-image crypto cost. + * + * The engine is reached only through the plain-C mpfs_athena_* API, because + * CAL's headers cannot share a translation unit with wolfSSL's. + * + * Hash contract (established empirically; CAL does not document it): a + * non-final update must be a whole number of blocks, 128 bytes for SHA-384, + * and only the final call may carry a remainder. wolfBoot feeds + * WOLFBOOT_SHA_BLOCK_SIZE (4096) chunks, already a multiple of 128, but an + * image tail and short hashes such as a public key are not - hence the + * partial-block buffer below. */ +#define ATHENA_SHA384_BLOCK 128 +#define ATHENA_HASH_SLOTS 2 + +struct athena_hash_slot { + void *owner; /* wc_Sha384* that owns this slot */ + uint8_t ctx[MPFS_ATHENA_CTX_SIZE]; /* opaque CAL hash context */ + uint8_t part[ATHENA_SHA384_BLOCK]; /* partial block awaiting more data */ + uint32_t part_len; + uint8_t failed; /* an update faulted: no digest from + * this context can be trusted */ +}; + +static struct athena_hash_slot athena_hash_slots[ATHENA_HASH_SLOTS]; + +/* Set once the self-check has passed: from then on a SHA-384 request the + * engine cannot take is a hardware error, never a silent software fallback. */ +static int athena_ready; + +/* Counts callback entries actually serviced by the engine, so a correct + * digest cannot be mistaken for hardware use when the callback never ran. */ +static uint32_t athena_cb_calls; + +static struct athena_hash_slot *athena_slot_find(void *owner) +{ + int i; + + for (i = 0; i < ATHENA_HASH_SLOTS; i++) { + if (athena_hash_slots[i].owner == owner) { + return &athena_hash_slots[i]; + } + } + return NULL; +} + +/* Feed whole blocks from the slot buffer plus the caller's data. Anything + * short of a block is retained for the next call or the final. */ +static int athena_hash_feed(struct athena_hash_slot *slot, const uint8_t *in, + uint32_t len) +{ + uint32_t take; + + if (slot->part_len > 0) { + take = ATHENA_SHA384_BLOCK - slot->part_len; + if (take > len) { + take = len; + } + memcpy(&slot->part[slot->part_len], in, take); + slot->part_len += take; + in += take; + len -= take; + if (slot->part_len < ATHENA_SHA384_BLOCK) { + return 0; /* still short of a block */ + } + if (mpfs_athena_sha384_update(slot->ctx, slot->part, + ATHENA_SHA384_BLOCK) != 0) { + return -1; + } + slot->part_len = 0; + } + + take = len - (len % ATHENA_SHA384_BLOCK); + if (take > 0) { + if (mpfs_athena_sha384_update(slot->ctx, in, take) != 0) { + return -1; + } + in += take; + len -= take; + } + + if (len > 0) { + memcpy(slot->part, in, len); + slot->part_len = len; + } + return 0; +} + +#if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) +/* AES-256-CTR. CTR is symmetric, so one path serves wolfBoot's encrypt and + * decrypt (both go through wc_AesCtrEncrypt). The raw key comes from + * aes->devKey, the field wolfCrypt fills for exactly this purpose; reading it + * does not depend on the key-schedule layout, and because wolfCrypt still runs + * its own key setup afterwards, declining a call here stays safe. The + * WOLF_CRYPTO_CB_SETKEY hook is deliberately not used: returning success from + * it makes wolfCrypt skip software key setup and strands the fallback. + * + * Every bail-out happens before any data reaches the engine. Afterwards + * wolfCrypt's counter no longer matches the data, so a late + * CRYPTOCB_UNAVAILABLE would silently corrupt the stream - a failure at that + * point has to be reported as an error instead. */ +static int athena_aesctr(wc_CryptoInfo *info) +{ + Aes *aes; + word32 sz; + word32 whole; + + if (info->cipher.type != WC_CIPHER_AES_CTR) { + return CRYPTOCB_UNAVAILABLE; + } + aes = info->cipher.aesctr.aes; + if (aes == NULL) { + return CRYPTOCB_UNAVAILABLE; + } + /* AES-256 only; the engine supports 128/192 but wolfBoot's encrypted + * images are AES-256 and an untested path is worse than none. */ + if (aes->keylen != 32) { + return CRYPTOCB_UNAVAILABLE; + } + /* aes->left != 0 means wolfCrypt holds keystream from a previous partial + * block; picking up mid-keystream would desync the counter. */ + if (aes->left != 0) { + return CRYPTOCB_UNAVAILABLE; + } + sz = info->cipher.aesctr.sz; + if (sz == 0) { + return 0; + } + + /* The engine advances the counter in place, so aes->reg stays correct for + * any subsequent call on the same context. */ + whole = sz - (sz % WC_AES_BLOCK_SIZE); + if (whole > 0) { + if (mpfs_athena_aes256_ctr(aes->devKey, aes->reg, + info->cipher.aesctr.in, info->cipher.aesctr.out, + whole) != 0) { + return WC_HW_E; + } + } + if (whole < sz) { + /* Trailing partial block: take one keystream block from the engine + * and leave the unused bytes where wolfCrypt keeps its own (aes->tmp + * with aes->left counting them), so the next call on this context + * continues the stream whichever path serves it. */ + uint8_t ks[WC_AES_BLOCK_SIZE]; + word32 i; + + memset(ks, 0, sizeof(ks)); + if (mpfs_athena_aes256_ctr(aes->devKey, aes->reg, ks, ks, + WC_AES_BLOCK_SIZE) != 0) { + return WC_HW_E; + } + for (i = whole; i < sz; i++) { + info->cipher.aesctr.out[i] = info->cipher.aesctr.in[i] ^ + ks[i - whole]; + } + memcpy(aes->tmp, ks, WC_AES_BLOCK_SIZE); + aes->left = WC_AES_BLOCK_SIZE - (sz - whole); + memset(ks, 0, sizeof(ks)); + } + athena_cb_calls++; + return 0; +} +#endif /* MPFS_ATHENA_AES && WOLFSSL_AES_COUNTER && !NO_AES */ + +static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) +{ + struct athena_hash_slot *slot; + + (void)devIdArg; + (void)ctx; + + if (info == NULL) { + return CRYPTOCB_UNAVAILABLE; + } + +#if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) + if (info->algo_type == WC_ALGO_TYPE_CIPHER) { + return athena_aesctr(info); + } +#endif + + if (info->algo_type != WC_ALGO_TYPE_HASH || + info->hash.type != WC_HASH_TYPE_SHA384) { + return CRYPTOCB_UNAVAILABLE; + } + + slot = athena_slot_find(info->hash.sha384); + if ((slot != NULL) && (info->hash.sha384->devCtx != slot)) { + /* Same address, but a context initialized after the one that held + * the slot was abandoned without a final: start over. */ + slot->owner = NULL; + slot->failed = 0; + slot = NULL; + } + if ((slot != NULL) && (slot->failed != 0)) { + /* An earlier update on this context faulted, so the engine holds a + * partial digest. Starting a fresh one here would digest only the + * data that follows the fault; callers that ignore an update's + * return code would then act on it. */ + return WC_HW_E; + } + if (slot == NULL) { + /* First call for this context. This is the only point where falling + * back to software is still safe: once any data has gone into the + * engine, wolfCrypt's own state is incomplete and a later + * CRYPTOCB_UNAVAILABLE would silently produce a wrong digest. */ + slot = athena_slot_find(NULL); + if (slot == NULL) { + return athena_ready ? WC_HW_E : CRYPTOCB_UNAVAILABLE; + } + if (mpfs_athena_sha384_init(slot->ctx) != 0) { + return athena_ready ? WC_HW_E : CRYPTOCB_UNAVAILABLE; + } + slot->owner = info->hash.sha384; + info->hash.sha384->devCtx = slot; + slot->part_len = 0; + slot->failed = 0; + } + + if (info->hash.in != NULL && info->hash.inSz > 0) { + if (athena_hash_feed(slot, info->hash.in, info->hash.inSz) != 0) { + slot->failed = 1; + return WC_HW_E; + } + } + + athena_cb_calls++; + + if (info->hash.digest != NULL) { + int ret = mpfs_athena_sha384_final(slot->ctx, slot->part, + slot->part_len, info->hash.digest); + slot->owner = NULL; /* release for reuse */ + slot->part_len = 0; + slot->failed = 0; + if (ret != 0) { + return WC_HW_E; + } + } + + return 0; +} + +/* Known answers for the self-check below. SHA-384 of the bytes 0x00..0xFF: + * a multi-block vector, hashed in chunks that straddle the 128-byte block so + * the intermediate state has to be carried across calls. */ +static const uint8_t athena_kat_sha384_256b[48] = { + 0xff, 0xda, 0xeb, 0xff, 0x65, 0xed, 0x05, 0xcf, + 0x40, 0x0f, 0x02, 0x21, 0xc4, 0xcc, 0xfb, 0x4b, + 0x21, 0x04, 0xfb, 0x6a, 0x51, 0xf8, 0x7e, 0x40, + 0xbe, 0x6c, 0x43, 0x09, 0x38, 0x6b, 0xfd, 0xec, + 0x28, 0x92, 0xe9, 0x17, 0x9b, 0x34, 0x63, 0x23, + 0x31, 0xa5, 0x95, 0x92, 0x73, 0x7d, 0xb5, 0xc5 +}; +#ifdef MPFS_ATHENA_AES +/* AES-256-CTR of the bytes 0x00..0x1F under key 0x00..0x1F, counter 0x00..0x0F. */ +static const uint8_t athena_kat_aesctr[32] = { + 0x5a, 0x6f, 0x06, 0x54, 0x0c, 0xfe, 0x77, 0x91, + 0xf8, 0x27, 0x5f, 0x36, 0x0e, 0xce, 0xa8, 0x9d, + 0x70, 0xe2, 0x02, 0xc6, 0xd7, 0x90, 0x4e, 0x4a, + 0x4d, 0x0f, 0xe1, 0x4a, 0x6e, 0xf8, 0x3e, 0xd0 +}; +#endif + +/* Run the offload through the wolfCrypt callback path once and compare with + * the known answers. The callback entry count is part of the check: wolfCrypt + * falls back to software silently when no device serves a call, and a correct + * result alone would not show that the hardware ran. */ +/* What the self-check tripped on, for the panic message. */ +static const char *athena_fail_what; +static int athena_fail_rc; + +static int mpfs_athena_selfcheck(void) +{ + wc_Sha384 sha; + uint8_t buf[256]; + uint8_t digest[48]; + int ret; + int i; + + athena_fail_what = NULL; + athena_fail_rc = 0; + athena_last_cal_rc = 0; + + for (i = 0; i < (int)sizeof(buf); i++) { + buf[i] = (uint8_t)i; + } + athena_cb_calls = 0; + ret = wc_InitSha384_ex(&sha, NULL, WOLFBOOT_DEVID_HASH); + if (ret != 0) { + athena_fail_what = "SHA-384 init"; + } + if (ret == 0) { + ret = wc_Sha384Update(&sha, buf, 100); + } + if (ret == 0) { + ret = wc_Sha384Update(&sha, &buf[100], 100); + } + if (ret == 0) { + ret = wc_Sha384Update(&sha, &buf[200], 56); + } + if ((ret != 0) && (athena_fail_what == NULL)) { + athena_fail_what = "SHA-384 update"; + } + if (ret == 0) { + ret = wc_Sha384Final(&sha, digest); + if (ret != 0) { + athena_fail_what = "SHA-384 final"; + } + } + wc_Sha384Free(&sha); + if (ret != 0) { + athena_fail_rc = ret; + return -1; + } + if (athena_cb_calls == 0) { + athena_fail_what = "SHA-384 callback never serviced (software fallback)"; + return -1; + } + if (memcmp(digest, athena_kat_sha384_256b, sizeof(digest)) != 0) { + athena_fail_what = "SHA-384 digest mismatch"; + return -1; + } + +#ifdef MPFS_ATHENA_AES + { + Aes aes; + uint8_t key[32], ctr[16], out[32], out2[32]; + + for (i = 0; i < 32; i++) { + key[i] = (uint8_t)i; + } + for (i = 0; i < 16; i++) { + ctr[i] = (uint8_t)i; + } + athena_cb_calls = 0; + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out, buf, 32); + } + wc_AesFree(&aes); + /* The disk decrypt path calls CTR once per chunk, so the counter + * must carry across calls: two 16-byte calls equal one 32-byte call. */ + if (ret == 0) { + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + } + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2, buf, 16); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2 + 16, buf + 16, 16); + } + wc_AesFree(&aes); + if (ret != 0) { + athena_fail_what = "AES-256-CTR"; + athena_fail_rc = ret; + return -1; + } + if (athena_cb_calls == 0) { + athena_fail_what = "AES-256-CTR callback never serviced (software fallback)"; + return -1; + } + if (memcmp(out, athena_kat_aesctr, sizeof(out)) != 0) { + athena_fail_what = "AES-256-CTR known-answer mismatch"; + return -1; + } + if (memcmp(out, out2, sizeof(out)) != 0) { + athena_fail_what = "AES-256-CTR counter does not carry across calls"; + return -1; + } + /* A length that is not a whole number of blocks, then the rest: the + * second call starts from the keystream the first one left behind. */ + memset(out2, 0, sizeof(out2)); + ret = wc_AesInit(&aes, NULL, WOLFBOOT_DEVID_CRYPT); + if (ret == 0) { + ret = wc_AesSetKeyDirect(&aes, key, 32, ctr, AES_ENCRYPTION); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2, buf, 20); + } + if (ret == 0) { + ret = wc_AesCtrEncrypt(&aes, out2 + 20, buf + 20, 12); + } + wc_AesFree(&aes); + if (ret != 0) { + athena_fail_what = "AES-256-CTR partial block"; + athena_fail_rc = ret; + return -1; + } + if (memcmp(out2, athena_kat_aesctr, sizeof(out2)) != 0) { + athena_fail_what = "AES-256-CTR partial block mismatch"; + return -1; + } + } +#endif + return 0; +} + +/* A build that asks for the hardware must not fall through to software + * crypto unnoticed: every failure here halts. */ +static void mpfs_athena_init(void) +{ + int i; + + for (i = 0; i < ATHENA_HASH_SLOTS; i++) { + athena_hash_slots[i].owner = NULL; + athena_hash_slots[i].part_len = 0; + } + + /* Free slots hold INVALID_DEVID (-2), not 0, so registration returns + * BUFFER_E until the zero-initialised device table is initialised. */ + wc_CryptoCb_Init(); + + if (mpfs_athena_engine_init() != 0) { + wolfBoot_printf("Athena: engine init failed\n"); + wolfBoot_panic(); + } + if (wc_CryptoCb_RegisterDevice(WOLFBOOT_DEVID_HASH, mpfs_athena_cryptocb, + NULL) != 0) { + wolfBoot_printf("Athena: RegisterDevice failed\n"); + wolfBoot_panic(); + } +#if defined(WOLFBOOT_DEVID_CRYPT) && (WOLFBOOT_DEVID_CRYPT != WOLFBOOT_DEVID_HASH) + if (wc_CryptoCb_RegisterDevice(WOLFBOOT_DEVID_CRYPT, mpfs_athena_cryptocb, + NULL) != 0) { + wolfBoot_printf("Athena: cipher RegisterDevice failed\n"); + wolfBoot_panic(); + } +#endif + if (mpfs_athena_selfcheck() != 0) { + wolfBoot_printf("Athena: self-check failed: %s (rc %d, CAL rc %d, " + "callbacks %u)\n", athena_fail_what, athena_fail_rc, + athena_last_cal_rc, (unsigned)athena_cb_calls); + wolfBoot_panic(); + } + athena_ready = 1; +#ifdef MPFS_ATHENA_AES + wolfBoot_printf("Athena: SHA-384 + AES-256-CTR offload active\n"); +#else + wolfBoot_printf("Athena: SHA-384 offload active\n"); +#endif +} +#endif /* MPFS_ATHENA */ + void hal_init(void) { #ifdef WOLFBOOT_RISCV_MMODE @@ -460,6 +1039,10 @@ void hal_init(void) LIBWOLFBOOT_VERSION_STRING, __DATE__, __TIME__); #endif +#ifdef MPFS_ATHENA + mpfs_athena_init(); +#endif + #ifdef WOLFBOOT_RISCV_MMODE wolfBoot_printf("Running on E51 (hart 0) in M-mode\n"); DBG_DDR("Boot WDT_E51: REFRESH=%x CTRL=%x STATUS=%x TIME=%x MVRP=%x TRIG=%x\n", @@ -938,7 +1521,6 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) return 0; } - /* Wait for SCB register bits to clear, with timeout */ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, uint32_t timeout) @@ -1456,7 +2038,6 @@ int ext_flash_erase(uintptr_t address, int len) #define QSPI_PROG_ACK 0x06 #define QSPI_RX_TIMEOUT_MS 10000U /* 10 s per byte -- aborts if host disappears */ - /* Returns 0-255 on success, -1 on timeout (so the boot path is never deadlocked). */ static int uart_qspi_rx(void) { @@ -1879,7 +2460,6 @@ void mpfs_iomux_init(void) #if defined(DISK_SDCARD) || defined(DISK_EMMC) /* SDHCI Platform HAL */ - /* MSS MPU base + per-master offset. Each AXI master (FIC0/1/2, CRYPTO, * GEM0/1, USB, MMC, SCB, TRACE) has 16 PMPCFG entries (uint64_t each) at * 0x20005000 + (master_index << 8). HSS calls mpu_configure() during early diff --git a/hal/mpfs250.h b/hal/mpfs250.h index e203c13b09..cc1876203b 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -170,7 +170,6 @@ #define MMUART_THR(base) *((volatile uint8_t*)((base)) + 0x100) /* Transmitter holding register */ #define MMUART_FCR(base) *((volatile uint8_t*)((base)) + 0x104) /* FIFO control register */ - /* LCR (Line Control Register) */ #define MSS_UART_DATA_8_BITS ((uint8_t)0x03) #define MSS_UART_NO_PARITY ((uint8_t)0x00) @@ -268,6 +267,31 @@ int mpfs_read_serial_number(uint8_t *serial); /* Crypto Engine: Athena F5200 (200 MHz) */ #define ATHENA_BASE (SYSREG_BASE + 0x125000) +/* Athena control block. Layout and bit names match athenareg_t / + * SYSREG_ATHENACR_* in config_athena.h from the Microchip user-crypto + * (CAL) library, where BASE32_ADDR_ATHENAREG is 0x20127000. */ +#define ATHENA_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x00))) +#define ATHENA_STALL_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x04))) +#define ATHENA_UPPER_ADDRESS (*((volatile uint32_t*)(ATHENA_BASE + 0x08))) +/* Security UG Table 7-7, MSS Crypto Control Register. RESET reads 1 out of + * power-on reset; MSS_OWNER reports that the MSS, not the fabric, holds the + * core, which is set by the Libero crypto ownership mode. */ +#define ATHENA_CR_RESET (1U << 0) +#define ATHENA_CR_PURGE (1U << 1) +#define ATHENA_CR_GO (1U << 2) +#define ATHENA_CR_RINGOSCON (1U << 3) +#define ATHENA_CR_STREAM_EN (1U << 4) +#define ATHENA_CR_STALL_EN (1U << 5) +#define ATHENA_CR_STALL_RATE_SHIFT 6 +#define ATHENA_CR_STALL_RATE_MASK (3U << ATHENA_CR_STALL_RATE_SHIFT) +#define ATHENA_CR_COMPLETE (1U << 8) +#define ATHENA_CR_ALARM (1U << 9) +#define ATHENA_CR_BUSERROR (1U << 10) +#define ATHENA_CR_STREAM_ENABLED (1U << 11) +#define ATHENA_CR_BUSY (1U << 12) +#define ATHENA_CR_MSS_OWNER (1U << 28) +#define ATHENA_CR_FAB_OWNER (1U << 29) + /* L2 Cache Controller (CACHE_CTRL @ 0x02010000) */ #define L2_CACHE_BASE 0x02010000UL @@ -305,7 +329,6 @@ int mpfs_read_serial_number(uint8_t *serial); #define L2_WAY_ENABLE_WITH_SCRATCH 0x0FFF #define L2_WAY_MASK_CACHE_ONLY 0xFF - /* CLINT - Core Local Interruptor */ #ifndef CLINT_BASE #define CLINT_BASE 0x02000000UL @@ -320,7 +343,6 @@ int mpfs_read_serial_number(uint8_t *serial); #define RISCV_SMODE_TIMER_FREQ MSS_CPU_CLK #endif - /* Hart Local Storage (HLS) - per-hart communication structure, 64 bytes at top of stack */ #define HLS_DEBUG_AREA_SIZE 64 @@ -387,7 +409,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #endif /* __ASSEMBLER__ */ - /* PLIC - Platform-Level Interrupt Controller (base 0x0C000000, 64MB) */ #define PLIC_BASE 0x0C000000UL #define PLIC_SIZE 0x04000000UL @@ -398,7 +419,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define PLIC_INT_MMC_MAIN 88 - /* ============================================================================ * DDR Controller and PHY (LPDDR4) - Video Kit MPFS250T * @@ -592,7 +612,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define IOSCB_IOC_REG0 0x004 #define IOSCB_IOC_REG1 0x008 - /* DDR Segment Register Offsets. * SEG is a 256-byte-stride peripheral pair (mss_seg.h:54): seg_t has * 8 x u32 control regs + 56 x u32 fill = 256 B. SEG[0] is at base @@ -630,7 +649,6 @@ void secondary_hart_entry(unsigned long hartid, HLS_DATA* hls); #define DDR_INIT_TRAINING_FAIL -2 #define DDR_INIT_MEM_TEST_FAIL -3 - /* ============================================================================ * Video Kit Clock/DDR Configuration * @@ -691,7 +709,6 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #endif #endif /* __ASSEMBLER__ */ - #ifdef EXT_FLASH /* QSPI Flash Controller * @@ -809,5 +826,4 @@ int qspi_init(void); #endif /* EXT_FLASH */ - #endif /* MPFS250_DEF_INCLUDED */ diff --git a/options.mk b/options.mk index f268203909..4ee8be824d 100644 --- a/options.mk +++ b/options.mk @@ -1671,6 +1671,40 @@ ifeq ($(WOLFBOOT_TEST_SIM_CRYPTOCB),1) endif endif +# Athena F5200 (TeraFire) offload. MPFS_ATHENA_CAL_DIR points at Microchip's +# user-crypto (CAL) directory, e.g. /services/crypto. +# Not vendored: it carries a Mercury Systems licence. +ifeq ($(MPFS_ATHENA),1) + ifeq ($(MPFS_ATHENA_CAL_DIR),) + $(error MPFS_ATHENA=1 requires MPFS_ATHENA_CAL_DIR to point at the \ + Microchip user-crypto (CAL) directory) + endif + # Both CAL archives are soft-float lp64, so callers must be too (see arch.mk). + # M-mode: hart-software-services/services/crypto. S-mode: the rv64imafd + # build from polarfire-soc-bare-metal-examples .../middleware/cal. + CFLAGS += -DMPFS_ATHENA -I$(MPFS_ATHENA_CAL_DIR) + CFLAGS += -DWOLF_CRYPTO_CB + CFLAGS += -DWOLFBOOT_DEVID_HASH=0xA7 + CFLAGS += -DWOLFBOOT_DEVID_CRYPT=0xA7 + WOLFCRYPT_OBJS += $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/cryptocb.o + # The offload and its self-check are SHA-384; an image hashed with + # something else keeps that in software and still gets the AES offload. + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif + # Archive name differs between Microchip's two published builds. + MPFS_ATHENA_CAL_LIB ?= mpfs-rv64imac-user-crypto-lib.a + LIBS += $(MPFS_ATHENA_CAL_DIR)/$(MPFS_ATHENA_CAL_LIB) + # AES-256-CTR offload rides on the AES-CTR path an AES-256 ENCRYPT build + # compiles; other builds offload the hash only. MPFS_ATHENA_AES=0 opts out. + ifeq ($(ENCRYPT_WITH_AES256),1) + MPFS_ATHENA_AES ?= 1 + ifeq ($(MPFS_ATHENA_AES),1) + CFLAGS += -DMPFS_ATHENA_AES + endif + endif +endif + # Size of the wolfHSM comm data payload, shared by the client and server blocks # below. The default is sized for certificate chains (the whole DER chain is # shipped to the HSM in a single message) and for ML-DSA keys/signatures. From ff02d77207779dd508e9e869e909404465cb3669 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 29 Sep 2026 15:46:15 -0700 Subject: [PATCH 3/9] PolarFire SoC: sNVM/PUF key storage and disk image encryption --- .github/workflows/test-configs.yml | 17 + Makefile | 20 +- arch.mk | 33 +- config/examples/polarfire_mpfs250.config | 8 +- config/examples/polarfire_mpfs250_m.config | 7 +- docs/Targets.md | 4 + docs/encrypted_partitions.md | 2 + docs/keystore.md | 4 + docs/polarfire_snvm_puf.md | 152 +++++++ hal/mpfs250-m.ld | 13 +- hal/mpfs250.c | 300 ++++++++++--- hal/mpfs250.h | 62 ++- hal/mpfs250_ddr.c | 20 +- hal/mpfs250_snvm.c | 486 +++++++++++++++++++++ hal/mpfs250_snvm.h | 145 ++++++ options.mk | 99 ++++- src/boot_riscv.c | 18 +- src/boot_riscv_start.S | 28 +- src/libwolfboot.c | 10 +- tools/unit-tests/Makefile | 24 + tools/unit-tests/unit-snvm-kek.c | 286 ++++++++++++ tools/unit-tests/unit-snvm-keystore.c | 328 ++++++++++++++ 22 files changed, 1916 insertions(+), 150 deletions(-) create mode 100644 docs/polarfire_snvm_puf.md create mode 100644 hal/mpfs250_snvm.c create mode 100644 hal/mpfs250_snvm.h create mode 100644 tools/unit-tests/unit-snvm-kek.c create mode 100644 tools/unit-tests/unit-snvm-keystore.c diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 361a08e064..cfcc8ba71f 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -521,6 +521,23 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 IMAGE_HEADER_SIZE=12288 WOLFBOOT_SECTOR_SIZE=0x4000 + # Trust anchor served from sNVM with the production hardenings: ROM-flag + # enforcement and the S-mode fence around the System Controller mailbox. + microchip_mpfs250_m_snvm_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1 MPFS_SCB_SMODE_DENY=1 + # Encrypted disk image with the AES key unwrapped by the PUF-derived KEK. + microchip_mpfs250_m_enc_kek_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 microchip_mpfs250_m_qspi_test: uses: ./.github/workflows/test-build-riscv.yml with: diff --git a/Makefile b/Makefile index 6ea253e5a3..0c1c429fbe 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,10 @@ else PRIVATE_KEY?=wolfboot_signing_private_key.der endif endif - ifeq ($(FLASH_OTP_KEYSTORE),1) + ifeq ($(SNVM_KEYSTORE),1) + # PolarFire SoC: trust anchor served from secure NVM at runtime. + MPFS_SNVM_OBJ=1 + else ifeq ($(FLASH_OTP_KEYSTORE),1) OBJS+=./src/flash_otp_keystore.o else ifeq ($(WOLFBOOT_NO_KEYSTORE),1) CFLAGS+=-DWOLFBOOT_NO_KEYSTORE @@ -188,9 +191,22 @@ else WOLFBOOT_SIGN_KEY_DEP=$(PRIVATE_KEY) else OBJS+=./src/keystore.o + # Provisioning build: compiled keys plus the helper that writes them to sNVM. + ifeq ($(SNVM_KEYSTORE_PROVISION),1) + MPFS_SNVM_OBJ=1 + endif endif endif +# PolarFire SoC: sNVM keystore, PUF KEK and the PUF-wrapped encryption-key +# provider live in one object. +ifeq ($(SNVM_KEK),1) + MPFS_SNVM_OBJ=1 +endif +ifeq ($(MPFS_SNVM_OBJ),1) + OBJS+=./hal/mpfs250_snvm.o +endif + WOLFCRYPT_OBJS:= SECURE_OBJS:= PUBLIC_KEY_OBJS:= @@ -751,7 +767,7 @@ wolfboot_stage1.bin: wolfboot.elf stage1/loader_stage1.bin $(Q) cp stage1/loader_stage1.bin wolfboot_stage1.bin wolfboot.elf: include/target.h $(LSCRIPT) $(OBJS) $(BINASSEMBLE) $(WOLFBOOT_SIGN_KEY_DEP) FORCE - $(Q)(test $(SIGN) = NONE) || (test $(FLASH_OTP_KEYSTORE) = 1) || (test "$(WOLFBOOT_NO_KEYSTORE)" = "1") || (grep -q $(SIGN_ALG) src/keystore.c) || \ + $(Q)(test $(SIGN) = NONE) || (test $(FLASH_OTP_KEYSTORE) = 1) || (test "$(WOLFBOOT_NO_KEYSTORE)" = "1") || (test "$(SNVM_KEYSTORE)" = "1") || (grep -q $(SIGN_ALG) src/keystore.c) || \ (echo "Key mismatch: please run 'make keysclean' to remove all keys if you want to change algorithm" && false) @echo "\t[LD] $@" @echo $(OBJS) diff --git a/arch.mk b/arch.mk index 571d8670d4..8c54816a73 100644 --- a/arch.mk +++ b/arch.mk @@ -1234,13 +1234,9 @@ ifeq ($(ARCH),RISCV64) endif # Use M-mode specific linker script LSCRIPT_IN:=hal/$(TARGET)-m.ld - # MPFS DDR init pulls LIBERO_SETTING_* values from a Libero/HSS-generated - # fpga_design_config.h. Setting LIBERO_FPGA_CONFIG_DIR enables DDR init - # and adds the directory to the include search path. - # - # Booting an S-mode OS means running from DDR, so an empty - # LIBERO_FPGA_CONFIG_DIR here would silently produce a bootloader with no - # DDR controller init at all. Fail the build instead. + # LIBERO_FPGA_CONFIG_DIR supplies fpga_design_config.h and enables DDR init. + # An S-mode OS runs from DDR, so an empty value would silently build a + # bootloader with no DDR init at all -- fail instead. ifeq ($(LIBERO_FPGA_CONFIG_DIR),) ifneq (,$(findstring WOLFBOOT_MMODE_SMODE_BOOT,$(CFLAGS_EXTRA) $(CFLAGS))) ifeq ($(filter clean keysclean distclean,$(MAKECMDGOALS)),) @@ -1302,19 +1298,16 @@ ifeq ($(ARCH),RISCV64) CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany else - # U54 cores: soft-float lp64, matching Microchip's published user-crypto - # (CAL) archives - both are lp64 (bare-metal-examples is rv64imafd/lp64, - # hart-software-services is rv64imac/lp64). An lp64d build cannot link - # against either. wolfBoot does no floating point, so nothing is lost. - # - # rv64imac rather than rv64imafd because this toolchain ships no - # F/D-ISA + soft-float-ABI multilib: every rv64i*f*d* variant is paired - # with lp64f/lp64d, so -march=rv64imafd -mabi=lp64 compiles but fails to - # link ("can\'t link double-float modules with soft-float modules" against - # libgcc). The U54 keeps its FPU; wolfBoot simply does not emit FP - # instructions, and CAL\'s own F/D instructions still execute fine. - CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany - LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany + # U54. Microchip's CAL archive is built rv64imac soft-float, so a build + # that links it has to match; rv64imac not rv64imafd because the toolchain + # ships no F/D-ISA + soft-float multilib. wolfBoot emits no FP either way. + ifeq ($(MPFS_ATHENA),1) + CFLAGS+=-march=rv64imac$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64 -mcmodel=medany + LDFLAGS+=-march=rv64imac -mabi=lp64 -mcmodel=medany + else + CFLAGS+=-march=rv64imafd$(RISCV64_ZICSR)$(RISCV64_ZIFENCEI) -mabi=lp64d -mcmodel=medany + LDFLAGS+=-march=rv64imafd -mabi=lp64d -mcmodel=medany + endif # FDT support for DDR S-mode (not needed for L2-LIM bare-metal boot) ifneq ($(MPFS_L2LIM),1) diff --git a/config/examples/polarfire_mpfs250.config b/config/examples/polarfire_mpfs250.config index 701e113f55..ec61ed6521 100644 --- a/config/examples/polarfire_mpfs250.config +++ b/config/examples/polarfire_mpfs250.config @@ -66,8 +66,12 @@ WOLFBOOT_LOAD_ADDRESS?=0x8E000000 # Using update_disk loader we just need to specify the partition number or A/B WOLFBOOT_NO_PARTITIONS=1 WOLFBOOT_RAMBOOT_MAX_SIZE=0x80000000 -CFLAGS_EXTRA+=-DBOOT_PART_A=1 -CFLAGS_EXTRA+=-DBOOT_PART_B=2 +# 0-based GPT index. Index 1 on the stock Microchip layout is the BIOS-boot +# partition HSS reads its own payload from, leaving one usable boot slot: A and +# B name the same one, so failover has no rollback target until a p4 is added. +# For a two-slot layout see polarfire_mpfs250_m.config (A=0, B=1). +CFLAGS_EXTRA+=-DBOOT_PART_A=0 +CFLAGS_EXTRA+=-DBOOT_PART_B=0 # ============================================================================ # Optional: read the signed image from a file on a read-only filesystem diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index 79658269c5..41a361f63a 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -36,6 +36,8 @@ SPMATHALL?=1 DUALBANK_SWAP?=0 PKA?=0 ENCRYPT=0 +# Decrypt via a staging buffer: see the SDHCI_BLOCK_VIA_PDMA note above. +DISK_DECRYPT_STAGING=1 WOLFTPM?=0 ELF?=1 #DEBUG_ELF?=1 @@ -81,9 +83,8 @@ DISK_EMMC?=0 WOLFBOOT_ORIGIN?=0x0A000000 WOLFBOOT_L2SCRATCH_SIZE?=512k -# Boot-hart stack. Sized for ML-DSA-87, whose verify working set lands on -# top of the two IMAGE_HEADER_SIZE buffers src/update_disk.c puts on the -# stack; ECC384 needs far less. +# Boot-hart stack, sized for ML-DSA-87 verify on top of the two +# IMAGE_HEADER_SIZE buffers update_disk.c puts on the stack. ECC384 needs less. STACK_SIZE?=64k # 4KB sector size (SD card flow is partition-based, not flash-erase-based) diff --git a/docs/Targets.md b/docs/Targets.md index 4fa3b7898c..41ea8da878 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1319,6 +1319,10 @@ target-independent `src/ddr_cadence.c` / `include/ddr_cadence.h` (controller bas board's `LIBERO_SETTING_*` values, stay in `hal/mpfs250_ddr.c`, which builds the controller register table and composes the generic calls. Both compile only when `MPFS_DDR_INIT` is set. +### PolarFire SoC hardware root of trust (PUF KEK, sNVM keystore, wrapped encryption key) + +The System Controller SRAM-PUF, secure NVM (sNVM), and TeraFire crypto can anchor key material in hardware: serve the verification public keys from sNVM, derive a device-unique KEK from the PUF, and store the AES image-encryption key in sNVM wrapped by that KEK. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). + ### PolarFire testing This section describes how to build the test-application, create a custom uSD with required partitions and copying signed test-application to uSD partitions. diff --git a/docs/encrypted_partitions.md b/docs/encrypted_partitions.md index c977a95f14..3c36883f5d 100644 --- a/docs/encrypted_partitions.md +++ b/docs/encrypted_partitions.md @@ -42,6 +42,8 @@ You can use the `CUSTOM_ENCRYPT_KEY` option to implement your own functions for: `wolfBoot_get_encrypt_key`, `wolfBoot_set_encrypt_key` and `wolfBoot_erase_encrypt_key`. +For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [polarfire_snvm_puf.md](polarfire_snvm_puf.md). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. + To enable: 1) Add `CUSTOM_ENCRYPT_KEY=1` to your `.config` diff --git a/docs/keystore.md b/docs/keystore.md index a61f85bcd0..d010562060 100644 --- a/docs/keystore.md +++ b/docs/keystore.md @@ -222,6 +222,10 @@ wolfBoot supports certain platforms that contain connected HSMs (Hardware Securi To support this mode of operation, the `keygen` tool supports the `--nolocalkeys` option, which instructs the tool to generate a keystore entry with a zeroed key material. It still generates the `.der` files for private and public keys, so the wolfBoot key tools can sign images, but the `keystore.c` file that is linked into wolfBoot will contain all zeros in the `pubkey` field. Because the key material isn't present in the keystore, the keypair used to sign the image and stored on the HSM for verification can be updated in the field without needing to rebuild wolfBoot against a new `keystore.c`, as long as the signature algorithm and key size does not change. Most targets that use this option will automatically add it to the key generation options or explicitly mention this step in the build documentation. +### Using KeyStore in secure NVM (PolarFire SoC sNVM) + +On Microchip PolarFire SoC the trust anchor can be served from the System Controller secure NVM (sNVM) instead of being compiled into the bootloader, using the `SNVM_KEYSTORE` backend. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). + ## Build System Integration By default, when running `make` to build the default target (`factory.bin`) for the first time, wolfBoot automatically generates a signing keypair and creates a single-key keystore as a "demonstration". This is distinct from using `keygen` directly with `-g` or `-i` options, which provides full control over keystore creation. diff --git a/docs/polarfire_snvm_puf.md b/docs/polarfire_snvm_puf.md new file mode 100644 index 0000000000..81f1083ecb --- /dev/null +++ b/docs/polarfire_snvm_puf.md @@ -0,0 +1,152 @@ +# PolarFire SoC: PUF Root of Trust, sNVM Keystore, and PUF-Wrapped Encryption Key (M-Mode) + +This document describes the PolarFire SoC MPFS250 hardware-root-of-trust features added for the wolfBoot E51 M-mode boot flow (the same flow that brings up LPDDR4 and boots a signed Yocto FIT from SD; see `Targets.md`). They let wolfBoot anchor key material in the chip's System Controller instead of compiling it into the bootloader image: + +1. A System Controller mailbox driver for secure NVM (sNVM), the SRAM-PUF, and the nonce/TRNG service. +2. An sNVM keystore backend that serves the verification public keys from sNVM at boot. +3. A device-unique Key Encryption Key (KEK) derived from the SRAM-PUF, used to wrap/unwrap individual keys. +4. A PUF-wrapped image-encryption key: the AES image key is stored in sNVM wrapped by the PUF KEK and unwrapped at boot, with the bulk image decrypted on the M-mode disk-boot path. + +All features are off by default and gated behind build flags, so a normal build is unaffected. + +## Trust model + +The intended chain, from the immutable hardware up: + +``` +PolarFire boot ROM (secure boot of wolfBoot: not configured on the Video Kit, bootmode 1) + -> wolfBoot (M-mode, eNVM) + -> System Controller SRAM-PUF (fixed challenge) -> SHA-384 -> 256-bit KEK + -> sNVM keystore: ECC384 verification public key(s) + -> sNVM: AES-256 image key, wrapped by the PUF KEK + -> verify + decrypt the signed/encrypted Yocto FIT + -> 4-CPU SMP Linux +``` + +The KEK is re-derived on every boot and never leaves the device. The public keys are not secret; storing them in sNVM (rather than the image) lets the trust anchor be provisioned and rotated per device, and binds verification to the System Controller. + +### Production requirement: the keystore pages must not stay writable + +The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: + +- Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. +- Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them: on the Video Kit Yocto image the `varlog-luks-key.service` unit, which fetches its LUKS key through the System Controller, fails and systemd waits out its device timeout before the login prompt. + +## 1. System Controller mailbox services + +`hal/mpfs250.c` drives the System Controller services mailbox (`SCBCTRL` at `0x37020000`, mailbox RAM at `0x37020800`). A small generic `mpfs_scb_request()` issues a service and polls the request/busy bits; `mpfs_scb_read()` reads the response. The following services are wrapped: + +| Service | Opcode | wolfBoot wrapper | +|---------|--------|------------------| +| Device serial number | `0x00` | `mpfs_read_serial_number()` | +| sNVM write (plaintext / authenticated / ciphertext) | `0x10` / `0x11` / `0x12` | `mpfs_snvm_write()` | +| sNVM read | `0x18` | `mpfs_snvm_read()` | +| PUF emulation | `0x20` | `mpfs_puf_emulation()` | +| Nonce (TRNG) | `0x21` | `mpfs_nonce()` | + +sNVM holds 221 modules of 252 bytes (plaintext) or 236 bytes plus a 12-byte User Secret Key (authenticated). The PUF emulation service returns a 32-byte device-unique response for a 16-byte challenge; for a fixed challenge the response is stable within a boot and across cold boots, which is what makes it usable as a KEK seed. The nonce service returns 32 random bytes. + +Note: the TRNG (nonce) and PUF services take far longer than serial/sNVM-read, so the busy-completion wait uses a larger bound (`MPFS_SCB_BUSY_TIMEOUT`) than the request-accept wait (`MPFS_SCB_TIMEOUT`). + +## 2. sNVM public-key keystore (`SNVM_KEYSTORE`) + +`hal/mpfs250_snvm.c` + `hal/mpfs250_snvm.h` implement the five-function keystore API (`keystore_num_pubkeys`, `keystore_get_buffer`, `keystore_get_size`, `keystore_get_mask`, `keystore_get_key_type`) by reading the trust anchor from sNVM. The on-sNVM layout is the same family as the OTP keystore (`src/flash_otp_keystore.c`): a `WOLFBOOT`-magic header followed by packed `struct keystore_slot` entries, read through a linear reader that spans consecutive sNVM modules from `SNVM_KEYSTORE_MODULE` (default 200, `SNVM_KEYSTORE_MAX_MODULES` default 1; an ML-DSA-87 key needs 11). For the on-device provisioning writer every module in the range must be left runtime-writable (not ROM) in the Libero sNVM configuration: on the stock Video Kit design only modules 200 and 201 are. A production bitstream delivers the same page contents as ROM instead (see the trust model above). + +Build wiring (mirrors `FLASH_OTP_KEYSTORE`): + +- `SNVM_KEYSTORE=1` links `hal/mpfs250_snvm.o` in place of the compiled-in `src/keystore.o`, so the running bootloader contains no public keys and serves them from sNVM. +- `SNVM_KEYSTORE_PROVISION=1` keeps the compiled keystore and adds `snvm_keystore_provision()`, a one-time on-device writer that copies the compiled keystore image into sNVM (called from `hal_init`). Run once on a board, then deploy the `SNVM_KEYSTORE=1` build. Both provisioning writers read the page first and skip it when it already holds the content (`snvm: module N already provisioned`), so the extra boot the programmer triggers after flashing, or a second boot of the provisioning image, does not spend another write cycle. + +A keystore page that reads back as all zeros is reported as `sNVM keystore zeroized` and halts the boot. That is the state a zeroize procedure leaves behind (the OS overwrites the keystore and encryption-key pages through the System Controller's plaintext sNVM write service), and it is distinguished from a corrupt header, which merely yields no keys, so a deliberately erased unit is identifiable on the console. Recovery is the provisioning build again. + +## 3. PUF KEK and AES key-wrap (`SNVM_KEK`) + +The KEK and key-wrap live in `hal/mpfs250_snvm.c` as well: + +- `mpfs_puf_kek()` derives the 256-bit KEK as `SHA-384(label || PUF(fixed-challenge))` truncated to 32 bytes. SHA-384 is used because it is the build's hash (SHA-256/HMAC/HKDF are compiled out); the PUF response is already a high-entropy device-unique secret, so a labelled hash provides domain separation without pulling in HKDF. +- `snvm_kek_wrap()` / `snvm_kek_unwrap()` wrap/unwrap a key with the KEK using RFC 3394 AES key-wrap (`wc_AesKeyWrap` / `wc_AesKeyUnWrap`). + +`SNVM_KEK=1` adds RFC 3394 key-wrap to the AES that an `ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1` build already compiles; the KEK has no other consumer, so the build refuses the flag without that combination. Determinism and the wrap/unwrap round trip are covered by the host unit test `tools/unit-tests/unit-snvm-kek.c`. + +The AES key-wrap uses the wolfCrypt software implementation. The Athena F5200 offload (`MPFS_ATHENA=1`, see `Targets.md`) serves SHA-384 and the bulk AES-256-CTR image decrypt through wolfCrypt crypto callbacks; the key-wrap is a few blocks per boot and stays in software. + +## 4. PUF-wrapped image-encryption key + +`hal/mpfs250_snvm.c` provides `wolfBoot_get_encrypt_key()` (under `CUSTOM_ENCRYPT_KEY`): it reads `[wrapped AES key (40 B)][nonce (16 B)]` from sNVM module `SNVM_ENCKEY_MODULE` (default 201, outside the keystore range), unwraps the key with the PUF KEK, and returns the plaintext key + nonce to the decrypt path. `snvm_enckey_provision()` (under `SNVM_ENCKEY_PROVISION`) wraps a key with the device KEK and writes the blob to sNVM. + +### Disk-image decryption enablement + +wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partition model and required `EXT_FLASH` or `MMU`. The M-mode disk-FIT path uses `WOLFBOOT_NO_PARTITIONS` and is neither. Two small, target-independent changes enable encrypted disk boot generally (regression-safe; host unit tests pass): + +- `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. +- `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. + +### PolarFire M-mode decrypt note + +The image is loaded into DDR by the SD controller and decrypted there. CPU stores into that region are not coherent with what the controller wrote (which is why the load uses `SDHCI_BLOCK_VIA_PDMA`), so `polarfire_mpfs250_m.config` sets `DISK_DECRYPT_STAGING=1`: the ciphertext is read through the non-cached DDR alias, decrypted a chunk at a time into a staging buffer and landed with the same PDMA copy the load used. + +## Build flags summary + +| Flag | Effect | +|------|--------| +| `SNVM_KEYSTORE` | Serve the public-key trust anchor from sNVM (replaces compiled keystore) | +| `SNVM_KEYSTORE_PROVISION` | Add the one-time on-device keystore writer | +| `SNVM_KEK` | Enable the PUF KEK + RFC 3394 AES key-wrap (needs the AES-256 `ENCRYPT` + `CUSTOM_ENCRYPT_KEY` build) | +| `SNVM_ENCKEY_PROVISION` | Add the one-time PUF-wrapped encryption-key writer | +| `SNVM_KEYSTORE_MODULE` / `SNVM_KEYSTORE_MAX_MODULES` / `SNVM_ENCKEY_MODULE` | sNVM module numbers (defaults 200 / 1 / 201; the key module must lie outside the keystore range and be runtime-writable) | +| `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | +| `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | +| `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | +| `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | +| `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | + +Example runtime encrypted build (compiled keystore for verification, PUF-wrapped key for decryption): + +```sh +make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR= \ + ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 wolfboot.elf +``` + +## Provisioning and validation recipe + +1. Build with `SNVM_ENCKEY_PROVISION=1` (and/or `SNVM_KEYSTORE_PROVISION=1`) plus `WOLFBOOT_SNVM_WRITE_APPROVED=1` added to the runtime build above, flash, and boot once: `hal_init` writes the PUF-wrapped AES key into sNVM[201] (and/or the keystore into sNVM[200]). +2. Sign + encrypt the FIT with a key file matching the provisioned key (`prov_aes_key` || `prov_aes_nonce` in `hal/mpfs250_snvm.c`, i.e. a 32-byte key followed by a 16-byte IV): + + ```sh + IMAGE_HEADER_SIZE=512 ./tools/keytools/sign --ecc384 --sha384 \ + --encrypt enc_key.bin --aes256 fitImage wolfboot_signing_private_key.der 1 + ``` + Note: `make keysclean` regenerates the signing key, so re-sign the image after any keysclean or the compiled keystore will not match. +3. Write `fitImage_v1_signed_and_encrypted.bin` to the SD boot partition. With the stock HSS, boot to the `>>` CLI, run `usbdmsc`, and `dd` to the first partition of the exposed disk. Do not move an SD-Wire mux while `usbdmsc` is running: HSS's MMC layer then serves one stale block for every LBA and does not recover until `usbdmsc` is restarted, which makes a populated card read as blank. +4. Flash the runtime build and cold-boot. Expected UART0: `Disk encryption enabled` -> `Decrypting image... done` -> `Firmware Valid.` -> `M->S handoff`, and UART1 reaches the Linux login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. + +## Sample validation logs (MPFS250TS Video Kit) + +The PUF response is identical for a fixed challenge across cold power cycles while the nonce service is random, and the PUF-derived KEK wraps and unwraps a test key deterministically (the same wrapped bytes on every boot); both were verified on the kit and the KEK logic is covered by `tools/unit-tests/unit-snvm-kek.c`. + +Full encrypted boot captured on the Video Kit (UART1, S-mode wolfBoot under HSS; the standalone M-mode target boots the same image through the staged PDMA decrypt): the AES key is unwrapped from sNVM with the PUF KEK, the image is loaded, decrypted and verified. + +``` +wolfBoot Version: 2.9.0 +Disk encryption enabled +... +Load address 0x8E000000 +Attempting boot from P:A +Loading image from disk...done +Decrypting image...done +Checking image integrity...done +Verifying image signature...done +Firmware Valid. +Booting at 8E000000 +PolarFire SoC MPFS250 wolfBoot demo Application +``` + +Linux (UART1): + +``` +[ 0.040726] smp: Brought up 1 node, 4 CPUs +OpenEmbedded nodistro.0 mpfs-video-kit ttyS1 +mpfs-video-kit login: +``` + +No `failed to come online`; the only error lines are the known-cosmetic eth-SGMII PHY, i2c clock-divider, and mmc-tuning messages unrelated to this feature. diff --git a/hal/mpfs250-m.ld b/hal/mpfs250-m.ld index bf012e9124..b73ccc53f9 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -26,18 +26,13 @@ MEMORY * This offset is added by mpfsBootmodeProgrammer (bootmode 1) */ FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 - /* L2 Scratchpad SRAM. Size is bounded by the number of scratchpad ways - * the startup asm populates one way at a time (see the pin loop in - * src/boot_riscv_start.S); MPFS250 hardwires ways 8-11 as scratchpad, so - * 4 x WAY_BYTE_LENGTH = 512 KB is the ceiling. Set per target with - * WOLFBOOT_L2SCRATCH_SIZE; the ASSERT below enforces the ceiling. */ + /* L2 scratchpad. MPFS250 hardwires ways 8-11, so 4 x 128 KB is the + * ceiling the startup pin loop can make resident; ASSERT below enforces it. */ L2_SCRATCH (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @WOLFBOOT_L2SCRATCH_SIZE@ } -/* Stack size for the boot hart (E51 in M-mode). Set per target with - * STACK_SIZE: ECC384 + SHA384 needs only a few KB, but ML-DSA-87 adds its - * own working set plus two IMAGE_HEADER_SIZE buffers that src/update_disk.c - * places on the stack. */ +/* Boot-hart stack. ECC384+SHA384 needs a few KB; ML-DSA-87 adds its working + * set plus the two IMAGE_HEADER_SIZE buffers update_disk.c puts on the stack. */ PROVIDE(STACK_SIZE = @STACK_SIZE@); /* 4 scratchpad ways x 128 KB (WAY_BYTE_LENGTH). Going past this hands out diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 1079d5dc24..e7297e787b 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -45,6 +45,9 @@ #include "hal.h" #include "gpt.h" #include "fdt.h" +#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) +#include "mpfs250_snvm.h" +#endif #ifdef MPFS_ATHENA #include @@ -67,6 +70,7 @@ #undef uint128_t #endif + #if defined(DISK_SDCARD) || defined(DISK_EMMC) #include "sdhci.h" @@ -140,6 +144,7 @@ __attribute__((noinline)) void udelay(uint32_t us) #endif /* WOLFBOOT_RISCV_MMODE */ + /* Multi-Hart Support */ #ifdef WOLFBOOT_RISCV_MMODE @@ -491,18 +496,9 @@ static int mpfs_athena_aes256_ctr(const void *key, void *iv, const void *in, #endif /* MPFS_ATHENA */ #ifdef MPFS_ATHENA -/* wolfCrypt crypto callback backed by the Athena F5200: SHA-384 and - * AES-256-CTR, which together are most of wolfBoot's per-image crypto cost. - * - * The engine is reached only through the plain-C mpfs_athena_* API, because - * CAL's headers cannot share a translation unit with wolfSSL's. - * - * Hash contract (established empirically; CAL does not document it): a - * non-final update must be a whole number of blocks, 128 bytes for SHA-384, - * and only the final call may carry a remainder. wolfBoot feeds - * WOLFBOOT_SHA_BLOCK_SIZE (4096) chunks, already a multiple of 128, but an - * image tail and short hashes such as a public key are not - hence the - * partial-block buffer below. */ +/* SHA-384 + AES-256-CTR via the plain-C mpfs_athena_* API (CAL headers cannot + * share a TU with wolfSSL's). Undocumented CAL contract: a non-final update + * must be whole 128-byte blocks, so short tails need the buffer below. */ #define ATHENA_SHA384_BLOCK 128 #define ATHENA_HASH_SLOTS 2 @@ -525,6 +521,9 @@ static int athena_ready; * digest cannot be mistaken for hardware use when the callback never ran. */ static uint32_t athena_cb_calls; +/* Keyed by wc_Sha384 address; the cryptocb has no free event, so a context + * freed without Final strands its slot and a later one at that address would + * resume stale CAL state (wrong digest). Only reachable via ONESHOT today. */ static struct athena_hash_slot *athena_slot_find(void *owner) { int i; @@ -580,18 +579,9 @@ static int athena_hash_feed(struct athena_hash_slot *slot, const uint8_t *in, } #if defined(MPFS_ATHENA_AES) && defined(WOLFSSL_AES_COUNTER) && !defined(NO_AES) -/* AES-256-CTR. CTR is symmetric, so one path serves wolfBoot's encrypt and - * decrypt (both go through wc_AesCtrEncrypt). The raw key comes from - * aes->devKey, the field wolfCrypt fills for exactly this purpose; reading it - * does not depend on the key-schedule layout, and because wolfCrypt still runs - * its own key setup afterwards, declining a call here stays safe. The - * WOLF_CRYPTO_CB_SETKEY hook is deliberately not used: returning success from - * it makes wolfCrypt skip software key setup and strands the fallback. - * - * Every bail-out happens before any data reaches the engine. Afterwards - * wolfCrypt's counter no longer matches the data, so a late - * CRYPTOCB_UNAVAILABLE would silently corrupt the stream - a failure at that - * point has to be reported as an error instead. */ +/* AES-256-CTR, one path for both directions. Key from aes->devKey, not the + * WOLF_CRYPTO_CB_SETKEY hook: succeeding there strands the software fallback. + * All bail-outs precede any data entering the engine (counter would desync). */ static int athena_aesctr(wc_CryptoInfo *info) { Aes *aes; @@ -694,10 +684,8 @@ static int mpfs_athena_cryptocb(int devIdArg, wc_CryptoInfo *info, void *ctx) return WC_HW_E; } if (slot == NULL) { - /* First call for this context. This is the only point where falling - * back to software is still safe: once any data has gone into the - * engine, wolfCrypt's own state is incomplete and a later - * CRYPTOCB_UNAVAILABLE would silently produce a wrong digest. */ + /* First call: the only safe point to decline, since afterwards + * wolfCrypt's own state is incomplete and the digest would be wrong. */ slot = athena_slot_find(NULL); if (slot == NULL) { return athena_ready ? WC_HW_E : CRYPTOCB_UNAVAILABLE; @@ -941,6 +929,7 @@ static void mpfs_athena_init(void) } #endif /* MPFS_ATHENA */ + void hal_init(void) { #ifdef WOLFBOOT_RISCV_MMODE @@ -1085,6 +1074,25 @@ void hal_init(void) #endif #endif + +#ifdef SNVM_KEYSTORE_PROVISION + /* One-time: write the compiled-in trust anchor into sNVM so a subsequent + * SNVM_KEYSTORE build serves its keys from sNVM. Stop on failure so a + * provisioning run cannot look successful. */ + if (snvm_keystore_provision() != 0) { + wolfBoot_printf("snvm provision: FAILED\n"); + wolfBoot_panic(); + } +#endif + +#ifdef SNVM_ENCKEY_PROVISION + /* One-time: store the PUF-wrapped image-encryption key in sNVM. */ + if (snvm_enckey_provision() != 0) { + wolfBoot_printf("enckey provision: FAILED\n"); + wolfBoot_panic(); + } +#endif + #ifdef EXT_FLASH if (qspi_init() != 0) { wolfBoot_printf("QSPI: Init failed\n"); @@ -1106,63 +1114,219 @@ static int mpfs_scb_mailbox_busy(void) return (SCBCTRL_REG(SERVICES_SR_OFFSET) & SERVICES_SR_BUSY_MASK); } -/* Read 16-byte device serial number via SCB system service (opcode 0x00). */ -int mpfs_read_serial_number(uint8_t *serial) +/* System Controller service, polling mode, mailbox word offset 0. Returns the + * 16-bit service status (0 = success) or a negative transport error. */ +static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, + uint32_t req_len) { - uint32_t cmd, status; - int i, timeout; - - if (serial == NULL) { - return -1; - } + uint32_t cmd, words, rem, i, v; + int timeout; - /* Check if mailbox is busy */ if (mpfs_scb_mailbox_busy()) { wolfBoot_printf("SCB mailbox busy\n"); return -2; } - /* Send serial number request command (opcode 0x00) - * Command format: [31:16] = opcode, [0] = request bit */ - cmd = (SYS_SERV_CMD_SERIAL_NUMBER << SERVICES_CR_COMMAND_SHIFT) | + /* Write request words into the mailbox (RMW for a non-word-aligned tail). */ + words = req_len / 4u; + for (i = 0; i < words; i++) { + v = (uint32_t)req[(i * 4u) + 0u]; + v |= ((uint32_t)req[(i * 4u) + 1u]) << 8; + v |= ((uint32_t)req[(i * 4u) + 2u]) << 16; + v |= ((uint32_t)req[(i * 4u) + 3u]) << 24; + SCBMBOX_REG(i * 4u) = v; + } + rem = req_len - (words * 4u); + if (rem > 0u) { + v = SCBMBOX_REG(words * 4u); + for (i = 0; i < rem; i++) { + v &= ~(((uint32_t)0xFFu) << (i * 8u)); + v |= ((uint32_t)req[(words * 4u) + i]) << (i * 8u); + } + SCBMBOX_REG(words * 4u) = v; + } + + /* Ensure mailbox writes land before the request is raised. */ + __asm__ volatile("fence w,w" ::: "memory"); + + /* Command: opcode in [22:16] (SERVICES_CR_COMMAND_SHIFT), REQ in bit 0; + * the mailbox word-offset field is left 0. */ + cmd = (((uint32_t)(opcode & 0x7Fu)) << SERVICES_CR_COMMAND_SHIFT) | SERVICES_CR_REQ_MASK; SCBCTRL_REG(SERVICES_CR_OFFSET) = cmd; - /* Wait for request bit to clear (command accepted) */ + /* Wait for request bit to clear (command accepted). */ timeout = MPFS_SCB_TIMEOUT; - while ((SCBCTRL_REG(SERVICES_CR_OFFSET) & SERVICES_CR_REQ_MASK) && timeout > 0) { + while ((SCBCTRL_REG(SERVICES_CR_OFFSET) & SERVICES_CR_REQ_MASK) && + (timeout > 0)) { timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB mailbox request timeout\n"); + wolfBoot_printf("SCB request timeout\n"); return -3; } - /* Wait for busy bit to clear (command completed) */ - timeout = MPFS_SCB_TIMEOUT; - while (mpfs_scb_mailbox_busy() && timeout > 0) { + /* Wait for busy bit to clear (service complete). Uses the larger + * completion bound: nonce/PUF take much longer than serial/sNVM-read. */ + timeout = MPFS_SCB_BUSY_TIMEOUT; + while (mpfs_scb_mailbox_busy() && (timeout > 0)) { timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB mailbox busy timeout\n"); + wolfBoot_printf("SCB busy timeout\n"); return -4; } - /* Check status (upper 16 bits of status register) */ - status = (SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) & 0xFFFF; - if (status != 0) { - wolfBoot_printf("SCB mailbox error: 0x%x\n", status); - return -5; + return (int)((SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) + & 0xFFFFu); +} + +/* Read len response bytes from the mailbox at byte offset off (mb word 0). */ +static void mpfs_scb_read(uint8_t *out, uint32_t off, uint32_t len) +{ + uint32_t i; + for (i = 0; i < len; i++) { + out[i] = SCBMBOX_BYTE(off + i); + } +} + +/* Read 16-byte device serial number via SCB system service (opcode 0x00). */ +int mpfs_read_serial_number(uint8_t *serial) +{ + int ret; + + if (serial == NULL) { + return -1; + } + ret = mpfs_scb_request(SYS_SERV_CMD_SERIAL_NUMBER, NULL, 0); + if (ret == 0) { + mpfs_scb_read(serial, 0, DEVICE_SERIAL_NUMBER_SIZE); + } + return ret; +} + +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t data_len) +{ + uint8_t frame[16]; + int ret, i; + + if ((data == NULL) || (module >= MPFS_SNVM_MODULE_MAX)) { + return -1; + } + if ((data_len != MPFS_SNVM_AUTH_DATA_LEN) && + (data_len != MPFS_SNVM_PLAIN_DATA_LEN)) { + return -1; + } + if ((data_len == MPFS_SNVM_AUTH_DATA_LEN) && (usk == NULL)) { + return -1; + } + + for (i = 0; i < (int)sizeof(frame); i++) { + frame[i] = 0; + } + frame[0] = module; /* bytes 1..3 reserved (0) */ + if (data_len == MPFS_SNVM_AUTH_DATA_LEN) { + for (i = 0; i < MPFS_SNVM_USK_LEN; i++) { + frame[4 + i] = usk[i]; + } } - /* Read serial number from mailbox RAM (16 bytes) */ - for (i = 0; i < DEVICE_SERIAL_NUMBER_SIZE; i++) { - serial[i] = SCBMBOX_BYTE(i); + ret = mpfs_scb_request(SYS_SERV_CMD_SNVM_READ, frame, sizeof(frame)); + if (ret != 0) { + return ret; } + /* Response: 4 admin bytes then data_len data bytes at READ_RET_OFFSET. */ + if (admin != NULL) { + mpfs_scb_read(admin, MPFS_SNVM_READ_RET_OFFSET, MPFS_SNVM_ADMIN_LEN); + } + mpfs_scb_read(data, MPFS_SNVM_READ_RET_OFFSET + MPFS_SNVM_ADMIN_LEN, + data_len); return 0; } +int mpfs_snvm_write(uint8_t format, uint8_t module, const uint8_t *data, + const uint8_t *usk) +{ + uint8_t frame[256]; + uint32_t datalen, total; + int i; + + if ((data == NULL) || (module >= MPFS_SNVM_MODULE_MAX)) { + return -1; + } + if (format == SYS_SERV_CMD_SNVM_WRITE_PLAIN) { + datalen = MPFS_SNVM_PLAIN_DATA_LEN; + total = 4u + MPFS_SNVM_PLAIN_DATA_LEN; /* 256 */ + } + else if ((format == SYS_SERV_CMD_SNVM_WRITE_AUTH) || + (format == SYS_SERV_CMD_SNVM_WRITE_CIPHER)) { + if (usk == NULL) { + return -1; + } + datalen = MPFS_SNVM_AUTH_DATA_LEN; + total = 4u + MPFS_SNVM_AUTH_DATA_LEN + MPFS_SNVM_USK_LEN; /* 252 */ + } + else { + return -1; + } + + for (i = 0; i < (int)sizeof(frame); i++) { + frame[i] = 0; + } + frame[0] = module; /* bytes 1..3 reserved (0) */ + for (i = 0; i < (int)datalen; i++) { + frame[4 + i] = data[i]; + } + if (datalen == MPFS_SNVM_AUTH_DATA_LEN) { + for (i = 0; i < MPFS_SNVM_USK_LEN; i++) { + frame[4 + MPFS_SNVM_AUTH_DATA_LEN + i] = usk[i]; + } + } + + return mpfs_scb_request(format, frame, total); +} + +int mpfs_puf_emulation(const uint8_t *challenge, uint8_t op_type, + uint8_t *response) +{ + uint8_t frame[20]; + int ret, i; + + if ((challenge == NULL) || (response == NULL)) { + return -1; + } + for (i = 0; i < (int)sizeof(frame); i++) { + frame[i] = 0; + } + frame[0] = op_type; /* bytes 1..3 reserved (0) */ + for (i = 0; i < MPFS_PUF_CHALLENGE_LEN; i++) { + frame[4 + i] = challenge[i]; + } + + ret = mpfs_scb_request(SYS_SERV_CMD_PUF_EMULATION, frame, sizeof(frame)); + if (ret == 0) { + mpfs_scb_read(response, MPFS_PUF_RET_OFFSET, MPFS_PUF_RESPONSE_LEN); + } + return ret; +} + +int mpfs_nonce(uint8_t *nonce) +{ + int ret; + + if (nonce == NULL) { + return -1; + } + ret = mpfs_scb_request(SYS_SERV_CMD_NONCE, NULL, 0); + if (ret == 0) { + mpfs_scb_read(nonce, 0, MPFS_NONCE_LEN); + } + return ret; +} + + /* Linux kernel command line arguments */ /* Must stay below the fdt.h include: the LINUX_BOOTARGS_OVERRIDE default * there keys off build-supplied macros only, not this fallback. */ @@ -1197,7 +1361,13 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) * when an OS-side petting story exists. */ static const char *const cpu_off[] = { "watchdog@20001000", "watchdog@20101000", "watchdog@20103000", - "watchdog@20105000", "watchdog@20107000" }; + "watchdog@20105000", "watchdog@20107000", +#ifdef MPFS_SCB_SMODE_DENY + /* PMP fences S-mode off the System Controller mailbox: the OS + * drivers for it would only fault. */ + "mailbox@37020800", "spi@37020100", "syscontroller", +#endif + }; unsigned int i; #endif @@ -1327,6 +1497,21 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) return 0; } +#ifdef DISK_DECRYPT_STAGING +/* Ciphertext is read through the non-cached DDR alias and plaintext is landed + * with the PDMA copy below: CPU stores into DDR are not coherent with what the + * SD controller wrote (see polarfire_mpfs250_m.config). */ +uintptr_t hal_disk_decrypt_addr(uintptr_t addr) +{ + return addr | 0x40000000UL; +} + +int hal_disk_decrypt_copy(void *dst, const void *src, uint32_t len) +{ + return wolfBoot_fit_memcpy(dst, src, len); +} +#endif + #if defined(WOLFBOOT_RISCV_MMODE) && defined(MPFS_DDR_INIT) /* FIT subimage copy via PDMA (overrides the weak default in src/fdt.c). * CPU writes to DDR do not land on this board, so route kernel/dtb copies @@ -1521,6 +1706,7 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) return 0; } + /* Wait for SCB register bits to clear, with timeout */ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, uint32_t timeout) @@ -2038,6 +2224,7 @@ int ext_flash_erase(uintptr_t address, int len) #define QSPI_PROG_ACK 0x06 #define QSPI_RX_TIMEOUT_MS 10000U /* 10 s per byte -- aborts if host disappears */ + /* Returns 0-255 on success, -1 on timeout (so the boot path is never deadlocked). */ static int uart_qspi_rx(void) { @@ -2460,6 +2647,7 @@ void mpfs_iomux_init(void) #if defined(DISK_SDCARD) || defined(DISK_EMMC) /* SDHCI Platform HAL */ + /* MSS MPU base + per-master offset. Each AXI master (FIC0/1/2, CRYPTO, * GEM0/1, USB, MMC, SCB, TRACE) has 16 PMPCFG entries (uint64_t each) at * 0x20005000 + (master_index << 8). HSS calls mpu_configure() during early diff --git a/hal/mpfs250.h b/hal/mpfs250.h index cc1876203b..48ae21598b 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -236,13 +236,39 @@ /* System Service command opcodes */ #define SYS_SERV_CMD_SERIAL_NUMBER 0x00u #define SYS_SERV_CMD_SPI_COPY 0x50u /* SCB mailbox SPI copy service */ +#define SYS_SERV_CMD_SNVM_WRITE_PLAIN 0x10u /* non-authenticated plaintext */ +#define SYS_SERV_CMD_SNVM_WRITE_AUTH 0x11u /* authenticated plaintext */ +#define SYS_SERV_CMD_SNVM_WRITE_CIPHER 0x12u /* authenticated ciphertext */ +#define SYS_SERV_CMD_SNVM_READ 0x18u +#define SYS_SERV_CMD_PUF_EMULATION 0x20u +#define SYS_SERV_CMD_NONCE 0x21u + +/* sNVM service parameters (see Microchip system services spec) */ +#define MPFS_SNVM_MODULE_MAX 221 /* modules 0..220 */ +#define MPFS_SNVM_USK_LEN 12 /* user secret key (authenticated modes) */ +#define MPFS_SNVM_AUTH_DATA_LEN 236 /* data bytes per authenticated page */ +#define MPFS_SNVM_PLAIN_DATA_LEN 252 /* data bytes per non-authenticated page */ +#define MPFS_SNVM_ADMIN_LEN 4 /* page admin bytes returned by read */ +/* Mailbox response byte offsets (mb_offset 0) */ +#define MPFS_SNVM_READ_RET_OFFSET 16 +#define MPFS_PUF_RET_OFFSET 20 + +/* PUF emulation / nonce service sizes */ +#define MPFS_PUF_CHALLENGE_LEN 16 +#define MPFS_PUF_RESPONSE_LEN 32 +#define MPFS_NONCE_LEN 32 /* Device serial number size in bytes */ #define DEVICE_SERIAL_NUMBER_SIZE 16 /* Timeout loop iteration counts (override at build time via CFLAGS) */ #ifndef MPFS_SCB_TIMEOUT -#define MPFS_SCB_TIMEOUT 10000 /* SCB mailbox polling */ +#define MPFS_SCB_TIMEOUT 10000 /* SCB request-accept polling */ +#endif +/* Completion (BUSY) wait. TRNG and PUF services are far slower than + * serial/sNVM reads; only a wedged controller reaches this bound. */ +#ifndef MPFS_SCB_BUSY_TIMEOUT +#define MPFS_SCB_BUSY_TIMEOUT 20000000 #endif #ifndef QSPI_TIMEOUT_TRIES #define QSPI_TIMEOUT_TRIES 100000 /* QSPI controller/TX polling */ @@ -256,26 +282,40 @@ #define SCBMBOX_REG(off) (*((volatile uint32_t*)(SCBMBOX_BASE + (off)))) #define SCBMBOX_BYTE(off) (*((volatile uint8_t*)(SCBMBOX_BASE + (off)))) -/* System Controller Mailbox API */ +/* System Controller mailbox, polling mode at word offset 0. Returns 0, a + * negative transport error, or a positive 16-bit service status. */ #ifndef __ASSEMBLER__ -int mpfs_scb_service_call(uint8_t opcode, const uint8_t *mb_data, - uint32_t mb_len, uint32_t timeout); -int mpfs_scb_read_mailbox(uint8_t *out, uint32_t len); int mpfs_read_serial_number(uint8_t *serial); + +/* Read one sNVM module. data_len 236 (AUTH, needs usk) or 252 (PLAIN, usk + * may be NULL); admin is 4 bytes and optional. */ +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t data_len); + +/* Write one sNVM module. data is 252 bytes for PLAIN, else 236; the 12-byte + * usk is required for AUTH/CIPHER. */ +int mpfs_snvm_write(uint8_t format, uint8_t module, const uint8_t *data, + const uint8_t *usk); + +/* PUF emulation: 16-byte challenge -> 32-byte device-unique response. */ +int mpfs_puf_emulation(const uint8_t *challenge, uint8_t op_type, + uint8_t *response); + +/* Nonce service: 32-byte random value. */ +int mpfs_nonce(uint8_t *nonce); + #endif /* __ASSEMBLER__ */ /* Crypto Engine: Athena F5200 (200 MHz) */ #define ATHENA_BASE (SYSREG_BASE + 0x125000) -/* Athena control block. Layout and bit names match athenareg_t / - * SYSREG_ATHENACR_* in config_athena.h from the Microchip user-crypto - * (CAL) library, where BASE32_ADDR_ATHENAREG is 0x20127000. */ +/* Athena control block, matching athenareg_t in the CAL library's + * config_athena.h (BASE32_ADDR_ATHENAREG = 0x20127000). */ #define ATHENA_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x00))) #define ATHENA_STALL_CR (*((volatile uint32_t*)(ATHENA_BASE + 0x04))) #define ATHENA_UPPER_ADDRESS (*((volatile uint32_t*)(ATHENA_BASE + 0x08))) -/* Security UG Table 7-7, MSS Crypto Control Register. RESET reads 1 out of - * power-on reset; MSS_OWNER reports that the MSS, not the fabric, holds the - * core, which is set by the Libero crypto ownership mode. */ +/* Security UG Table 7-7. RESET reads 1 out of power-on reset; MSS_OWNER says + * the Libero ownership mode gave the core to the MSS rather than the fabric. */ #define ATHENA_CR_RESET (1U << 0) #define ATHENA_CR_PURGE (1U << 1) #define ATHENA_CR_GO (1U << 2) diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index cf990986b6..b57fe62b8c 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -518,17 +518,9 @@ static void setup_segments(void) mb(); } -/* DDR controller configuration, in HSS init_ddrc() order. - * - * Values come from the Video Kit Libero header - * hart-software-services/build/boards/mpfs-video-kit/fpga_design_config/ - * ddr/hw_ddrc.h - * - * ddr_cadence_controller_setup() writes the table in array order, so row - * order is write order. That matters at CTRLR_SOFT_RESET_N below: the PHY - * training, MTC and AXI_IF blocks are programmed only after the controller - * is released from soft reset. - */ +/* DDR controller configuration in HSS init_ddrc() order, values from the Libero + * ddr/hw_ddrc.h. Row order is write order, which matters at + * CTRLR_SOFT_RESET_N: PHY training and MTC must be programmed after it. */ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x2400, LIBERO_SETTING_CFG_MANUAL_ADDRESS_MAP }, { 0x2404, LIBERO_SETTING_CFG_CHIPADDR_MAP }, @@ -638,10 +630,8 @@ static const ddr_cadence_reg_t mpfs_ddrc_regs[] = { { 0x3D74, LIBERO_SETTING_CFG_FAW_DLR }, { 0x3D98, LIBERO_SETTING_CFG_ADVANCE_ACTIVATE_READY }, - /* Everything above is programmed with the controller still in soft - * reset. This write releases it; HSS init_ddrc() programs the PHY - * training, MTC and AXI_IF blocks only after this point, so the order - * of the rows below is a sequencing requirement, not a preference. */ + /* Releases the controller from soft reset. Rows below are the PHY + * training, MTC and AXI_IF blocks and must follow it, not precede it. */ { MC_CTRLR_SOFT_RESET_N, LIBERO_SETTING_CTRLR_SOFT_RESET_N }, { MC_CFG_LOOKAHEAD_PCH, LIBERO_SETTING_CFG_LOOKAHEAD_PCH }, diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c new file mode 100644 index 0000000000..aaf2531350 --- /dev/null +++ b/hal/mpfs250_snvm.c @@ -0,0 +1,486 @@ +/* mpfs250_snvm.c + * + * PolarFire SoC secure NVM (sNVM) and SRAM-PUF key material for wolfBoot: a + * keystore backend that serves the trust anchor from sNVM, a PUF-derived key + * encryption key with RFC 3394 AES key-wrap, and the image-encryption key + * provider that unwraps a PUF-wrapped AES key stored in sNVM. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include "wolfboot/wolfboot.h" +#include "keystore.h" +#include "hal.h" +#include "hal/mpfs250.h" +#include "hal/mpfs250_snvm.h" +#include "printf.h" +#include "loader.h" +#include "encrypt.h" +#include +#include +#include +#include +#include /* wc_ForceZero */ + +#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) +/* Provisioning runs from hal_init() on every boot of a provisioning image, + * and the programmer reboots the board once on its own, so a page that + * already holds the content is left alone: sNVM pages have a write-cycle + * budget and a brownout mid-write can lock one read-only. */ +static int snvm_page_write_if_changed(uint8_t module, const uint8_t *data) +{ + static uint8_t cur[MPFS_SNVM_PLAIN_DATA_LEN]; + + if (mpfs_snvm_read(module, NULL, NULL, cur, sizeof(cur)) != 0) { + /* Never spend a write cycle on a page that could not be read. */ + wolfBoot_printf("snvm: module %u read failed, not written\n", + (unsigned)module); + return -1; + } + if (memcmp(cur, data, sizeof(cur)) == 0) { + wolfBoot_printf("snvm: module %u already provisioned\n", + (unsigned)module); + return 0; + } + return mpfs_snvm_write(SYS_SERV_CMD_SNVM_WRITE_PLAIN, module, data, NULL); +} +#endif + +#if defined(SNVM_KEYSTORE) && !defined(WOLFBOOT_NO_SIGN) + +static uint8_t snvm_page_cache[SNVM_KEYSTORE_PAGE_DATA]; +static uint8_t snvm_slot_cache[SIZEOF_KEYSTORE_SLOT]; + +/* Read from the logical keystore image (header + slots) spanning consecutive + * sNVM modules. 0 on success, -1 on read error or out-of-range module. */ +static int snvm_keystore_read(uint32_t offset, uint8_t *out, uint32_t len) +{ + uint32_t got = 0; + uint32_t mod, moff, n; + + while (got < len) { + mod = (uint32_t)SNVM_KEYSTORE_MODULE + + ((offset + got) / SNVM_KEYSTORE_PAGE_DATA); + moff = (offset + got) % SNVM_KEYSTORE_PAGE_DATA; + if (mod >= (uint32_t)MPFS_SNVM_MODULE_MAX) { + return -1; + } + if (mpfs_snvm_read((uint8_t)mod, NULL, NULL, snvm_page_cache, + SNVM_KEYSTORE_PAGE_DATA) != 0) { + return -1; + } + n = SNVM_KEYSTORE_PAGE_DATA - moff; + if (n > (len - got)) { + n = len - got; + } + memcpy(out + got, snvm_page_cache + moff, n); + got += n; + } + return 0; +} + +#ifdef SNVM_KEYSTORE_REQUIRE_ROM +/* The ROM flag is a bitstream property of the page (sNVM client marked ROM + * in Libero): the write service cannot set or clear it, so a page that + * carries it cannot be rewritten by anything running on the device. The + * admin word is little-endian; ROMFLAG is bit 23. */ +static int snvm_keystore_require_rom(void) +{ + static uint8_t scratch[MPFS_SNVM_PLAIN_DATA_LEN]; + uint8_t admin[MPFS_SNVM_ADMIN_LEN]; + unsigned int m; + + for (m = 0; m < SNVM_KEYSTORE_MAX_MODULES; m++) { + if (mpfs_snvm_read((uint8_t)(SNVM_KEYSTORE_MODULE + m), NULL, admin, + scratch, sizeof(scratch)) != 0) { + wolfBoot_printf("sNVM keystore module %u read failed\n", + (unsigned)(SNVM_KEYSTORE_MODULE + m)); + return -1; + } + if ((admin[2] & 0x80U) == 0U) { + wolfBoot_printf("sNVM keystore module %u is not ROM " + "(admin %02x%02x%02x%02x)\n", + (unsigned)(SNVM_KEYSTORE_MODULE + m), admin[3], admin[2], + admin[1], admin[0]); + return -1; + } + } + return 0; +} +#endif + +int keystore_num_pubkeys(void) +{ + uint8_t hdr_buf[SNVM_KEYSTORE_HDR_SIZE]; + struct snvm_keystore_hdr *hdr = (struct snvm_keystore_hdr *)hdr_buf; +#ifdef SNVM_KEYSTORE_REQUIRE_ROM + static int rom_checked; + + if (!rom_checked) { + if (snvm_keystore_require_rom() != 0) { + wolfBoot_panic(); + } + rom_checked = 1; + } +#endif + + if (snvm_keystore_read(0, hdr_buf, SNVM_KEYSTORE_HDR_SIZE) != 0) { + return 0; + } + if (memcmp(hdr->magic, SNVM_KEYSTORE_MAGIC, 8) != 0) { + /* A whole first page of zeros (still in the page cache from the + * header read) is a zeroized keystore, not a provisioning mistake: + * say so and stop, there is no trust anchor to verify with. A page + * that is merely corrupt yields no keys below. */ + if (snvm_page_cache[0] == 0 && + memcmp(snvm_page_cache, snvm_page_cache + 1, + SNVM_KEYSTORE_PAGE_DATA - 1) == 0) { + wolfBoot_printf("sNVM keystore zeroized\n"); + wolfBoot_panic(); + } + return 0; + } + if (hdr->item_count > SNVM_KEYSTORE_MAX_PUBKEYS) { + return 0; + } + return (int)hdr->item_count; +} + +/* Load slot id into the static cache; returns NULL on any error. */ +static struct keystore_slot *snvm_load_slot(int id) +{ + if ((id < 0) || (id >= keystore_num_pubkeys())) { + return (struct keystore_slot *)0; + } + if (snvm_keystore_read(SNVM_KEYSTORE_HDR_SIZE + + ((uint32_t)id * SIZEOF_KEYSTORE_SLOT), snvm_slot_cache, + SIZEOF_KEYSTORE_SLOT) != 0) { + return (struct keystore_slot *)0; + } + return (struct keystore_slot *)snvm_slot_cache; +} + +uint8_t *keystore_get_buffer(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return (uint8_t *)0; + } + return slot->pubkey; +} + +int keystore_get_size(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return -1; + } + /* Reject an over-large size from a corrupt/mis-provisioned slot so callers + * cannot read past the fixed-size cache. */ + if (slot->pubkey_size > KEYSTORE_PUBKEY_SIZE) { + return -1; + } + return (int)slot->pubkey_size; +} + +uint32_t keystore_get_mask(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return 0; + } + return slot->part_id_mask; +} + +uint32_t keystore_get_key_type(int id) +{ + struct keystore_slot *slot = snvm_load_slot(id); + if (slot == NULL) { + return (uint32_t)-1; + } + return slot->key_type; +} + +#endif /* SNVM_KEYSTORE && !WOLFBOOT_NO_SIGN */ + +#if defined(SNVM_KEYSTORE_PROVISION) && !defined(WOLFBOOT_NO_SIGN) + +/* Write the compiled-in keystore into sNVM page by page. Run once; an + * SNVM_KEYSTORE build then serves the trust anchor from sNVM. Public keys only. */ +int snvm_keystore_provision(void) +{ + static uint8_t img[SNVM_KEYSTORE_MAX_MODULES * SNVM_KEYSTORE_PAGE_DATA]; + struct snvm_keystore_hdr *hdr = (struct snvm_keystore_hdr *)img; + struct keystore_slot slot; + uint8_t *pub; + uint32_t total, modules, m, off; + int n, i, sz; + + n = keystore_num_pubkeys(); + if (n <= 0) { + wolfBoot_printf("snvm provision: no compiled keys\n"); + return -1; + } + total = SNVM_KEYSTORE_HDR_SIZE + ((uint32_t)n * SIZEOF_KEYSTORE_SLOT); + if (total > sizeof(img)) { + wolfBoot_printf("snvm provision: image too large (%u)\n", + (unsigned)total); + return -1; + } + + memset(img, 0, sizeof(img)); + memcpy(hdr->magic, SNVM_KEYSTORE_MAGIC, 8); + hdr->item_count = (uint16_t)n; + hdr->flags = 0; + hdr->version = 0; + + for (i = 0; i < n; i++) { + memset(&slot, 0, sizeof(slot)); + sz = keystore_get_size(i); + pub = keystore_get_buffer(i); + if ((sz < 0) || (pub == NULL)) { + return -1; + } + slot.slot_id = (uint32_t)i; + slot.key_type = keystore_get_key_type(i); + slot.part_id_mask = keystore_get_mask(i); + slot.pubkey_size = (uint32_t)sz; + memcpy(slot.pubkey, pub, (uint32_t)sz); + memcpy(&img[SNVM_KEYSTORE_HDR_SIZE + ((uint32_t)i * SIZEOF_KEYSTORE_SLOT)], + &slot, SIZEOF_KEYSTORE_SLOT); + } + + modules = (total + SNVM_KEYSTORE_PAGE_DATA - 1u) / SNVM_KEYSTORE_PAGE_DATA; + for (m = 0; m < modules; m++) { + off = m * SNVM_KEYSTORE_PAGE_DATA; + if (snvm_page_write_if_changed((uint8_t)(SNVM_KEYSTORE_MODULE + m), + &img[off]) != 0) { + wolfBoot_printf("snvm provision: write module %u failed\n", + (unsigned)(SNVM_KEYSTORE_MODULE + m)); + return -1; + } + } + wolfBoot_printf("snvm provision: wrote %d key(s) in %u module(s)\n", n, + (unsigned)modules); + return 0; +} + +#endif /* SNVM_KEYSTORE_PROVISION && !WOLFBOOT_NO_SIGN */ + +#ifdef SNVM_KEK + +/* Fixed PUF challenge. Any constant works: the same challenge returns the + * same device-unique response across cold boots, anchoring a stable KEK. */ +static const uint8_t snvm_kek_challenge[MPFS_PUF_CHALLENGE_LEN] = { + 0x77, 0x6f, 0x6c, 0x66, 0x42, 0x6f, 0x6f, 0x74, + 0x4b, 0x45, 0x4b, 0x76, 0x31, 0x00, 0x00, 0x00 +}; +/* Domain-separation label hashed with the PUF response. */ +static const char snvm_kek_label[] = "wolfBoot-sNVM-KEK-v1"; + +int mpfs_puf_kek(uint8_t *kek) +{ + uint8_t resp[MPFS_PUF_RESPONSE_LEN]; + uint8_t digest[WC_SHA384_DIGEST_SIZE]; + wc_Sha384 sha; + int ret; + + if (kek == NULL) { + return -1; + } + ret = mpfs_puf_emulation(snvm_kek_challenge, 0, resp); + if (ret != 0) { + return -1; + } + ret = wc_InitSha384(&sha); + if (ret == 0) { + ret = wc_Sha384Update(&sha, (const byte *)snvm_kek_label, + (word32)(sizeof(snvm_kek_label) - 1)); + if (ret == 0) { + ret = wc_Sha384Update(&sha, resp, (word32)sizeof(resp)); + } + if (ret == 0) { + ret = wc_Sha384Final(&sha, digest); + } + wc_Sha384Free(&sha); + /* the context buffered the whole sub-block message, PUF response included */ + wc_ForceZero(&sha, sizeof(sha)); + } + /* KEK = first 256 bits of SHA-384(label || PUF response). */ + if (ret == 0) { + memcpy(kek, digest, SNVM_KEK_LEN); + } + wc_ForceZero(resp, sizeof(resp)); + wc_ForceZero(digest, sizeof(digest)); + return (ret == 0) ? 0 : -1; +} + +int snvm_kek_wrap(const uint8_t *key, uint32_t keysz, uint8_t *out, + uint32_t outsz) +{ + uint8_t kek[SNVM_KEK_LEN]; + int ret; + + if ((key == NULL) || (out == NULL)) { + return -1; + } + if (mpfs_puf_kek(kek) != 0) { + return -1; + } + ret = wc_AesKeyWrap(kek, (word32)sizeof(kek), key, keysz, out, outsz, NULL); + wc_ForceZero(kek, sizeof(kek)); + return ret; +} + +int snvm_kek_unwrap(const uint8_t *in, uint32_t insz, uint8_t *key, + uint32_t keysz) +{ + uint8_t kek[SNVM_KEK_LEN]; + int ret; + + if ((in == NULL) || (key == NULL)) { + return -1; + } + if (mpfs_puf_kek(kek) != 0) { + return -1; + } + ret = wc_AesKeyUnWrap(kek, (word32)sizeof(kek), in, insz, key, keysz, NULL); + wc_ForceZero(kek, sizeof(kek)); + return ret; +} + + +#endif /* SNVM_KEK */ + +#if defined(CUSTOM_ENCRYPT_KEY) && defined(SNVM_KEK) + +/* sNVM module SNVM_ENCKEY_MODULE holds: [wrapped AES key (40)][nonce (N)]. */ +#define SNVM_ENCKEY_BLOB_LEN (SNVM_KEK_WRAPPED_LEN + ENCRYPT_NONCE_SIZE) +#if SNVM_ENCKEY_BLOB_LEN > MPFS_SNVM_PLAIN_DATA_LEN +#error "wrapped key + nonce does not fit one plaintext sNVM page" +#endif + +#if (ENCRYPT_KEY_SIZE != SNVM_KEK_AESKEY_LEN) +#error "SNVM PUF encrypt-key provider expects a 256-bit (AES-256) image key" +#endif + +/* Read the wrapped AES key + nonce from sNVM, unwrap the key with the PUF KEK + * and return both. Returns 0 on success. */ +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + int ret; + + if ((key == NULL) || (nonce == NULL)) { + return -1; + } + /* Fail closed: a caller that ignores the return must never see stale + * or candidate key material in either output. */ + wc_ForceZero(key, ENCRYPT_KEY_SIZE); + wc_ForceZero(nonce, ENCRYPT_NONCE_SIZE); + ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, + MPFS_SNVM_PLAIN_DATA_LEN); + if (ret != 0) { + wolfBoot_printf("encrypt-key: sNVM read failed (%d)\n", ret); + return -1; + } + ret = snvm_kek_unwrap(page, SNVM_KEK_WRAPPED_LEN, key, ENCRYPT_KEY_SIZE); + if (ret != (int)ENCRYPT_KEY_SIZE) { + wolfBoot_printf("encrypt-key: PUF unwrap failed (%d)\n", ret); + wc_ForceZero(key, ENCRYPT_KEY_SIZE); + wc_ForceZero(page, sizeof(page)); + return -1; + } + memcpy(nonce, page + SNVM_KEK_WRAPPED_LEN, ENCRYPT_NONCE_SIZE); + wc_ForceZero(page, sizeof(page)); + return 0; +} + +/* Provisioned out-of-band, so runtime set/erase are unused by the decrypt + * path. Neither can be honoured at runtime, so both fail closed: an update + * flow that relies on them must not believe a key was stored or erased. */ +int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce) +{ + (void)key; + (void)nonce; + return -1; +} + +int wolfBoot_erase_encrypt_key(void) +{ + return -1; +} + +#ifdef SNVM_ENCKEY_PROVISION +/* One-time: wrap the AES key with the device PUF KEK and store + * [wrapped key][nonce] in sNVM. It must match the sign tool's key file. + * The key comes from snvm_enckey_prov_key/nonce, defined by the integrator in + * another object (SNVM_ENCKEY_PROVISION_EXTERN), or from the built-in public + * test vector, which needs SNVM_ENCKEY_INSECURE_TEST_KEY as acknowledgement. */ +#if defined(SNVM_ENCKEY_PROVISION_EXTERN) +extern const uint8_t snvm_enckey_prov_key[SNVM_KEK_AESKEY_LEN]; +extern const uint8_t snvm_enckey_prov_nonce[ENCRYPT_NONCE_SIZE]; +#define prov_aes_key snvm_enckey_prov_key +#define prov_aes_nonce snvm_enckey_prov_nonce +#elif !defined(SNVM_ENCKEY_INSECURE_TEST_KEY) +#error "SNVM_ENCKEY_PROVISION writes a key into sNVM permanently. Define \ +SNVM_ENCKEY_PROVISION_EXTERN and provide snvm_enckey_prov_key/nonce, or define \ +SNVM_ENCKEY_INSECURE_TEST_KEY to provision the built-in public test vector." +#else +/* Publicly known test vector. A device provisioned with this has no image + * confidentiality, and sNVM cannot be rewritten to undo it. */ +static const uint8_t prov_aes_key[SNVM_KEK_AESKEY_LEN] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f +}; +static const uint8_t prov_aes_nonce[ENCRYPT_NONCE_SIZE] = { + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f +}; +#endif + +int snvm_enckey_provision(void) +{ + uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + int ret, i; + + for (i = 0; i < (int)sizeof(page); i++) { + page[i] = 0; + } + ret = snvm_kek_wrap(prov_aes_key, (uint32_t)sizeof(prov_aes_key), page, + SNVM_KEK_WRAPPED_LEN); + if (ret != SNVM_KEK_WRAPPED_LEN) { + wolfBoot_printf("enckey provision: wrap failed (%d)\n", ret); + wc_ForceZero(page, sizeof(page)); + return -1; + } + memcpy(page + SNVM_KEK_WRAPPED_LEN, prov_aes_nonce, ENCRYPT_NONCE_SIZE); + ret = snvm_page_write_if_changed(SNVM_ENCKEY_MODULE, page); + wolfBoot_printf("enckey provision: sNVM write[%d] ret=%d\n", + SNVM_ENCKEY_MODULE, ret); + wc_ForceZero(page, sizeof(page)); + return ret; +} +#endif /* SNVM_ENCKEY_PROVISION */ + +#endif /* CUSTOM_ENCRYPT_KEY && SNVM_KEK */ diff --git a/hal/mpfs250_snvm.h b/hal/mpfs250_snvm.h new file mode 100644 index 0000000000..3e5bb2892e --- /dev/null +++ b/hal/mpfs250_snvm.h @@ -0,0 +1,145 @@ +/* mpfs250_snvm.h + * + * sNVM keystore layout and PUF KEK interface for hal/mpfs250_snvm.c. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef MPFS250_SNVM_H +#define MPFS250_SNVM_H + +#if (defined(SNVM_KEYSTORE) || defined(SNVM_KEYSTORE_PROVISION)) && \ + !defined(WOLFBOOT_NO_SIGN) + +#include "keystore.h" +#include "hal/mpfs250.h" + +#define SNVM_KEYSTORE_HDR_SIZE 16 + +#if defined(__GNUC__) + #define SNVM_KEYSTORE_PACKED __attribute__((packed)) +#else + #define SNVM_KEYSTORE_PACKED +#endif + +/* On-sNVM image: this header at logical offset 0, then item_count packed + * keystore_slot entries. Same layout family as the OTP keystore. */ +struct SNVM_KEYSTORE_PACKED snvm_keystore_hdr { + char magic[8]; + uint16_t item_count; + uint16_t flags; + uint32_t version; +}; + +static const char SNVM_KEYSTORE_MAGIC[8] = + {'W', 'O', 'L', 'F', 'B', 'O', 'O', 'T'}; + +/* First sNVM module holding the keystore image (override per board/config). + * Defaults high to leave low modules for a future bootmode-2 sNVM boot image. */ +#ifndef SNVM_KEYSTORE_MODULE +#define SNVM_KEYSTORE_MODULE 200 +#endif + +/* Modules the image may span (provisioning buffer bound). One page holds two + * ECC384 keys; an ML-DSA-87 key needs 11. Every page in the range must be + * runtime-writable (not marked ROM) in the Libero design for the on-device + * provisioning writer; a production bitstream can instead ship them as ROM + * pages, which SNVM_KEYSTORE_REQUIRE_ROM then insists on at boot. */ +#ifndef SNVM_KEYSTORE_MAX_MODULES +#define SNVM_KEYSTORE_MAX_MODULES 1 +#endif +#if (SNVM_KEYSTORE_MODULE < 0) || (SNVM_KEYSTORE_MAX_MODULES < 1) || \ + ((SNVM_KEYSTORE_MODULE + SNVM_KEYSTORE_MAX_MODULES) > MPFS_SNVM_MODULE_MAX) +#error "sNVM keystore module range must lie within modules 0..220" +#endif + +/* Plaintext page payload. Public keys are not secret; their integrity rests + * on whatever protects the wolfBoot image itself. Authenticated sNVM is a + * future option. */ +#define SNVM_KEYSTORE_PAGE_DATA MPFS_SNVM_PLAIN_DATA_LEN + +/* Upper bound on a trusted item_count, from the usable keystore capacity. + * SIZEOF_KEYSTORE_SLOT comes from keystore.h, which the user includes first. */ +#define SNVM_KEYSTORE_MAX_PUBKEYS \ + ((((SNVM_KEYSTORE_MAX_MODULES) * (SNVM_KEYSTORE_PAGE_DATA)) - \ + (SNVM_KEYSTORE_HDR_SIZE)) / (SIZEOF_KEYSTORE_SLOT)) + +#ifdef SNVM_KEYSTORE_PROVISION +/* Write the compiled-in keystore (keystore.c) into sNVM. Run once. */ +int snvm_keystore_provision(void); +#endif + +#endif /* SNVM_KEYSTORE || SNVM_KEYSTORE_PROVISION */ + +#ifdef SNVM_KEK + +#include +#include "hal/mpfs250.h" + +/* Device-unique KEK: the first 256 bits of a SHA-384 digest. */ +#define SNVM_KEK_LEN 32 + +/* AES-256 key wrapped with the PUF KEK: RFC 3394 adds 8 bytes of overhead. */ +#define SNVM_KEK_AESKEY_LEN 32 +#define SNVM_KEK_WRAPPED_LEN (SNVM_KEK_AESKEY_LEN + 8) + +/* sNVM module holding the wrapped image-encryption key (override per board). + * Must not fall inside the keystore's module range, and must be runtime-writable + * in the Libero design: on the Video Kit only pages 200 and 201 are. */ +#ifndef SNVM_ENCKEY_MODULE +#define SNVM_ENCKEY_MODULE 201 +#endif +#if (SNVM_ENCKEY_MODULE < 0) || (SNVM_ENCKEY_MODULE >= MPFS_SNVM_MODULE_MAX) +#error "SNVM_ENCKEY_MODULE is outside the sNVM module range 0..220" +#endif +#if defined(SNVM_KEYSTORE) || defined(SNVM_KEYSTORE_PROVISION) +#if (SNVM_ENCKEY_MODULE >= SNVM_KEYSTORE_MODULE) && \ + (SNVM_ENCKEY_MODULE < (SNVM_KEYSTORE_MODULE + SNVM_KEYSTORE_MAX_MODULES)) +#error "SNVM_ENCKEY_MODULE overlaps the sNVM keystore module range" +#endif +#endif + +/* Derive the 256-bit device-unique KEK from the System Controller SRAM-PUF: + * KEK = SHA384(label || PUF(fixed-challenge))[0..31]. Returns 0 on success. */ +int mpfs_puf_kek(uint8_t *kek); + +/* AES key-wrap (RFC 3394) using the PUF KEK. out needs keysz+8 bytes. + * Returns the wrapped length (keysz+8) on success, < 0 on error. */ +int snvm_kek_wrap(const uint8_t *key, uint32_t keysz, uint8_t *out, + uint32_t outsz); + +/* AES key-unwrap using the PUF KEK. key needs insz-8 bytes. + * Returns the unwrapped length (insz-8) on success, < 0 on error. */ +int snvm_kek_unwrap(const uint8_t *in, uint32_t insz, uint8_t *key, + uint32_t keysz); + + +#ifdef SNVM_ENCKEY_PROVISION +/* Provided by hal/mpfs250_snvm.c: wrap the compiled-in AES key with + * the PUF KEK and store [wrapped key][nonce] in sNVM. */ +int snvm_enckey_provision(void); +#endif + +#endif /* SNVM_KEK */ + +#if defined(SNVM_ENCKEY_PROVISION) && !defined(SNVM_KEK) +#error "SNVM_ENCKEY_PROVISION needs SNVM_KEK (the key is wrapped with the PUF KEK)" +#endif + +#endif /* MPFS250_SNVM_H */ diff --git a/options.mk b/options.mk index 4ee8be824d..97573f93e6 100644 --- a/options.mk +++ b/options.mk @@ -77,6 +77,98 @@ ifeq ($(FLASH_OTP_KEYSTORE),1) CFLAGS+=-D"FLASH_OTP_KEYSTORE" endif +# PolarFire SoC: trust anchor in secure NVM (sNVM). SNVM_KEYSTORE serves keys +# from sNVM at runtime; SNVM_KEYSTORE_PROVISION adds the one-time writer. +ifeq ($(SNVM_KEYSTORE),1) + CFLAGS+=-D"SNVM_KEYSTORE" +endif +# sNVM page numbers, overridable per board; the headers range-check them. +ifneq ($(SNVM_KEYSTORE_MODULE),) + CFLAGS+=-DSNVM_KEYSTORE_MODULE=$(SNVM_KEYSTORE_MODULE) +endif +ifneq ($(SNVM_KEYSTORE_MAX_MODULES),) + CFLAGS+=-DSNVM_KEYSTORE_MAX_MODULES=$(SNVM_KEYSTORE_MAX_MODULES) +endif +ifneq ($(SNVM_ENCKEY_MODULE),) + CFLAGS+=-DSNVM_ENCKEY_MODULE=$(SNVM_ENCKEY_MODULE) +endif +ifeq ($(SNVM_KEYSTORE_PROVISION),1) + ifeq ($(SNVM_KEYSTORE),1) + $(error SNVM_KEYSTORE_PROVISION writes the compiled-in keystore to sNVM \ + and cannot be combined with SNVM_KEYSTORE=1, which serves keys \ + from sNVM instead of compiling them in.) + endif + ifneq ($(WOLFBOOT_SNVM_WRITE_APPROVED),1) + $(error SNVM_KEYSTORE_PROVISION writes sNVM: irreversible, and a \ + brownout mid-write can lock the page read-only. Re-run with \ + WOLFBOOT_SNVM_WRITE_APPROVED=1 to confirm.) + endif + CFLAGS+=-D"SNVM_KEYSTORE_PROVISION" +endif + +# PolarFire SoC: production hardening of the sNVM trust anchor. +# SNVM_KEYSTORE_REQUIRE_ROM refuses to boot unless every keystore page is +# ROM-flagged, which only a bitstream can set. MPFS_SCB_SMODE_DENY fences +# S-mode off the System Controller mailbox (PMP) and disables its OS nodes, +# so nothing after wolfBoot can reach the sNVM write service. +ifeq ($(SNVM_KEYSTORE_REQUIRE_ROM),1) + ifneq ($(SNVM_KEYSTORE),1) + $(error SNVM_KEYSTORE_REQUIRE_ROM=1 needs SNVM_KEYSTORE=1) + endif + CFLAGS+=-D"SNVM_KEYSTORE_REQUIRE_ROM" +endif +ifeq ($(MPFS_SCB_SMODE_DENY),1) + # The fence is installed at the M->S hand-off; an S-mode build under HSS + # has no PMP to program and must not claim the protection. + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifneq ($(RISCV_MMODE),1) + $(error MPFS_SCB_SMODE_DENY=1 needs the M-mode target (RISCV_MMODE=1)) + endif + endif + CFLAGS+=-D"MPFS_SCB_SMODE_DENY" -DWOLFBOOT_PMP_DENY_BASE=0x37020000UL \ + -DWOLFBOOT_PMP_DENY_SIZE=0x1000UL +endif + +# PolarFire SoC: PUF-derived KEK + RFC 3394 AES key-wrap for sNVM-stored keys. +ifeq ($(SNVM_KEK),1) + # The KEK only ever wraps the image-encryption key, so it rides on the + # AES-256 ENCRYPT build's AES; RFC 3394 key-wrap is added here. + ifneq ($(ENCRYPT)$(ENCRYPT_WITH_AES256)$(CUSTOM_ENCRYPT_KEY),111) + $(error SNVM_KEK=1 requires ENCRYPT=1 ENCRYPT_WITH_AES256=1 \ + CUSTOM_ENCRYPT_KEY=1) + endif + CFLAGS+=-D"SNVM_KEK" + CFLAGS+=-DHAVE_AES_KEYWRAP -DWOLFSSL_AES_DIRECT -DHAVE_AES_ECB \ + -DHAVE_AES_DECRYPT + # The KEK derivation is SHA-384 whatever the image hash is. + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif +endif +# One-time writer for the PUF-wrapped image-encryption key into sNVM. +ifeq ($(SNVM_ENCKEY_PROVISION),1) + ifneq ($(SNVM_KEK)$(ENCRYPT)$(CUSTOM_ENCRYPT_KEY),111) + $(error SNVM_ENCKEY_PROVISION requires SNVM_KEK=1 ENCRYPT=1 \ + CUSTOM_ENCRYPT_KEY=1, which build the provider it calls.) + endif + ifneq ($(WOLFBOOT_SNVM_WRITE_APPROVED),1) + $(error SNVM_ENCKEY_PROVISION writes sNVM: irreversible, and a \ + brownout mid-write can lock the page read-only. Re-run with \ + WOLFBOOT_SNVM_WRITE_APPROVED=1 to confirm.) + endif + CFLAGS+=-D"SNVM_ENCKEY_PROVISION" + # Second acknowledgement: the helper's built-in key is a public test vector + # and sNVM is written once, so such a device has no image confidentiality. + ifeq ($(SNVM_ENCKEY_INSECURE_TEST_KEY),1) + CFLAGS+=-D"SNVM_ENCKEY_INSECURE_TEST_KEY" + endif + # Production path: the integrator's object defines snvm_enckey_prov_key and + # snvm_enckey_prov_nonce (see hal/mpfs250_snvm.c). + ifeq ($(SNVM_ENCKEY_PROVISION_EXTERN),1) + CFLAGS+=-D"SNVM_ENCKEY_PROVISION_EXTERN" + endif +endif + ifeq ($(WOLFBOOT_TEST_FILLER),1) CFLAGS+=-D"WOLFBOOT_TEST_FILLER" endif @@ -1607,11 +1699,8 @@ ifneq (,$(filter RISCV RISCV64,$(ARCH))) CFLAGS+=-DSTACK_SIZE_PER_HART=$(STACK_SIZE_PER_HART) endif -# L2 scratchpad region size and boot-hart stack size, substituted into the -# linker script. The scratchpad is capped by the number of scratchpad ways -# the startup asm populates (4 ways x 128 KB on MPFS250); the linker script -# asserts the limit. Defaults keep the pre-existing layout for targets that -# do not override them. +# Substituted into the linker script. The scratchpad is capped at the 4 x 128 KB +# of ways the startup asm populates; the linker script asserts it. WOLFBOOT_L2SCRATCH_SIZE ?= 256k STACK_SIZE ?= 32k diff --git a/src/boot_riscv.c b/src/boot_riscv.c index 4a03fbe5f1..234d0daf6c 100644 --- a/src/boot_riscv.c +++ b/src/boot_riscv.c @@ -391,11 +391,27 @@ int WEAKFUNCTION hal_dts_fixup(void* dts_addr, uint32_t capacity) #endif #ifdef WOLFBOOT_RISCV_MMODE -/* Configure PMP entry 0: NAPOT full address space, RWX, for S-mode access */ +/* Configure PMP for S-mode access: NAPOT full address space, RWX. With + * WOLFBOOT_PMP_DENY_BASE/SIZE the window is carved out first (entries match + * lowest-numbered first), so S-mode cannot touch it; M-mode is unaffected. */ static void setup_pmp_for_smode(void) { +#if defined(WOLFBOOT_PMP_DENY_BASE) && defined(WOLFBOOT_PMP_DENY_SIZE) + unsigned long lo = (unsigned long)WOLFBOOT_PMP_DENY_BASE >> 2; + unsigned long hi = ((unsigned long)WOLFBOOT_PMP_DENY_BASE + + (unsigned long)WOLFBOOT_PMP_DENY_SIZE) >> 2; + /* entry 0: TOR below the window, RWX; entry 1: TOR the window, no + * access; entry 2: NAPOT everything else, RWX */ + unsigned long cfg = (0x1FUL << 16) | (0x08UL << 8) | 0x0FUL; + + csr_write(pmpaddr0, lo); + csr_write(pmpaddr1, hi); + csr_write(pmpaddr2, -1UL); + csr_write(pmpcfg0, cfg); +#else csr_write(pmpaddr0, -1UL); /* all-ones = cover entire address space (NAPOT) */ csr_write(pmpcfg0, 0x1F); /* A=NAPOT(3), R=1, W=1, X=1 */ +#endif __asm__ volatile("sfence.vma" ::: "memory"); } diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 0093aa1f98..650cbb7032 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -67,28 +67,12 @@ _reset: sw zero, 0x174(t1) /* SYSREG_L2_SHUTDOWN_CR = 0 */ fence - /* Populate the scratchpad ways one way at a time, so every line of - * the Zero Device (0x0A000000) is resident before anything else runs. - * - * With the default mask (0xFF = cache ways 0-7), a store to the Zero - * Device allocates in an ordinary cache way; that line is evictable and - * its writeback is discarded, so the store is silently lost. Selecting - * all four scratchpad ways at once (0xF00) is not enough either: it only - * covers the stores made while the mask is set, which used to be the - * .text copy alone. Everything after the mask was restored -- .data, - * .bss (including the SDHCI PDMA staging buffer), the heap and the E51 - * stack -- still allocated in evictable ways. - * - * Walking one way at a time is what makes the whole 512 KB resident: a - * store can only allocate in the single enabled way, so 2048 blocks x 4 - * ways fills each way exactly once. Once resident, later stores hit and - * the mask no longer matters. This is HSS config_l2_cache(), see - * mss_l2_cache.c; the marker value matches its INIT_MARKER. - * - * Constants for WAY_ENABLE=0x0B, NUM_SCRATCH_PAD_WAYS=4: - * first way = 1 << ((WAY_ENABLE + 1) - NUM_SCRATCH_PAD_WAYS) = 1 << 8 - * blocks per way = WAY_BYTE_LENGTH / CACHE_BLOCK_BYTE_LENGTH - * = 0x20000 / 64 = 2048 */ + /* Make all 512 KB of the Zero Device resident by enabling one scratchpad + * way at a time and storing a marker every 64 bytes: a store can only + * allocate in the enabled way, so 2048 blocks x 4 ways fills each once. + * A mask of 0xF00 covers only the stores made while it is set, leaving + * .data/.bss/heap/stack in evictable ways whose writeback is discarded. + * This is HSS config_l2_cache(); the marker matches its INIT_MARKER. */ li t0, 0x02010828 /* L2_WAY_MASK_E51_DCACHE */ li t1, 0x100 /* scratchpad way 8 */ li t2, 0x0A000000 /* ZERO_DEVICE_BOTTOM */ diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 51e18ff245..b3d6b862c2 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -1944,8 +1944,8 @@ int wolfBoot_fallback_is_possible(void) #include "enckey_data.h" #endif -#if !defined(EXT_FLASH) && !defined(MMU) - #error option EXT_ENCRYPTED requires EXT_FLASH or MMU mode +#if !defined(EXT_FLASH) && !defined(MMU) && !defined(CUSTOM_ENCRYPT_KEY) + #error option EXT_ENCRYPTED requires EXT_FLASH, MMU, or CUSTOM_ENCRYPT_KEY #endif #ifndef WOLFBOOT_ENCRYPT_CACHE @@ -1960,7 +1960,7 @@ int wolfBoot_fallback_is_possible(void) #define ENCRYPT_CACHE (WOLFBOOT_ENCRYPT_CACHE) #endif -#if defined(EXT_ENCRYPTED) && defined(MMU) +#if defined(EXT_ENCRYPTED) && defined(MMU) && !defined(CUSTOM_ENCRYPT_KEY) static uint8_t ENCRYPT_KEY[ENCRYPT_KEY_SIZE + ENCRYPT_NONCE_SIZE]; #endif @@ -2006,6 +2006,9 @@ void RAMFUNCTION wolfBoot_crypto_set_iv(const uint8_t *nonce, uint32_t iv_counte } #endif /* EXT_ENCRYPTED && (__WOLFBOOT || UNIT_TEST || MMU) */ +#ifndef CUSTOM_ENCRYPT_KEY +/* Under CUSTOM_ENCRYPT_KEY the platform supplies the key, so the + * partition-resident storage below (needs WOLFBOOT_PARTITION_*) is dropped. */ static int RAMFUNCTION hal_set_key(const uint8_t *k, const uint8_t *nonce) { #ifdef WOLFBOOT_RENESAS_TSIP @@ -2095,7 +2098,6 @@ static int RAMFUNCTION hal_set_key(const uint8_t *k, const uint8_t *nonce) return ret; #endif } -#ifndef CUSTOM_ENCRYPT_KEY /** * @brief Set the encryption key. * diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 5ccc3509b1..2613e0116f 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -103,6 +103,8 @@ TESTS+=unit-flash-erase-u3 TESTS+=unit-flash-erase-kinetis TESTS+=unit-flash-m2354 TESTS+=unit-otp-keystore +TESTS+=unit-snvm-keystore +TESTS+=unit-snvm-kek TESTS+=unit-otp-keystore-gen-zeroize TESTS+=unit-x86-paging-oob TESTS+=unit-ahci-unlock-panic @@ -787,6 +789,28 @@ unit-flash-m2354: unit-flash-m2354.c ../../hal/m2354.c ../../hal/m2354.h unit-otp-keystore: unit-otp-keystore.c ../../src/flash_otp_keystore.c gcc -o $@ unit-otp-keystore.c $(CFLAGS) $(LDFLAGS) +# unit-snvm-keystore includes hal/mpfs250_snvm.c directly and mocks +# mpfs_snvm_read, so it is not a separate input. -I../.. resolves +# hal/mpfs250.h, which mpfs250_snvm.h includes. Four modules so the +# page-spanning reads are exercised. +unit-snvm-keystore: unit-snvm-keystore.c ../../hal/mpfs250_snvm.c + gcc -o $@ unit-snvm-keystore.c -I../.. -DSNVM_KEYSTORE_MAX_MODULES=4 \ + $(CFLAGS) $(LDFLAGS) + +# unit-snvm-kek includes hal/mpfs250_snvm.c (KEK and encryption-key provider) +# directly with the PUF and sNVM services mocked. SHA-384 and AES key-wrap +# come from wolfCrypt, built from source like the image tests. +unit-snvm-kek: unit-snvm-kek.c ../../hal/mpfs250_snvm.c + gcc -o $@ unit-snvm-kek.c -I../.. \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/aes.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha512.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/memory.c \ + -DSNVM_KEK -DWOLFBOOT_ENABLE_HASH_SHA384 -DWOLFSSL_AES_256 \ + -DHAVE_AES_KEYWRAP -DWOLFSSL_AES_DIRECT -DHAVE_AES_ECB \ + -DHAVE_AES_DECRYPT -DCUSTOM_ENCRYPT_KEY -DEXT_ENCRYPTED \ + -DENCRYPT_WITH_AES256 -DSNVM_ENCKEY_PROVISION \ + -DSNVM_ENCKEY_INSECURE_TEST_KEY $(CFLAGS) $(LDFLAGS) + # unit-otp-keystore-gen-zeroize includes the host tool # tools/keytools/otp/otp-keystore-gen.c directly (via #define main) and # interposes read()/malloc()/free() (dlsym RTLD_NEXT, hence -ldl) to observe diff --git a/tools/unit-tests/unit-snvm-kek.c b/tools/unit-tests/unit-snvm-kek.c new file mode 100644 index 0000000000..17cb8f1880 --- /dev/null +++ b/tools/unit-tests/unit-snvm-kek.c @@ -0,0 +1,286 @@ +/* unit-snvm-kek.c + * + * Host unit test for the PolarFire SoC PUF-derived KEK, the RFC 3394 key + * wrap around it, and the sNVM-backed image-encryption-key provider. The + * System Controller services are mocked: the PUF response is a deterministic + * function of the challenge and a per-"device" seed, and sNVM is an array. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#include "hal/mpfs250.h" + +static uint8_t mock_puf_seed; +static int mock_puf_fail; +static int mock_puf_calls; +static uint8_t mock_snvm[MPFS_SNVM_MODULE_MAX][MPFS_SNVM_PLAIN_DATA_LEN]; +static int mock_snvm_read_fail; +static int mock_snvm_writes; + +int mpfs_puf_emulation(const uint8_t *challenge, uint8_t op_type, + uint8_t *response) +{ + int i; + + (void)op_type; + mock_puf_calls++; + if (mock_puf_fail) + return -1; + for (i = 0; i < MPFS_PUF_RESPONSE_LEN; i++) { + response[i] = (uint8_t)(challenge[i % MPFS_PUF_CHALLENGE_LEN] ^ + mock_puf_seed ^ (uint8_t)(i * 7)); + } + return 0; +} + +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t data_len) +{ + (void)usk; (void)admin; + if (mock_snvm_read_fail || module >= MPFS_SNVM_MODULE_MAX || + data_len > MPFS_SNVM_PLAIN_DATA_LEN) + return -1; + memcpy(data, mock_snvm[module], data_len); + return 0; +} + +int mpfs_snvm_write(uint8_t format, uint8_t module, const uint8_t *data, + const uint8_t *usk) +{ + (void)format; (void)usk; + mock_snvm_writes++; + if (module >= MPFS_SNVM_MODULE_MAX) + return -1; + memcpy(mock_snvm[module], data, MPFS_SNVM_PLAIN_DATA_LEN); + return 0; +} + +#include "../../hal/mpfs250_snvm.c" + +static void reset_mocks(void) +{ + mock_puf_seed = 0x5A; + mock_puf_fail = 0; + mock_puf_calls = 0; + mock_snvm_read_fail = 0; + mock_snvm_writes = 0; + memset(mock_snvm, 0, sizeof(mock_snvm)); +} + +static void fill(uint8_t *buf, uint32_t len, uint8_t base) +{ + uint32_t i; + for (i = 0; i < len; i++) + buf[i] = (uint8_t)(base + i); +} + +START_TEST(test_kek_deterministic_per_device) +{ + uint8_t k1[SNVM_KEK_LEN], k2[SNVM_KEK_LEN], k3[SNVM_KEK_LEN]; + + reset_mocks(); + ck_assert_int_eq(mpfs_puf_kek(k1), 0); + ck_assert_int_eq(mpfs_puf_kek(k2), 0); + ck_assert_int_eq(memcmp(k1, k2, SNVM_KEK_LEN), 0); + ck_assert_int_eq(mock_puf_calls, 2); + + /* Another device (different PUF) must derive a different KEK. */ + mock_puf_seed = 0xA5; + ck_assert_int_eq(mpfs_puf_kek(k3), 0); + ck_assert_int_ne(memcmp(k1, k3, SNVM_KEK_LEN), 0); +} +END_TEST + +START_TEST(test_kek_guards) +{ + uint8_t kek[SNVM_KEK_LEN]; + uint8_t key[SNVM_KEK_AESKEY_LEN], out[SNVM_KEK_WRAPPED_LEN]; + + reset_mocks(); + ck_assert_int_eq(mpfs_puf_kek(NULL), -1); + ck_assert_int_eq(snvm_kek_wrap(NULL, sizeof(key), out, sizeof(out)), -1); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), NULL, sizeof(out)), -1); + ck_assert_int_eq(snvm_kek_unwrap(NULL, sizeof(out), key, sizeof(key)), -1); + ck_assert_int_eq(snvm_kek_unwrap(out, sizeof(out), NULL, sizeof(key)), -1); + + mock_puf_fail = 1; + ck_assert_int_eq(mpfs_puf_kek(kek), -1); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), out, sizeof(out)), -1); + ck_assert_int_eq(snvm_kek_unwrap(out, sizeof(out), key, sizeof(key)), -1); +} +END_TEST + +START_TEST(test_wrap_unwrap_roundtrip) +{ + uint8_t key[SNVM_KEK_AESKEY_LEN], back[SNVM_KEK_AESKEY_LEN]; + uint8_t wrapped[SNVM_KEK_WRAPPED_LEN], wrapped2[SNVM_KEK_WRAPPED_LEN]; + + reset_mocks(); + fill(key, sizeof(key), 0xA0); + memset(back, 0, sizeof(back)); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), wrapped, sizeof(wrapped)), + SNVM_KEK_WRAPPED_LEN); + ck_assert_int_ne(memcmp(wrapped, key, sizeof(key)), 0); + /* Deterministic: same device, same key, same blob. */ + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), wrapped2, + sizeof(wrapped2)), SNVM_KEK_WRAPPED_LEN); + ck_assert_int_eq(memcmp(wrapped, wrapped2, sizeof(wrapped)), 0); + + ck_assert_int_eq(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, + sizeof(back)), SNVM_KEK_AESKEY_LEN); + ck_assert_int_eq(memcmp(key, back, sizeof(key)), 0); +} +END_TEST + +START_TEST(test_unwrap_rejects_tamper_and_foreign_device) +{ + uint8_t key[SNVM_KEK_AESKEY_LEN], back[SNVM_KEK_AESKEY_LEN]; + uint8_t wrapped[SNVM_KEK_WRAPPED_LEN]; + + reset_mocks(); + fill(key, sizeof(key), 0x10); + ck_assert_int_eq(snvm_kek_wrap(key, sizeof(key), wrapped, sizeof(wrapped)), + SNVM_KEK_WRAPPED_LEN); + + /* A blob wrapped on one device does not unwrap on another. */ + mock_puf_seed = 0x3C; + ck_assert_int_ne(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, + sizeof(back)), SNVM_KEK_AESKEY_LEN); + mock_puf_seed = 0x5A; + + /* The RFC 3394 integrity check catches a flipped bit. */ + wrapped[SNVM_KEK_WRAPPED_LEN - 1] ^= 0x01; + ck_assert_int_ne(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, + sizeof(back)), SNVM_KEK_AESKEY_LEN); + + /* Too short an output buffer is refused rather than truncated. */ + wrapped[SNVM_KEK_WRAPPED_LEN - 1] ^= 0x01; + ck_assert_int_ne(snvm_kek_unwrap(wrapped, sizeof(wrapped), back, 16), + SNVM_KEK_AESKEY_LEN); +} +END_TEST + +START_TEST(test_provision_then_get_encrypt_key) +{ + uint8_t key[ENCRYPT_KEY_SIZE], nonce[ENCRYPT_NONCE_SIZE]; + uint8_t expect_key[ENCRYPT_KEY_SIZE], expect_nonce[ENCRYPT_NONCE_SIZE]; + + reset_mocks(); + fill(expect_key, sizeof(expect_key), 0x00); + fill(expect_nonce, sizeof(expect_nonce), 0x20); + + ck_assert_int_eq(snvm_enckey_provision(), 0); + ck_assert_int_eq(mock_snvm_writes, 1); + /* A second run finds the page already holding the content and does not + * spend another write cycle. */ + ck_assert_int_eq(snvm_enckey_provision(), 0); + ck_assert_int_eq(mock_snvm_writes, 1); + /* The page holds the wrapped key, never the key itself. */ + ck_assert_int_ne(memcmp(mock_snvm[SNVM_ENCKEY_MODULE], expect_key, + sizeof(expect_key)), 0); + ck_assert_int_eq(memcmp(mock_snvm[SNVM_ENCKEY_MODULE] + + SNVM_KEK_WRAPPED_LEN, expect_nonce, sizeof(expect_nonce)), 0); + + memset(key, 0, sizeof(key)); + memset(nonce, 0, sizeof(nonce)); + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), 0); + ck_assert_int_eq(memcmp(key, expect_key, sizeof(key)), 0); + ck_assert_int_eq(memcmp(nonce, expect_nonce, sizeof(nonce)), 0); +} +END_TEST + +START_TEST(test_get_encrypt_key_failures) +{ + uint8_t key[ENCRYPT_KEY_SIZE], nonce[ENCRYPT_NONCE_SIZE]; + + reset_mocks(); + ck_assert_int_eq(snvm_enckey_provision(), 0); + + ck_assert_int_eq(wolfBoot_get_encrypt_key(NULL, nonce), -1); + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, NULL), -1); + + mock_snvm_read_fail = 1; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); + mock_snvm_read_fail = 0; + + /* Blob from another device: unwrap fails, no key is returned. */ + mock_puf_seed = 0xC3; + memset(key, 0, sizeof(key)); + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); + mock_puf_seed = 0x5A; + + /* Corrupted page: integrity check fails. */ + mock_snvm[SNVM_ENCKEY_MODULE][3] ^= 0x80; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); + mock_snvm[SNVM_ENCKEY_MODULE][3] ^= 0x80; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), 0); + + /* PUF service down: fail closed. */ + mock_puf_fail = 1; + ck_assert_int_eq(wolfBoot_get_encrypt_key(key, nonce), -1); +} +END_TEST + +START_TEST(test_set_erase_fail_closed) +{ + uint8_t key[ENCRYPT_KEY_SIZE], nonce[ENCRYPT_NONCE_SIZE]; + + reset_mocks(); + memset(key, 0x11, sizeof(key)); + memset(nonce, 0x22, sizeof(nonce)); + ck_assert_int_eq(wolfBoot_set_encrypt_key(key, nonce), -1); + ck_assert_int_eq(wolfBoot_erase_encrypt_key(), -1); + ck_assert_int_eq(mock_snvm_writes, 0); +} +END_TEST + +Suite *snvm_kek_suite(void) +{ + Suite *s = suite_create("snvm-kek"); + TCase *tc = tcase_create("snvm-kek"); + + tcase_add_test(tc, test_kek_deterministic_per_device); + tcase_add_test(tc, test_kek_guards); + tcase_add_test(tc, test_wrap_unwrap_roundtrip); + tcase_add_test(tc, test_unwrap_rejects_tamper_and_foreign_device); + tcase_add_test(tc, test_provision_then_get_encrypt_key); + tcase_add_test(tc, test_get_encrypt_key_failures); + tcase_add_test(tc, test_set_erase_fail_closed); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = snvm_kek_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} diff --git a/tools/unit-tests/unit-snvm-keystore.c b/tools/unit-tests/unit-snvm-keystore.c new file mode 100644 index 0000000000..f915e2f853 --- /dev/null +++ b/tools/unit-tests/unit-snvm-keystore.c @@ -0,0 +1,328 @@ +/* unit-snvm-keystore.c + * + * Host unit tests for the PolarFire SoC sNVM keystore backend. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#define SNVM_KEYSTORE +#define KEYSTORE_PUBKEY_SIZE 64 /* model an ECC-256 keystore slot */ + +#include "keystore.h" +#include "hal/mpfs250.h" +#include "hal/mpfs250_snvm.h" + +/* Mock sNVM: one page per module, plus a failure injector. */ +static uint8_t mock_snvm[MPFS_SNVM_MODULE_MAX][MPFS_SNVM_PLAIN_DATA_LEN]; +static int mock_snvm_fail_module = -1; /* module whose read returns -1 */ +static int mock_snvm_reads; + +int mpfs_snvm_read(uint8_t module, const uint8_t *usk, uint8_t *admin, + uint8_t *data, uint16_t len) +{ + (void)usk; (void)admin; + mock_snvm_reads++; + if (module >= MPFS_SNVM_MODULE_MAX) + return -1; + if ((int)module == mock_snvm_fail_module) + return -1; + if (len > MPFS_SNVM_PLAIN_DATA_LEN) + return -1; + memcpy(data, mock_snvm[module], len); + return 0; +} + +#include "../../hal/mpfs250_snvm.c" + +#define PAGE SNVM_KEYSTORE_PAGE_DATA + +/* Fill the mock modules with a byte pattern that encodes its logical offset, + * so a mis-computed module/offset shows up as wrong data rather than luck. */ +static void setup_pattern(void) +{ + uint32_t m, i; + + memset(mock_snvm, 0, sizeof(mock_snvm)); + mock_snvm_fail_module = -1; + mock_snvm_reads = 0; + for (m = 0; m < SNVM_KEYSTORE_MAX_MODULES; m++) { + for (i = 0; i < PAGE; i++) { + mock_snvm[SNVM_KEYSTORE_MODULE + m][i] = + (uint8_t)((m * PAGE + i) & 0xFF); + } + } +} + +static void expect_pattern(const uint8_t *buf, uint32_t off, uint32_t len) +{ + uint32_t i; + for (i = 0; i < len; i++) + ck_assert_uint_eq(buf[i], (uint8_t)((off + i) & 0xFF)); +} + +/* A read wholly inside the first page touches one module. */ +START_TEST(test_read_within_one_page) +{ + uint8_t buf[16]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(0, buf, sizeof(buf)), 0); + expect_pattern(buf, 0, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 1); +} +END_TEST + +/* A read straddling the page boundary takes the tail of one module and the head + * of the next -- the offset arithmetic the OTP sibling never has to do. */ +START_TEST(test_read_spans_two_pages) +{ + uint8_t buf[8]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(PAGE - 2, buf, sizeof(buf)), 0); + expect_pattern(buf, PAGE - 2, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 2); +} +END_TEST + +/* Spanning three modules exercises the loop rather than a single carry. */ +START_TEST(test_read_spans_three_pages) +{ + uint8_t buf[PAGE + 10]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(PAGE - 5, buf, sizeof(buf)), 0); + expect_pattern(buf, PAGE - 5, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 3); +} +END_TEST + +/* Starting exactly on a boundary must land at offset 0 of the next module, + * not offset PAGE of the previous one. */ +START_TEST(test_read_at_exact_boundary) +{ + uint8_t buf[4]; + setup_pattern(); + ck_assert_int_eq(snvm_keystore_read(PAGE, buf, sizeof(buf)), 0); + expect_pattern(buf, PAGE, sizeof(buf)); + ck_assert_int_eq(mock_snvm_reads, 1); +} +END_TEST + +/* Running off the end of the sNVM module range is refused, not wrapped. */ +START_TEST(test_read_past_module_max) +{ + uint8_t buf[8]; + uint32_t off; + setup_pattern(); + off = (uint32_t)(MPFS_SNVM_MODULE_MAX - SNVM_KEYSTORE_MODULE) * PAGE; + ck_assert_int_eq(snvm_keystore_read(off, buf, sizeof(buf)), -1); +} +END_TEST + +/* An sNVM read error propagates instead of leaving stale cache contents. */ +START_TEST(test_read_error_propagates) +{ + uint8_t buf[8]; + setup_pattern(); + mock_snvm_fail_module = SNVM_KEYSTORE_MODULE; + ck_assert_int_eq(snvm_keystore_read(0, buf, sizeof(buf)), -1); +} +END_TEST + +/* Provision slot `id` in the mock image, after the header. */ +static void setup_slot(int id, uint32_t pubkey_size, uint32_t key_type, + uint32_t mask) +{ + struct keystore_slot slot; + uint32_t off = SNVM_KEYSTORE_HDR_SIZE + + ((uint32_t)id * SIZEOF_KEYSTORE_SLOT); + uint32_t m = off / PAGE, moff = off % PAGE; + uint8_t *p = (uint8_t *)&slot; + uint32_t i; + + memset(&slot, 0, sizeof(slot)); + slot.slot_id = id; + slot.key_type = key_type; + slot.part_id_mask = mask; + slot.pubkey_size = pubkey_size; + for (i = 0; i < SIZEOF_KEYSTORE_SLOT; i++) { + mock_snvm[SNVM_KEYSTORE_MODULE + m + ((moff + i) / PAGE)] + [(moff + i) % PAGE] = p[i]; + } +} + +/* Provision the mock header with n slots. */ +static void setup_hdr(int n) +{ + struct snvm_keystore_hdr hdr; + setup_pattern(); + memset(&hdr, 0, sizeof(hdr)); + memcpy(hdr.magic, SNVM_KEYSTORE_MAGIC, 8); + hdr.item_count = (uint16_t)n; + memcpy(mock_snvm[SNVM_KEYSTORE_MODULE], &hdr, sizeof(hdr)); +} + +START_TEST(test_num_pubkeys_valid) +{ + setup_hdr(3); + ck_assert_int_eq(keystore_num_pubkeys(), 3); +} +END_TEST + +START_TEST(test_num_pubkeys_bad_magic) +{ + setup_hdr(3); + mock_snvm[SNVM_KEYSTORE_MODULE][0] = 'X'; + ck_assert_int_eq(keystore_num_pubkeys(), 0); + ck_assert_int_eq(wolfBoot_panicked, 0); +} +END_TEST + +/* A zeroized page is reported and halts; a merely corrupt one just yields + * no keys. */ +START_TEST(test_num_pubkeys_zeroized_halts) +{ + setup_hdr(3); + memset(mock_snvm[SNVM_KEYSTORE_MODULE], 0, PAGE); + wolfBoot_panicked = 0; + ck_assert_int_eq(keystore_num_pubkeys(), 0); + ck_assert_int_eq(wolfBoot_panicked, 1); + wolfBoot_panicked = 0; +} +END_TEST + +/* A corrupt header must not drive slot indexing past the keystore; the OTP + * sibling bounds item_count the same way. */ +START_TEST(test_num_pubkeys_over_max) +{ + setup_hdr(SNVM_KEYSTORE_MAX_PUBKEYS + 1); + ck_assert_int_eq(keystore_num_pubkeys(), 0); +} +END_TEST + +START_TEST(test_num_pubkeys_at_max) +{ + setup_hdr(SNVM_KEYSTORE_MAX_PUBKEYS); + ck_assert_int_eq(keystore_num_pubkeys(), SNVM_KEYSTORE_MAX_PUBKEYS); +} +END_TEST + +/* A correctly provisioned slot reports its real size. */ +START_TEST(test_get_size_valid) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + ck_assert_int_eq(keystore_get_size(0), KEYSTORE_PUBKEY_SIZE); +} +END_TEST + +/* A corrupt slot must not report a size past the fixed slot cache, which + * callers use as a hash length and coordinate offset. */ +START_TEST(test_get_size_oversize_rejected) +{ + setup_hdr(1); + setup_slot(0, 2 * KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +START_TEST(test_get_size_just_over_rejected) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE + 1, 1, 0xFFFFFFFF); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +/* An out-of-range id yields the documented not-found values, not a slot. */ +START_TEST(test_accessors_reject_bad_id) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + ck_assert_ptr_eq(keystore_get_buffer(5), NULL); + ck_assert_int_eq(keystore_get_size(5), -1); + ck_assert_uint_eq(keystore_get_mask(5), 0); + ck_assert_uint_eq(keystore_get_key_type(5), (uint32_t)-1); +} +END_TEST + +/* key_type and mask come back as provisioned. */ +START_TEST(test_get_key_type_and_mask) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 7, 0x0000A5A5); + ck_assert_uint_eq(keystore_get_key_type(0), 7); + ck_assert_uint_eq(keystore_get_mask(0), 0x0000A5A5); +} +END_TEST + +/* An sNVM failure while loading the slot propagates through every accessor + * rather than handing back the previous slot left in the cache. */ +START_TEST(test_accessors_propagate_read_error) +{ + setup_hdr(1); + setup_slot(0, KEYSTORE_PUBKEY_SIZE, 1, 0xFFFFFFFF); + mock_snvm_fail_module = SNVM_KEYSTORE_MODULE; + ck_assert_ptr_eq(keystore_get_buffer(0), NULL); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +Suite *snvm_keystore_suite(void) +{ + Suite *s = suite_create("snvm-keystore"); + TCase *tc = tcase_create("snvm-keystore"); + + tcase_add_test(tc, test_read_within_one_page); + tcase_add_test(tc, test_read_spans_two_pages); + tcase_add_test(tc, test_read_spans_three_pages); + tcase_add_test(tc, test_read_at_exact_boundary); + tcase_add_test(tc, test_read_past_module_max); + tcase_add_test(tc, test_read_error_propagates); + tcase_add_test(tc, test_num_pubkeys_valid); + tcase_add_test(tc, test_num_pubkeys_bad_magic); + tcase_add_test(tc, test_num_pubkeys_zeroized_halts); + tcase_add_test(tc, test_num_pubkeys_over_max); + tcase_add_test(tc, test_num_pubkeys_at_max); + tcase_add_test(tc, test_get_size_valid); + tcase_add_test(tc, test_get_size_oversize_rejected); + tcase_add_test(tc, test_get_size_just_over_rejected); + tcase_add_test(tc, test_accessors_reject_bad_id); + tcase_add_test(tc, test_get_key_type_and_mask); + tcase_add_test(tc, test_accessors_propagate_read_error); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = snvm_keystore_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From 5431297c5540027c36dfeacfad476b388e3f506d Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 5 Oct 2026 16:04:51 -0700 Subject: [PATCH 4/9] PolarFire SoC: read DDR once after init so CPU writes land, drop the workarounds --- config/examples/polarfire_mpfs250_m.config | 17 -- docs/Targets.md | 26 +-- docs/polarfire_snvm_puf.md | 6 +- hal/mpfs250.c | 187 +-------------------- hal/mpfs250.h | 4 +- hal/mpfs250_ddr.c | 18 ++ src/image.c | 15 -- 7 files changed, 38 insertions(+), 235 deletions(-) diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index 41a361f63a..faf4e143cb 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -36,8 +36,6 @@ SPMATHALL?=1 DUALBANK_SWAP?=0 PKA?=0 ENCRYPT=0 -# Decrypt via a staging buffer: see the SDHCI_BLOCK_VIA_PDMA note above. -DISK_DECRYPT_STAGING=1 WOLFTPM?=0 ELF?=1 #DEBUG_ELF?=1 @@ -129,21 +127,6 @@ CFLAGS_EXTRA+=-DBOOT_PART_B=1 # Speed up disk partition read (512KB chunks - max DMA size) CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x80000 -# Disable SDMA on the Cadence SD4HC. SDMA hangs silently at first -# multi-block read on the Video Kit (Cadence boundary-cross bug). -# Use PIO single-block reads instead. -CFLAGS_EXTRA+=-DSDHCI_SDMA_DISABLED - -# Force single-block (CMD17) reads. Multi-block PIO suffers a BRR -# race on Arasan/Cadence-family controllers; single-block avoids it. -CFLAGS_EXTRA+=-DSDHCI_FORCE_SINGLE_BLOCK_READ - -# Disk-load via PDMA staging: SDHCI PIO into an L2 Scratch staging buffer, -# then mpfs_pdma_memcpy() lands each block in DDR through the PDMA master. -# A CPU copy into DDR reads back wrong depending on access order and code -# layout (cause not identified); the PDMA path has never failed. -CFLAGS_EXTRA+=-DSDHCI_BLOCK_VIA_PDMA - # Video Kit routes the SD slot's Card Detect (CD#) signal through the FPGA # fabric rather than MSSIO, so the SDHCI controller's hardware CI/CDPL # detection always reads 'no card' in M-mode (no fabric configuration). diff --git a/docs/Targets.md b/docs/Targets.md index 41ea8da878..e7a09038ab 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1665,19 +1665,19 @@ Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB | ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | | ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | -Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit for the same 19.7 MB FIT, plaintext, ECC384/SHA384, with the DDR training and SD-card load included: - -| Phase | Software crypto | Athena offload | -|-----------------------------------------------|-----------------|----------------| -| DDR training, SD init, GPT read | ~1.5 s | ~1.5 s | -| SD load of the FIT (CMD17 single block + PDMA staging) | ~15.5 s | ~15.5 s | -| SHA384 integrity (reads through the non-cached alias) | ~4.1 s | ~4.8 s | -| ECC384 signature verify | ~0.7 s | ~0.7 s | -| FIT kernel copy to its load address (PDMA + read-back verify) | ~9.1 s | ~9.1 s | -| M-mode -> S-mode handoff | ~31 s | ~32 s | -| Linux login prompt | ~53-62 s | ~53 s | - -The offload does not shorten the integrity check on this path because the time is in reading the image through the non-cached DDR alias, not in the hashing; measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. The SD read and the PDMA copy with its byte-wise verify are the dominant costs; both exist because of the CPU-write-to-DDR coherence workaround (`SDHCI_BLOCK_VIA_PDMA`). ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. +Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit with host-side timestamps on the console, plaintext FIT, ECC384/SHA384, software crypto, the same 19.7 MB kernel either gzip-compressed in the FIT (5.9 MB) or stored uncompressed (19.8 MB): + +| Phase | gzip FIT | uncompressed FIT | +|-----------------------------------------------|----------|------------------| +| DDR training, SD init, GPT read, FIT load, SHA384 integrity, ECC384 verify | ~2 s | ~4 s | +| gzip inflate of the kernel | ~16 s | - | +| FIT copies, DTB fixups | <1 s | <1 s | +| M-mode -> S-mode handoff | ~18 s | ~5 s | +| Linux login prompt | ~41 s | ~28 s | + +The inflate runs on the E51 at about 1.2 MB/s of output, so a compressed kernel costs far more than the extra card read it saves; for a short boot, build the FIT with `compression = "none"` for the kernel (`FIT_KERNEL_COMP_ALG = "none"` in Yocto). The software SHA384 runs at about 11 MB/s on the E51 and the ECC384 verify is well under a second; neither is a lever. `BOOT_BENCHMARK` phase prints on this target are in real time now that the timer uses the live CPU clock. + +The LPDDR4 controller on this board needs the first CPU-side transaction after DDR initialization to be a read: when a write comes first on a cold boot, it lands in DDR displaced by several bus beats and every later cached-port write in that boot follows it. `mpfs_ddr_init()` therefore reads one block through the non-cached alias before anything is written, after which CPU, L2 write-back and SDMA writes land exactly and the SD load, the FIT copies, the DTB fixups and the image hash use DDR directly. Measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. ### PolarFire Soc Debugging diff --git a/docs/polarfire_snvm_puf.md b/docs/polarfire_snvm_puf.md index 81f1083ecb..bbb77120cd 100644 --- a/docs/polarfire_snvm_puf.md +++ b/docs/polarfire_snvm_puf.md @@ -81,10 +81,6 @@ wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partiti - `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. - `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. -### PolarFire M-mode decrypt note - -The image is loaded into DDR by the SD controller and decrypted there. CPU stores into that region are not coherent with what the controller wrote (which is why the load uses `SDHCI_BLOCK_VIA_PDMA`), so `polarfire_mpfs250_m.config` sets `DISK_DECRYPT_STAGING=1`: the ciphertext is read through the non-cached DDR alias, decrypted a chunk at a time into a staging buffer and landed with the same PDMA copy the load used. - ## Build flags summary | Flag | Effect | @@ -124,7 +120,7 @@ make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR=0xCx and flushes - * L2. Chunked + WDT-petted for kernel-sized copies. */ -int wolfBoot_fit_memcpy(void *dst, const void *src, uint32_t len) -{ - uintptr_t d = (uintptr_t)dst; - uintptr_t s = (uintptr_t)src; - volatile const uint8_t *ncd; - const uint8_t *ncs; - uint32_t off = 0; - uint32_t chunk; - uint32_t k; - int retry; - int mism; - int rc = 0; - - if ((s & 0xF0000000UL) == 0x80000000UL) { - s |= 0x40000000UL; /* non-cached source alias */ - } - while (off < len) { - chunk = len - off; - if (chunk > (1024U * 1024U)) { - chunk = 1024U * 1024U; - } - /* mpfs_pdma_memcpy always returns 0, so the read-back verify below is - * the authoritative success check for this chunk. The PDMA->DDR write - * intermittently drops a block, so re-PDMA on a mismatch (same pattern - * as sdhci_platform_block_copy). A DDR destination (0x8xxxxxxx) is - * read back through its non-cached alias (| 0x40000000) so we compare - * what actually landed in DDR, not stale L2; this makes the caller's - * fail-closed rc real for the signature-uncovered kernel/dtb copies. - * A non-DDR destination (e.g. an L2 scratch buffer) lands directly, so - * a single copy suffices. */ - mism = 1; - for (retry = 0; retry < 8 && mism != 0; retry++) { - (void)mpfs_pdma_memcpy((void *)(d + off), - (const void *)(s + off), chunk); - /* Refresh all five MSS watchdogs (they always count and reset the - * chip and cannot be disabled) during the multi-MB kernel copy - * and its read-back verify. */ - MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_1_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_2_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_3_BASE) = 0xDEADC0DEU; - MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; - if ((d & 0xF0000000UL) != 0x80000000UL) { - mism = 0; /* non-DDR dst lands on the first copy */ - break; - } - __asm__ volatile("fence iorw,iorw" ::: "memory"); - ncd = (volatile const uint8_t *)((d + off) | 0x40000000UL); - ncs = (const uint8_t *)(s + off); - mism = 0; - for (k = 0; k < chunk; k++) { - if (ncd[k] != ncs[k]) { - mism = 1; - break; - } - } - } - if (mism != 0) { - /* Copy could not be verified within the retry budget; remember the - * failure so the caller fails closed rather than boot corrupt, - * no-longer-signature-covered data. */ - rc = -1; - } - off += chunk; - } - return rc; -} - -/* L2 round-trip wrapper around mpfs_dts_fixup_inplace(). The dtb lives in DDR - * (WOLFBOOT_LOAD_DTS_ADDRESS) but CPU writes to DDR do not land here, so copy - * it (non-cached read) into an L2 scratch buffer, run the FDT fixups there - * (CPU L2 writes work), then PDMA the result back to DDR. */ -int hal_dts_fixup(void* dts_addr, uint32_t capacity) -{ - static uint8_t l2_dtb[64 * 1024] __attribute__((aligned(8))); - fdt_ctx ctx; - const uint8_t *ddr_nc; - uint32_t sz; - int ret; - - if (dts_addr == NULL) { - return -1; - } - ddr_nc = (const uint8_t *)((uintptr_t)dts_addr | 0x40000000UL); - /* The source is bounded by whichever is smaller: the caller's DDR - * window, or what the L2 scratch buffer can hold once the fixup - * headroom is set aside. fdt_open() enforces it, so the memcpy below - * cannot overrun l2_dtb however corrupt the header is. */ - sz = (uint32_t)(sizeof(l2_dtb) - WOLFBOOT_FDT_FIXUP_HEADROOM); - if (capacity < sz) { - sz = capacity; - } - if (fdt_open(&ctx, (void *)ddr_nc, sz) != 0) { - wolfBoot_printf("FDT: invalid header at %p\n", dts_addr); - return -1; - } - sz = fdt_size(&ctx); - /* DDR (non-cached) -> L2 */ - memcpy(l2_dtb, ddr_nc, sz); - /* fixup in the CPU-writable L2 buffer, which may use the whole of it */ - ret = mpfs_dts_fixup_inplace(l2_dtb, (uint32_t)sizeof(l2_dtb)); - /* L2 -> DDR via PDMA (expanded totalsize) */ - if (fdt_open(&ctx, l2_dtb, (uint32_t)sizeof(l2_dtb)) != 0) { - wolfBoot_printf("FDT: fixed-up dtb rejected\n"); - return -1; - } - if (wolfBoot_fit_memcpy(dts_addr, l2_dtb, fdt_size(&ctx)) != 0) { - wolfBoot_printf("FDT: dtb copy-back to DDR failed\n"); - return -1; - } - return ret; -} -#else -/* Without the M-mode DDR constraints the dtb buffer is CPU-writable, so - * run the fixups directly in place (the original behavior, kept so - * FDT-enabled non-DDR builds do not silently fall back to the weak - * no-op hal_dts_fixup). */ +/* Overrides the weak no-op hal_dts_fixup so FDT-enabled builds get the + * fixups above. */ int hal_dts_fixup(void* dts_addr, uint32_t capacity) { if (dts_addr == NULL) { @@ -1642,7 +1506,6 @@ int hal_dts_fixup(void* dts_addr, uint32_t capacity) } return mpfs_dts_fixup_inplace(dts_addr, capacity); } -#endif /* WOLFBOOT_RISCV_MMODE && MPFS_DDR_INIT */ void hal_prepare_boot(void) { @@ -2672,8 +2535,7 @@ static void mpfs_mpu_init_mmc(void) } #endif /* MPFS_DDR_INIT */ -#ifdef SDHCI_BLOCK_VIA_PDMA -/* Pet all five MSS watchdogs during the (long) per-block SDHCI read loop. +/* Pet all five MSS watchdogs during the SDHCI read loops. * Overrides the weak no-op in src/sdhci.c. The MSS watchdogs always count * and reset the chip at timeout and cannot be disabled, so the multi-second * load of a large image must keep refreshing them. */ @@ -2686,49 +2548,6 @@ void sdhci_platform_wdt_pet(void) MSS_WDT_REFRESH(MSS_WDT_U54_4_BASE) = 0xDEADC0DEU; } -/* Copy a staged SDHCI block to its final destination (overrides the weak - * memcpy default in src/sdhci.c). Direct CPU writes to DDR do not land on - * this board, so a DDR destination (0x8xxxxxxx) is written through the PDMA - * master and verified via its non-cached alias (| 0x40000000), re-PDMA'ing on - * a drop (the PDMA->DDR write intermittently drops a block when interleaved - * with SDHCI reads). A non-DDR destination (L2 header/GPT buffers) is a plain - * CPU copy, which lands. Returns 0 on success, -1 if a DDR write cannot be - * verified within the retry budget. */ -int sdhci_platform_block_copy(void *dst, const void *src, uint32_t len) -{ - volatile const uint8_t *ncv; - const uint8_t *s = (const uint8_t *)src; - int retry; - int mism; - uint32_t k; - - if (((uintptr_t)dst & 0xF0000000UL) != 0x80000000UL) { - memcpy(dst, src, len); - return 0; - } - ncv = (volatile const uint8_t *)((uintptr_t)dst | 0x40000000UL); - mism = 1; - for (retry = 0; retry < 8 && mism != 0; retry++) { - /* The read-back verify below is the authoritative success check, so - * a PDMA-engine error is caught there and retried like any drop. */ - (void)mpfs_pdma_memcpy(dst, src, len); - sdhci_platform_wdt_pet(); - __asm__ volatile("fence iorw,iorw" ::: "memory"); - mism = 0; - for (k = 0; k < len; k++) { - if (ncv[k] != s[k]) { - mism = 1; - break; - } - } - } - if (mism != 0) { - return -1; - } - return 0; -} -#endif /* SDHCI_BLOCK_VIA_PDMA */ - void sdhci_platform_init(void) { /* IOMUX/MSSIO routing was already programmed in nwc_init() before the diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 48ae21598b..5dd9c6869e 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -378,9 +378,11 @@ int mpfs_nonce(uint8_t *nonce); #define RTC_CLOCK_FREQ 1000000UL /* In M-mode CLINT MTIME is not running without HSS; use mcycle (CPU clock) instead. + * The E51 clock is raised from the reset rate to the Libero PLL rate during + * init, so the rate is the live value, not MSS_CPU_CLK. * In S-mode MTIME runs at 1 MHz (default RISCV_SMODE_TIMER_FREQ). */ #if defined(WOLFBOOT_RISCV_MMODE) && !defined(RISCV_SMODE_TIMER_FREQ) -#define RISCV_SMODE_TIMER_FREQ MSS_CPU_CLK +#define RISCV_SMODE_TIMER_FREQ (mpfs_cpu_freq_mhz * 1000000UL) #endif /* Hart Local Storage (HLS) - per-hart communication structure, 64 bytes at top of stack */ diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index b57fe62b8c..1b454c7d3b 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -3358,6 +3358,24 @@ int mpfs_ddr_init(unsigned int outer_retry) * underlying address-decoder issue is still affecting writes. */ mpfs_clear_bootup_cache_ways(0xC0000000UL, 1UL * 1024UL * 1024UL); + /* The first transaction the CPU side sends to the controller after init + * must be a read. On a cold boot a write first lands displaced by + * several bus beats and every later cached-port write in that boot + * follows it; a non-cached read of one block before any write leaves + * the port clean. The PDMA fill above goes through the switch port + * and does not count. */ + { + volatile uint8_t *ncv = (volatile uint8_t *)0xC0000000UL; + uint32_t k; + uint32_t sink = 0; + + for (k = 0; k < 512; k++) { + sink += ncv[k]; + } + __asm__ volatile("fence iorw,iorw" ::: "memory"); + (void)sink; + } + wolfBoot_printf("DDR: Initialization COMPLETE\n"); diff --git a/src/image.c b/src/image.c index 03056e1b41..745767abd5 100644 --- a/src/image.c +++ b/src/image.c @@ -1079,19 +1079,6 @@ static uint8_t *get_sha_block(struct wolfBoot_image *img, uint32_t offset) } #endif p = (uint8_t *)(img->fw_base + offset); -#if defined(MPFS_DDR_INIT) - /* PolarFire SoC DDR build: route in-DDR image-body reads through the - * non-cached DDR SEG window (0xC0000000 base) so cache fills don't evict - * L2 Scratch lines (where wolfBoot's own code/stack live). PDMA already - * L2-flushed the cached writes at disk-load, so the non-cached side reads - * the correct DDR contents. 0x8xxxxxxx -> 0xCxxxxxxx. Applied at this - * single point so every image-body hasher (SHA256/384/3-384) behaves - * identically; inert for the L2-Scratch QSPI M-mode build (fw_base is not - * in the 0x8xxxxxxx window). */ - if (((uintptr_t)p & 0xF0000000UL) == 0x80000000UL) { - p = (uint8_t *)((uintptr_t)p | 0x40000000UL); - } -#endif return p; } @@ -1375,8 +1362,6 @@ static int image_sha384(struct wolfBoot_image *img, uint8_t *hash) blksz = WOLFBOOT_SHA_BLOCK_SIZE; if (position + blksz > img->fw_size) blksz = img->fw_size - position; - /* p is already routed to the non-cached DDR alias by - * get_sha_block() under MPFS_DDR_INIT (see above). */ wc_Sha384Update(&sha384_ctx, p, blksz); position += blksz; wolfBoot_watchdog_feed(); From 38653b2dace8cf144be628f5553e9db806079919 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 8 Oct 2026 08:35:20 -0700 Subject: [PATCH 5/9] PolarFire SoC: a FIT ramdisk that must load, and failover actions before the golden boot --- docs/compile.md | 4 + docs/polarfire_snvm_puf.md | 5 + hal/mpfs250_snvm.c | 40 ++ include/disk_trailer.h | 12 +- include/hal.h | 13 + options.mk | 25 + src/fdt.c | 5 +- src/update_disk.c | 208 +++++++- src/update_ram.c | 8 +- tools/unit-tests/Makefile | 7 +- tools/unit-tests/unit-update-disk-failover.c | 473 +++++++++++++++++++ 11 files changed, 771 insertions(+), 29 deletions(-) create mode 100644 tools/unit-tests/unit-update-disk-failover.c diff --git a/docs/compile.md b/docs/compile.md index f1377cea37..6f0c06ac6e 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -386,6 +386,10 @@ The magic is load-bearing rather than decorative. A freshly imaged partition tai 3. Whatever comes up clears it to **`success`**. 4. If the next boot still finds **`testing`**, that slot did not come up. wolfBoot skips it and boots the other slot. +`DISK_BOOT_TRIES=N` (default 1) gives a staged slot N unconfirmed boots before it is reaped: wolfBoot arms the slot as `0x10 | (N-1)` and, at every boot that still finds it unconfirmed, writes the count down by one before reading the image, so a boot that dies early still counts; `0x10` is the last try. A reader that knows only the four plain states sees `0x11..0x1F` as unknown and never reaps. + +With `DISK_GOLDEN_SLOT=1`, `DISK_FAILOVER_ZEROIZE=1` runs `hal_disk_failover(cause)` once, just before the golden slot is booted because both operational slots were given up on (no image, verification failure, unconfirmed boots, rollback, or a missing image key). The default hook erases the image key (`wolfBoot_erase_encrypt_key`) and zeroes the head of both operational slots so neither can be elected again; a platform overrides it for its own actions. The golden image must then be a plaintext signed image: with `DISK_ENCRYPT` the golden slot is verified but not decrypted, and a missing key marks the operational slots unbootable instead of stopping the boot. + **A slot is only ever put on probation if an update was staged into it.** A slot that is `new`, or already `success`, is booted without anything being written, so a device that never stages an update is never probated and a steady-state boot performs no writes at all. This mirrors `src/update_ram.c`, which also promotes only `updating`. The consequence is deliberate: enabling this cannot strand a system whose OS does not confirm, but an integrator who writes an image with plain `dd` and never marks the slot gets no protection either. Skipping an unconfirmed slot writes nothing. Its version is dropped from the election in memory, which also removes it from the anti-rollback ceiling, and that is what lets an older confirmed slot boot. **Anti-rollback itself is not relaxed**: a slot that merely fails verification keeps its version and still blocks an older slot, exactly as before. diff --git a/docs/polarfire_snvm_puf.md b/docs/polarfire_snvm_puf.md index bbb77120cd..7e99f52ea4 100644 --- a/docs/polarfire_snvm_puf.md +++ b/docs/polarfire_snvm_puf.md @@ -81,6 +81,10 @@ wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partiti - `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. - `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. +## Field-recoverable zeroize + +`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. + ## Build flags summary | Flag | Effect | @@ -93,6 +97,7 @@ wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partiti | `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | | `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | | `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | +| `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | | `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | | `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c index aaf2531350..541522fcd2 100644 --- a/hal/mpfs250_snvm.c +++ b/hal/mpfs250_snvm.c @@ -424,10 +424,50 @@ int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce) return -1; } +#ifdef DISK_FAILOVER_ZEROIZE +/* Zero the sNVM page holding the wrapped image key, read it back. A page + * that already reads as zeros is left alone: sNVM write cycles are finite + * and a brownout during a write can lock the page read-only. */ +int wolfBoot_erase_encrypt_key(void) +{ + static uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; + static const uint8_t zeros[MPFS_SNVM_PLAIN_DATA_LEN]; + int ret; + + ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, sizeof(page)); + if (ret != 0) { + wolfBoot_printf("failover: image key page %u read failed (%d)\n", + (unsigned)SNVM_ENCKEY_MODULE, ret); + return -1; + } + if (memcmp(page, zeros, sizeof(page)) == 0) { + wolfBoot_printf("failover: image key page %u already zero\n", + (unsigned)SNVM_ENCKEY_MODULE); + return 0; + } + ret = mpfs_snvm_write(SYS_SERV_CMD_SNVM_WRITE_PLAIN, SNVM_ENCKEY_MODULE, + zeros, NULL); + if (ret != 0) { + wolfBoot_printf("failover: image key page %u write failed (%d)\n", + (unsigned)SNVM_ENCKEY_MODULE, ret); + return -1; + } + ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, sizeof(page)); + if ((ret != 0) || (memcmp(page, zeros, sizeof(page)) != 0)) { + wolfBoot_printf("failover: image key page %u did not read back as " + "zeros\n", (unsigned)SNVM_ENCKEY_MODULE); + return -1; + } + wolfBoot_printf("failover: image key page %u zeroized\n", + (unsigned)SNVM_ENCKEY_MODULE); + return 0; +} +#else int wolfBoot_erase_encrypt_key(void) { return -1; } +#endif #ifdef SNVM_ENCKEY_PROVISION /* One-time: wrap the AES key with the device PUF KEK and store diff --git a/include/disk_trailer.h b/include/disk_trailer.h index b0d956eab9..3a73b3bc15 100644 --- a/include/disk_trailer.h +++ b/include/disk_trailer.h @@ -56,6 +56,13 @@ #define DISK_STATE_TESTING 0x10U /* IMG_STATE_TESTING */ #define DISK_STATE_SUCCESS 0x00U /* IMG_STATE_SUCCESS */ +/* TESTING keeps the number of unconfirmed boots still allowed in its low + * nibble: 0x10 is the last one, 0x12 allows two more. A reader that knows + * only the four plain states sees 0x11..0x1F as unknown and never reaps. */ +#define DISK_STATE_TESTING_MASK 0xF0U +#define DISK_STATE_TRIES_MASK 0x0FU +#define DISK_STATE_IS_TESTING(s) (((s) & DISK_STATE_TESTING_MASK) == DISK_STATE_TESTING) + /* Byte offset of the trailer in a partition of size sz, or a non-zero return * when the partition is too small to carry one. Derived from the size the * caller observed, so both programs locate it the same way: from the media, @@ -104,11 +111,12 @@ static inline const char *disk_trailer_state_name(uint8_t state) return "NEW"; case DISK_STATE_UPDATING: return "UPDATING"; - case DISK_STATE_TESTING: - return "TESTING"; case DISK_STATE_SUCCESS: return "SUCCESS"; default: + if (DISK_STATE_IS_TESTING(state)) { + return "TESTING"; + } return "UNKNOWN"; } } diff --git a/include/hal.h b/include/hal.h index 103c999a77..f217e8f6ff 100644 --- a/include/hal.h +++ b/include/hal.h @@ -147,8 +147,21 @@ int hal_dma_set_noncached(uintptr_t start, uintptr_t end); * cannot be silently disabled by adding an fdt sub-image. Runs while the * boot disk is still open. Weak default is a no-op returning 0. */ int hal_boot_slot_select(void); + #endif +/* Disk boot: why the operational slots are being given up on, handed to + * hal_disk_failover() just before the golden slot is booted. */ +enum disk_failover_cause { + DISK_FAILOVER_NO_IMAGE = 1, /* no readable, parseable image */ + DISK_FAILOVER_VERIFY, /* integrity or signature failure */ + DISK_FAILOVER_UNCONFIRMED, /* boot confirmation never arrived */ + DISK_FAILOVER_ROLLBACK, /* only older images left */ + DISK_FAILOVER_NO_KEY /* the image key could not be obtained */ +}; +int hal_disk_failover(int cause); + + #if defined(MMU) || defined(WOLFBOOT_FDT) /* Apply this target's device tree fixups in place, bounded by `capacity` * (the DTS staging window). Returns 0, or negative when the tree handed diff --git a/options.mk b/options.mk index 97573f93e6..b7d999b4e9 100644 --- a/options.mk +++ b/options.mk @@ -129,6 +129,31 @@ ifeq ($(MPFS_SCB_SMODE_DENY),1) -DWOLFBOOT_PMP_DENY_SIZE=0x1000UL endif +# Field recovery: when both operational slots are given up on and the golden +# slot is about to boot, zero the sNVM page holding the image key and the head +# of both operational slots (hal_disk_failover). The golden image must be a +# plaintext signed image. Writes sNVM, so it carries the same approval gate as +# the provisioning writers. +ifeq ($(DISK_FAILOVER_ZEROIZE),1) + ifneq ($(DISK_GOLDEN_SLOT)$(SNVM_KEK),11) + $(error DISK_FAILOVER_ZEROIZE=1 requires DISK_GOLDEN_SLOT=1 and SNVM_KEK=1) + endif + ifeq ($(SNVM_ENCKEY_PROVISION),1) + $(error DISK_FAILOVER_ZEROIZE=1 and SNVM_ENCKEY_PROVISION=1 undo each \ + other at every boot; build one or the other) + endif + ifneq ($(WOLFBOOT_SNVM_WRITE_APPROVED),1) + $(error DISK_FAILOVER_ZEROIZE writes sNVM: irreversible, and a \ + brownout mid-write can lock the page read-only. Re-run with \ + WOLFBOOT_SNVM_WRITE_APPROVED=1 to confirm.) + endif + CFLAGS+=-D"DISK_FAILOVER_ZEROIZE" +endif +# Unconfirmed boots a staged slot gets before DISK_BOOT_CONFIRM reaps it. +ifneq ($(DISK_BOOT_TRIES),) + CFLAGS+=-DDISK_BOOT_TRIES=$(DISK_BOOT_TRIES) +endif + # PolarFire SoC: PUF-derived KEK + RFC 3394 AES key-wrap for sNVM-stored keys. ifeq ($(SNVM_KEK),1) # The KEK only ever wraps the image-encryption key, so it rides on the diff --git a/src/fdt.c b/src/fdt.c index 257c04e6b2..7b208d377a 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -1700,9 +1700,8 @@ const char* fit_get_compatible(fdt_ctx* ctx, const char* image) * DTB context for the initrd fixup, or NULL to skip the fixup. * * Returns 0 on success, -1 if the ramdisk node was found but the - * load failed. The current callers ignore the return value - * (log-and-continue), so a missing/failed ramdisk does not abort - * the boot. */ + * load failed; the callers treat that as fatal, since a FIT that names + * a ramdisk has no other root filesystem to offer the kernel. */ int fit_load_ramdisk(fdt_ctx* ctx, const char* ramdisk_node, fdt_ctx* dts) { int rd_size = 0; diff --git a/src/update_disk.c b/src/update_disk.c index 8cdacb7a6a..405ae5ab5f 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -119,6 +119,14 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; #define BOOT_LABEL_B NULL #endif +/* Unconfirmed boots a freshly staged slot is allowed before it is reaped. */ +#ifndef DISK_BOOT_TRIES +#define DISK_BOOT_TRIES 1 +#endif +#if (DISK_BOOT_TRIES < 1) || (DISK_BOOT_TRIES > 16) +#error "DISK_BOOT_TRIES must be 1..16" +#endif + #ifndef MAX_FAILURES #define MAX_FAILURES 4 #endif @@ -154,6 +162,30 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; #define SLOT_IS_GOLDEN(s) 0 #endif +/* With the failover zeroize the image key may already be gone when the + * loader runs, and the golden image is a plaintext one; without it every + * slot is encrypted and a missing key is fatal as before. */ +/* The last reason an operational slot was passed over (enum + * disk_failover_cause in hal.h), reported to hal_disk_failover(). */ +static int failover_cause; + +#ifdef DISK_FAILOVER_ZEROIZE +#ifndef DISK_GOLDEN_SLOT +#error "DISK_FAILOVER_ZEROIZE needs DISK_GOLDEN_SLOT" +#endif +static int disk_key_ok = 1; +#define SLOT_ENCRYPTED_KEY_OK() (disk_key_ok != 0) +#define SLOT_ENCRYPTED(s) (!SLOT_IS_GOLDEN(s)) +/* Bytes zeroed at the start of each operational slot: the manifest header + * and the signature go, so the image can never be elected again. */ +#ifndef DISK_FAILOVER_ERASE_SIZE +#define DISK_FAILOVER_ERASE_SIZE IMAGE_HEADER_SIZE +#endif +#else +#define SLOT_ENCRYPTED_KEY_OK() 1 +#define SLOT_ENCRYPTED(s) 1 +#endif + #ifndef DISK_BLOCK_SIZE #define DISK_BLOCK_SIZE 512 #endif @@ -368,6 +400,8 @@ static uint8_t disk_trailer[DISK_TRAILER_SZ]; /* Last known state of each slot, filled in by disk_boot_state_reap(). */ static uint8_t slot_state[DISK_SLOTS]; +/* Slots disk_boot_state_reap() took out of the election. */ +static uint8_t slot_reaped[DISK_SLOTS]; /* Byte offset of the trailer within the slot's partition, or -1 when the slot * cannot carry one. */ @@ -434,6 +468,7 @@ static int slot_state_write(struct boot_slot *s, uint8_t state, return 0; } + /* Read both slots' states, and drop any slot still marked TESTING out of the * election. Such a slot was armed before the previous boot and nothing * confirmed it, so whatever it started did not come up. @@ -451,15 +486,30 @@ static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) for (i = 0; i < 2; i++) { st = DISK_STATE_NEW; slot_state[i] = DISK_STATE_NEW; + slot_reaped[i] = 0; if (slot_state_read(&boot_slots[i], &st) != 0) { continue; } slot_state[i] = st; - if (st != DISK_STATE_TESTING) { + if (!DISK_STATE_IS_TESTING(st)) { + continue; + } + if ((st & DISK_STATE_TRIES_MASK) != 0U) { + /* Not confirmed yet, but it still has tries: spend one and let + * it run again. The write happens before the image is read, + * so a boot that dies early still counts. */ + wolfBoot_printf("Slot %c was not confirmed; %u more boot(s)\r\n", + 'A' + i, (unsigned)(st & DISK_STATE_TRIES_MASK)); + if (slot_state_write(&boot_slots[i], (uint8_t)(st - 1U), 0) != 0) { + wolfBoot_printf("WARNING: could not record the retry on " + "p%d\r\n", boot_slots[i].part); + } continue; } wolfBoot_printf("Slot %c was not confirmed; skipping it\r\n", 'A' + i); + slot_reaped[i] = 1; + failover_cause = DISK_FAILOVER_UNCONFIRMED; if (i == 0) { *pA_ver = 0; } @@ -470,6 +520,59 @@ static void disk_boot_state_reap(uint32_t *pA_ver, uint32_t *pB_ver) } #endif /* DISK_BOOT_CONFIRM */ +#ifdef DISK_FAILOVER_ZEROIZE +/* Default failover actions when the operational slots are given up on: drop + * the image key (the platform's wolfBoot_erase_encrypt_key) and zero the + * head of both operational slots so neither image can be elected again. + * The golden slot is never touched. */ +static int disk_failover_default(int cause) +{ + /* One sector at a time: DISK_BLOCK_SIZE is the load chunk and can be + * hundreds of KB on targets that stage whole images. */ + static const uint8_t zeros[512]; + int i, ret = 0; + uint64_t off; + + wolfBoot_printf("Failover: operational images abandoned (cause %d)\r\n", + cause); +#ifdef DISK_ENCRYPT + if (wolfBoot_erase_encrypt_key() != 0) { + wolfBoot_printf("Failover: image key not erased\r\n"); + ret = -1; + } +#endif + for (i = 0; i < 2; i++) { + struct boot_slot *s = &boot_slots[i]; + if (s->ready == 0) { + continue; + } +#ifdef WOLFBOOT_DISK_FS + if (s->vol.type != FS_TYPE_RAW) { + continue; + } +#endif + for (off = 0; off < (uint64_t)DISK_FAILOVER_ERASE_SIZE; + off += sizeof(zeros)) { + if (disk_part_write(BOOT_DISK, s->part, off, sizeof(zeros), + zeros) != (int)sizeof(zeros)) { + wolfBoot_printf("Failover: p%d not erased\r\n", s->part); + ret = -1; + break; + } + } +#ifdef DISK_BOOT_CONFIRM + (void)slot_state_write(s, DISK_STATE_NEW, 0); +#endif + } + return ret; +} + +int WEAKFUNCTION hal_disk_failover(int cause) +{ + return disk_failover_default(cause); +} +#endif /* DISK_FAILOVER_ZEROIZE */ + /** * @brief Read from a boot slot. * @@ -668,6 +771,9 @@ void RAMFUNCTION wolfBoot_start(void) int selected; uint32_t *load_address; int failures = 0; +#ifdef DISK_FAILOVER_ZEROIZE + int failover_done = 0; +#endif uint32_t load_off; uint32_t max_ver; const uint8_t *hdr_ptr = NULL; @@ -706,16 +812,30 @@ void RAMFUNCTION wolfBoot_start(void) disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); wolfBoot_printf("Error initializing encryption\r\n"); +#ifdef DISK_FAILOVER_ZEROIZE + disk_key_ok = 0; + failover_cause = DISK_FAILOVER_NO_KEY; +#else wolfBoot_panic(); +#endif } /* Retrieve encryption key and nonce for disk decryption */ if (wolfBoot_get_encrypt_key(disk_encrypt_key, disk_encrypt_nonce) != 0) { disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); wolfBoot_printf("Error getting encryption key\r\n"); +#ifdef DISK_FAILOVER_ZEROIZE + /* The key is gone (a previous failover zeroized it): the encrypted + * operational images cannot boot, the plaintext golden one can. */ + disk_key_ok = 0; + failover_cause = DISK_FAILOVER_NO_KEY; +#else wolfBoot_panic(); +#endif + } + else { + wolfBoot_printf("Disk encryption enabled\r\n"); } - wolfBoot_printf("Disk encryption enabled\r\n"); #endif ret = disk_init(BOOT_DISK); @@ -789,7 +909,7 @@ void RAMFUNCTION wolfBoot_start(void) if (slot_read(&boot_slots[0], 0, IMAGE_HEADER_SIZE, p_hdr) == IMAGE_HEADER_SIZE) { #ifdef DISK_ENCRYPT - if (decrypt_header(p_hdr, dec_hdr) == 0) { + if (SLOT_ENCRYPTED_KEY_OK() && (decrypt_header(p_hdr, dec_hdr) == 0)) { /* Use local version parser to avoid double-decryption issue * when EXT_ENCRYPTED && MMU is also defined */ pA_ver = get_decrypted_blob_version(dec_hdr); @@ -804,7 +924,7 @@ void RAMFUNCTION wolfBoot_start(void) if (slot_read(&boot_slots[1], 0, IMAGE_HEADER_SIZE, p_hdr) == IMAGE_HEADER_SIZE) { #ifdef DISK_ENCRYPT - if (decrypt_header(p_hdr, dec_hdr) == 0) { + if (SLOT_ENCRYPTED_KEY_OK() && (decrypt_header(p_hdr, dec_hdr) == 0)) { /* Use local version parser to avoid double-decryption issue * when EXT_ENCRYPTED && MMU is also defined */ pB_ver = get_decrypted_blob_version(dec_hdr); @@ -824,6 +944,9 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef DISK_GOLDEN_SLOT /* Straight to the golden slot. A version-0 header is not a bootable * A/B image here any more than it was before this option existed. */ + if (failover_cause == 0) { + failover_cause = DISK_FAILOVER_NO_IMAGE; + } failures = MAX_FAILURES; #else #ifdef DISK_ENCRYPT @@ -865,6 +988,17 @@ void RAMFUNCTION wolfBoot_start(void) } wolfBoot_printf("Falling back to the golden image on p%d\r\n", boot_slots[SLOT_GOLDEN].part); +#ifdef DISK_FAILOVER_ZEROIZE + /* The operational images are being given up on: run the + * platform's failover actions once, before the recovery image + * is read. They may fail; the golden slot still boots. */ + if (!failover_done) { + failover_done = 1; + if (hal_disk_failover(failover_cause) != 0) { + wolfBoot_printf("Failover actions incomplete\r\n"); + } + } +#endif } #endif slot = &boot_slots[selected]; @@ -876,10 +1010,10 @@ void RAMFUNCTION wolfBoot_start(void) * with ALLOW_DOWNGRADE defined the version guard that would other- * wise refuse it is compiled out. Refuse it here so the exclusion * holds on every path into the slot, not just the first choice. */ - if (!SLOT_IS_GOLDEN(selected) && - slot_state[selected] == DISK_STATE_TESTING) { + if (!SLOT_IS_GOLDEN(selected) && slot_reaped[selected]) { wolfBoot_printf("Slot %c was not confirmed; not booting it\r\n", 'A' + selected); + failover_cause = DISK_FAILOVER_UNCONFIRMED; selected ^= 1; continue; } @@ -892,6 +1026,7 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_printf("Rollback to lower version not allowed\r\n"); #ifdef DISK_GOLDEN_SLOT /* Nothing newer can boot: only the golden slot is left. */ + failover_cause = DISK_FAILOVER_ROLLBACK; failures = MAX_FAILURES; continue; #else @@ -918,24 +1053,33 @@ void RAMFUNCTION wolfBoot_start(void) != IMAGE_HEADER_SIZE) { wolfBoot_printf("Error reading image header from disk: p%d\r\n", cur_part); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } hdr_ptr = p_hdr; #ifdef DISK_ENCRYPT - /* Decrypt header to parse image size */ - if (decrypt_header(p_hdr, dec_hdr) != 0) { + /* Decrypt header to parse image size. A golden image is signed + * but not encrypted: it must boot after the image key is gone. */ + if (!SLOT_ENCRYPTED(selected)) { + hdr_ptr = p_hdr; + } + else if (decrypt_header(p_hdr, dec_hdr) != 0) { wolfBoot_printf("Error decrypting header for %s\r\n", part_name); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } - hdr_ptr = dec_hdr; + else { + hdr_ptr = dec_hdr; + } #endif memset(&os_image, 0, sizeof(os_image)); ret = wolfBoot_open_image_address(&os_image, (void*)hdr_ptr); if (ret < 0) { wolfBoot_printf("Error parsing loaded image\r\n"); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } @@ -954,6 +1098,7 @@ void RAMFUNCTION wolfBoot_start(void) (((uint64_t)os_image.fw_size + (uint64_t)IMAGE_HEADER_SIZE) > fs_size(&slot->f))) { wolfBoot_printf("Image larger than file for %s\r\n", part_name); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } @@ -963,6 +1108,7 @@ void RAMFUNCTION wolfBoot_start(void) if (os_image.fw_size > WOLFBOOT_RAMBOOT_MAX_SIZE) { wolfBoot_printf("Image size %u exceeds max RAM load size\r\n", os_image.fw_size); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } @@ -975,6 +1121,7 @@ void RAMFUNCTION wolfBoot_start(void) if (os_image.fw_size > slot_max) { wolfBoot_printf("Image size %u doesn't fit in low memory\r\n", os_image.fw_size); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } @@ -1008,6 +1155,7 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_printf("Error reading image from disk: p%d " "(%u of %u bytes)\r\n", cur_part, (unsigned int)load_off, (unsigned int)os_image.fw_size); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } @@ -1015,24 +1163,27 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef DISK_ENCRYPT /* Decrypt the payload in RAM */ - wolfBoot_printf("Decrypting image..."); - BENCHMARK_START(); - if ((IMAGE_HEADER_SIZE % ENCRYPT_BLOCK_SIZE) != 0) { - disk_decrypted_header_clear(dec_hdr); - disk_crypto_clear(); - wolfBoot_printf("Encrypted disk images require aligned header size\r\n"); - wolfBoot_panic(); + if (SLOT_ENCRYPTED(selected)) { + wolfBoot_printf("Decrypting image..."); + BENCHMARK_START(); + if ((IMAGE_HEADER_SIZE % ENCRYPT_BLOCK_SIZE) != 0) { + disk_decrypted_header_clear(dec_hdr); + disk_crypto_clear(); + wolfBoot_printf("Encrypted disk images require aligned header size\r\n"); + wolfBoot_panic(); + } + disk_crypto_set_iv(IMAGE_HEADER_SIZE / ENCRYPT_BLOCK_SIZE); + crypto_decrypt((uint8_t*)load_address, (uint8_t*)load_address, + os_image.fw_size); + BENCHMARK_END("done"); } - disk_crypto_set_iv(IMAGE_HEADER_SIZE / ENCRYPT_BLOCK_SIZE); - crypto_decrypt((uint8_t*)load_address, (uint8_t*)load_address, - os_image.fw_size); - BENCHMARK_END("done"); #endif memset(&os_image, 0, sizeof(os_image)); ret = wolfBoot_open_image_address(&os_image, (void*)hdr_ptr); if (ret < 0) { wolfBoot_printf("Error parsing loaded image\r\n"); + failover_cause = DISK_FAILOVER_NO_IMAGE; selected ^= 1; continue; } @@ -1046,6 +1197,7 @@ void RAMFUNCTION wolfBoot_start(void) BENCHMARK_START(); if (wolfBoot_verify_integrity(&os_image) != 0) { wolfBoot_printf("Error validating integrity for %s\r\n", part_name); + failover_cause = DISK_FAILOVER_VERIFY; selected ^= 1; continue; } @@ -1056,6 +1208,7 @@ void RAMFUNCTION wolfBoot_start(void) if (wolfBoot_verify_authenticity(&os_image) != 0) { wolfBoot_printf("Error validating authenticity for %s\r\n", part_name); + failover_cause = DISK_FAILOVER_VERIFY; selected ^= 1; continue; } else { @@ -1095,7 +1248,8 @@ void RAMFUNCTION wolfBoot_start(void) * steady-state boot writes nothing at all. */ if (!SLOT_IS_GOLDEN(selected) && slot_state[selected] == DISK_STATE_UPDATING) { - if (slot_state_write(&boot_slots[selected], DISK_STATE_TESTING, + if (slot_state_write(&boot_slots[selected], + (uint8_t)(DISK_STATE_TESTING | (DISK_BOOT_TRIES - 1)), (uint64_t)IMAGE_HEADER_SIZE + (uint64_t)os_image.fw_size) != 0) { /* Loud, because the consequence is silent: without the TESTING @@ -1235,7 +1389,17 @@ void RAMFUNCTION wolfBoot_start(void) fdt_open(&dts_ctx, dts_addr, WOLFBOOT_DTS_MAX_SIZE) == 0) { dts_for_initrd = &dts_ctx; } - (void)fit_load_ramdisk(fit, ramdisk, dts_for_initrd); + /* A ramdisk the FIT names is part of the OS image: without it + * the kernel would come up with no root filesystem. */ + if ((ramdisk != NULL) && + (fit_load_ramdisk(fit, ramdisk, dts_for_initrd) != 0)) { + wolfBoot_printf("Failed to load the FIT ramdisk\r\n"); +#ifdef DISK_ENCRYPT + disk_decrypted_header_clear(dec_hdr); + disk_crypto_clear(); +#endif + wolfBoot_panic(); + } } #else (void)ramdisk; diff --git a/src/update_ram.c b/src/update_ram.c index cd7cf8bcdb..6bf54e33a1 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -699,7 +699,13 @@ void RAMFUNCTION wolfBoot_start(void) fdt_open(&dts_ctx, dts_addr, WOLFBOOT_DTS_MAX_SIZE) == 0) { dts_for_initrd = &dts_ctx; } - (void)fit_load_ramdisk(fit, ramdisk, dts_for_initrd); + /* A ramdisk the FIT names is part of the OS image: without it + * the kernel would come up with no root filesystem. */ + if ((ramdisk != NULL) && + (fit_load_ramdisk(fit, ramdisk, dts_for_initrd) != 0)) { + wolfBoot_printf("Failed to load the FIT ramdisk\r\n"); + wolfBoot_panic(); + } } #else (void)ramdisk; diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 2613e0116f..4ce1dac22a 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -68,7 +68,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update \ unit-nsc-update \ unit-update-flash-enc unit-update-flash-enc-full unit-update-ram unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ - unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ + unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-failover unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ @@ -438,6 +438,8 @@ unit-update-disk-confirm:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_R -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-disk-golden:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-disk-failover:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Non-FSP disk-boot OOB regression (CRIT-03). WOLFBOOT_RAMBOOT_MAX_SIZE is the # cap the loader applies to the unauthenticated header fw_size before loading to # RAM, the same bound update_disk.c and update_ram.c enforce. @@ -1156,6 +1158,9 @@ unit-update-disk-confirm: ../../include/target.h unit-update-disk-confirm.c unit-update-disk-golden: ../../include/target.h unit-update-disk-golden.c gcc -o $@ unit-update-disk-golden.c $(CFLAGS) $(LDFLAGS) +unit-update-disk-failover: ../../include/target.h unit-update-disk-failover.c + gcc -o $@ unit-update-disk-failover.c $(CFLAGS) $(LDFLAGS) + # WOLFBOOT_FSP (x86) boot path of update_disk.c: the low-memory (tolum) # size check must reject a slot and fall back to the other one, like every # other per-slot rejection in the retry loop. diff --git a/tools/unit-tests/unit-update-disk-failover.c b/tools/unit-tests/unit-update-disk-failover.c new file mode 100644 index 0000000000..fa3c7ab416 --- /dev/null +++ b/tools/unit-tests/unit-update-disk-failover.c @@ -0,0 +1,473 @@ +/* unit-update-disk-failover.c + * + * Unit test for the disk boot failover actions (DISK_FAILOVER_ZEROIZE) and + * the DISK_BOOT_TRIES count in the TESTING trailer state. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WOLFBOOT_UPDATE_DISK +#define WOLFBOOT_SELF_UPDATE_MONOLITHIC +#define RAM_CODE +#define WOLFBOOT_SELF_HEADER +#define EXT_ENCRYPTED +#define ENCRYPT_WITH_CHACHA +#define HAVE_CHACHA +#define IMAGE_HEADER_SIZE 256 +#define BOOT_PART_A 0 +#define BOOT_PART_B 1 +#define BOOT_PART_GOLDEN 2 +#define DISK_GOLDEN_SLOT +#define DISK_BOOT_CONFIRM +#define DISK_FAILOVER_ZEROIZE +#define DISK_BOOT_TRIES 3 +#define MOCK_ADDRESS_BOOT 0xCD000000 + +#include +#include +#include +#include + +#include "hal.h" +#include "target.h" +#include "wolfboot/wolfboot.h" +#include "image.h" +#include "loader.h" +#include + +#define TEST_PAYLOAD_SIZE 64 +#define TEST_PART_SIZE (IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE + 1024) +#define FILL_A 0xA1 +#define FILL_B 0xB2 +#define FILL_G 0xC3 +#define N_PARTS 3 + +static uint8_t load_buffer[TEST_PART_SIZE]; +#define WOLFBOOT_LOAD_ADDRESS ((uintptr_t)load_buffer) + +static uint8_t part_image[N_PARTS][TEST_PART_SIZE]; +static int mock_reads[N_PARTS]; +static int mock_writes[N_PARTS]; +static int mock_bad_fill[N_PARTS]; +static int mock_do_boot_called; +static int mock_booted_fill; +static int mock_key_erased; +static int mock_key_missing; +static int mock_key_erase_fail; +static int mock_write_fail_part; +ChaCha chacha; + +static void set_u16_le(uint8_t *dst, uint16_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)(value >> 8); +} + +static void set_u32_le(uint8_t *dst, uint32_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)((value >> 8) & 0xFF); + dst[2] = (uint8_t)((value >> 16) & 0xFF); + dst[3] = (uint8_t)(value >> 24); +} + +static void build_image(uint8_t *image, uint32_t version, uint8_t fill) +{ + memset(image, 0, TEST_PART_SIZE); + set_u32_le(image, WOLFBOOT_MAGIC); + set_u32_le(image + sizeof(uint32_t), TEST_PAYLOAD_SIZE); + set_u16_le(image + IMAGE_HEADER_OFFSET, HDR_VERSION); + set_u16_le(image + IMAGE_HEADER_OFFSET + sizeof(uint16_t), 4); + set_u32_le(image + IMAGE_HEADER_OFFSET + 2 * sizeof(uint16_t), version); + memset(image + IMAGE_HEADER_SIZE, fill, TEST_PAYLOAD_SIZE); +} + +static void reset_mocks(void) +{ + memset(load_buffer, 0, sizeof(load_buffer)); + build_image(part_image[0], 3, FILL_A); + build_image(part_image[1], 2, FILL_B); + build_image(part_image[2], 1, FILL_G); + memset(mock_reads, 0, sizeof(mock_reads)); + memset(mock_writes, 0, sizeof(mock_writes)); + memset(mock_bad_fill, 0, sizeof(mock_bad_fill)); + mock_do_boot_called = 0; + mock_booted_fill = -1; + mock_key_erased = 0; + mock_key_missing = 0; + mock_key_erase_fail = 0; + mock_write_fail_part = -1; + wolfBoot_panicked = 0; +} + +static void mark_bad(uint8_t fill) +{ + int i; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] == 0) { + mock_bad_fill[i] = fill; + return; + } + } +} + +int chacha_init(void) +{ + return 0; +} + +int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter) +{ + (void)ctx; (void)inIv; (void)counter; + return 0; +} + +int wc_Chacha_Process(ChaCha* ctx, byte* output, const byte* input, word32 msglen) +{ + (void)ctx; + memmove(output, input, msglen); + return 0; +} + +void wc_ForceZero(void* mem, size_t len) +{ + volatile uint8_t *p = (volatile uint8_t *)mem; + while (len-- > 0) + *p++ = 0; +} + +int wolfBoot_initialize_encryption(void) +{ + return mock_key_missing ? -1 : 0; +} + +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + if (mock_key_missing) { + return -1; + } + memset(key, 0x5A, ENCRYPT_KEY_SIZE); + memset(nonce, 0xC3, ENCRYPT_NONCE_SIZE); + return 0; +} + +int wolfBoot_erase_encrypt_key(void) +{ + mock_key_erased++; + return mock_key_erase_fail ? -1 : 0; +} + +int disk_init(int drv) +{ + (void)drv; + return 0; +} + +int disk_open(int drv) +{ + (void)drv; + return 0; +} + +void disk_close(int drv) +{ + (void)drv; +} + +int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + mock_reads[part]++; + memcpy(buf, part_image[part] + off, (size_t)sz); + return (int)sz; +} + +int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, + const uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + if (part == mock_write_fail_part) { + mock_writes[part]++; + return -1; + } + mock_writes[part]++; + memcpy(part_image[part] + off, buf, (size_t)sz); + return (int)sz; +} + +int disk_part_size(int drv, int part, uint64_t *size) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + *size = TEST_PART_SIZE; + return 0; +} + +int wolfBoot_open_image_address(struct wolfBoot_image* img, uint8_t* image) +{ + uint32_t magic; + uint32_t fw_size; + + memcpy(&magic, image, sizeof(magic)); + if (magic != WOLFBOOT_MAGIC) + return -1; + memset(img, 0, sizeof(*img)); + img->hdr = image; + memcpy(&fw_size, image + sizeof(uint32_t), sizeof(fw_size)); + img->fw_size = fw_size; + img->fw_base = image + IMAGE_HEADER_SIZE; + img->hdr_ok = 1; + return 0; +} + +int wolfBoot_verify_integrity(struct wolfBoot_image* img) +{ + int i; + uint8_t fill = load_buffer[0]; + (void)img; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] != 0 && mock_bad_fill[i] == fill) + return -1; + } + img->sha_ok = 1; + return 0; +} + +int wolfBoot_verify_authenticity(struct wolfBoot_image* img) +{ + img->signature_ok = 1; + return 0; +} + +int wolfBoot_get_dts_size(void *dts_addr, uint32_t capacity) +{ + (void)capacity; (void)dts_addr; + return -1; +} + +void hal_prepare_boot(void) +{ +} + +void do_boot(const uint32_t *address) +{ + mock_do_boot_called++; + mock_booted_fill = ((const uint8_t*)address)[0]; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; (void)len; + return 0; +} + +#include "update_disk.c" + +static void set_state(int part, uint8_t state) +{ + uint64_t off = 0; + ck_assert_int_eq(disk_trailer_offset(TEST_PART_SIZE, &off), 0); + disk_trailer_encode(part_image[part] + off, state); +} + +static uint8_t get_state(int part) +{ + uint64_t off = 0; + ck_assert_int_eq(disk_trailer_offset(TEST_PART_SIZE, &off), 0); + return disk_trailer_decode(part_image[part] + off); +} + +static int head_is_zero(int part) +{ + int i; + for (i = 0; i < IMAGE_HEADER_SIZE; i++) { + if (part_image[part][i] != 0) + return 0; + } + return 1; +} + +START_TEST(test_no_failover_when_a_boots) +{ + reset_mocks(); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(mock_key_erased, 0); + ck_assert_int_eq(mock_writes[BOOT_PART_A], 0); + ck_assert_int_eq(mock_writes[BOOT_PART_B], 0); +} +END_TEST + +START_TEST(test_failover_erases_key_and_operational_slots) +{ + reset_mocks(); + /* B at A's version, so the flip to B is a verification failure and not + * an anti-rollback refusal. */ + build_image(part_image[1], 3, FILL_B); + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_VERIFY); + ck_assert(head_is_zero(BOOT_PART_A)); + ck_assert(head_is_zero(BOOT_PART_B)); + ck_assert(!head_is_zero(BOOT_PART_GOLDEN)); + ck_assert_int_eq(get_state(BOOT_PART_A), DISK_STATE_NEW); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_failover_runs_once_and_golden_still_boots_if_it_fails) +{ + /* The key erase fails, so hal_disk_failover() returns an error: the + * golden image must still boot and the actions must not be retried. */ + reset_mocks(); + mock_key_erase_fail = 1; + memset(part_image[0], 0, TEST_PART_SIZE); + memset(part_image[1], 0, TEST_PART_SIZE); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_NO_IMAGE); +} +END_TEST + +START_TEST(test_golden_boots_when_a_slot_head_cannot_be_erased) +{ + /* Slot A rejects every write. The key is still erased, slot B is still + * zeroed, and the golden image still boots. */ + reset_mocks(); + mock_write_fail_part = BOOT_PART_A; + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert(!head_is_zero(BOOT_PART_A)); + ck_assert(head_is_zero(BOOT_PART_B)); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_missing_key_boots_plaintext_golden) +{ + reset_mocks(); + mock_key_missing = 1; + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_NO_KEY); +} +END_TEST + +START_TEST(test_staged_slot_is_armed_with_tries) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_UPDATING); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(get_state(BOOT_PART_A), + DISK_STATE_TESTING | (DISK_BOOT_TRIES - 1)); +} +END_TEST + +START_TEST(test_unconfirmed_slot_spends_tries_then_is_reaped) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_TESTING | 2); + wolfBoot_start(); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(get_state(BOOT_PART_A), DISK_STATE_TESTING | 1); + ck_assert_int_eq(mock_key_erased, 0); + + mock_do_boot_called = 0; + wolfBoot_start(); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(get_state(BOOT_PART_A), DISK_STATE_TESTING); + + /* The last try was spent: A is reaped, B (older) boots without any + * failover action, since one operational slot is still good. */ + mock_do_boot_called = 0; + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_B); + ck_assert_int_eq(mock_key_erased, 0); + ck_assert(!head_is_zero(BOOT_PART_A)); +} +END_TEST + +START_TEST(test_both_unconfirmed_fail_over_with_the_unconfirmed_cause) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_TESTING); + set_state(BOOT_PART_B, DISK_STATE_TESTING); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_key_erased, 1); + ck_assert_int_eq(failover_cause, DISK_FAILOVER_UNCONFIRMED); + ck_assert(head_is_zero(BOOT_PART_A)); + ck_assert(head_is_zero(BOOT_PART_B)); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot"); + TCase *tc = tcase_create("update-disk-failover"); + + tcase_add_test(tc, test_no_failover_when_a_boots); + tcase_add_test(tc, test_failover_erases_key_and_operational_slots); + tcase_add_test(tc, test_failover_runs_once_and_golden_still_boots_if_it_fails); + tcase_add_test(tc, test_golden_boots_when_a_slot_head_cannot_be_erased); + tcase_add_test(tc, test_missing_key_boots_plaintext_golden); + tcase_add_test(tc, test_staged_slot_is_armed_with_tries); + tcase_add_test(tc, test_unconfirmed_slot_spends_tries_then_is_reaped); + tcase_add_test(tc, test_both_unconfirmed_fail_over_with_the_unconfirmed_cause); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = wolfboot_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} From e9aae536ae80ae91f17758ab32b484cc3b0cf795 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 8 Oct 2026 08:49:55 -0700 Subject: [PATCH 6/9] PolarFire SoC: SBI vendor extension for the OS: PUF keys, System Controller services and flash access --- .github/workflows/test-configs.yml | 8 + Makefile | 3 + docs/polarfire_sbi_vendor.md | 31 ++ hal/mpfs250.c | 471 +++++++++++++++++++++++- hal/mpfs250.h | 23 +- hal/mpfs250_snvm.c | 69 +++- hal/mpfs250_snvm.h | 9 + include/riscv_sbi.h | 97 +++++ options.mk | 30 ++ src/riscv_sbi.c | 203 ++++++++-- src/vector_riscv.S | 45 ++- tools/linux/wolfboot-sbi/Makefile | 14 + tools/linux/wolfboot-sbi/wolfboot_sbi.c | 331 +++++++++++++++++ tools/linux/wolfboot-sbi/wolfboot_sbi.h | 91 +++++ 14 files changed, 1351 insertions(+), 74 deletions(-) create mode 100644 docs/polarfire_sbi_vendor.md create mode 100644 include/riscv_sbi.h create mode 100644 tools/linux/wolfboot-sbi/Makefile create mode 100644 tools/linux/wolfboot-sbi/wolfboot_sbi.c create mode 100644 tools/linux/wolfboot-sbi/wolfboot_sbi.h diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index cfcc8ba71f..ad515e99c9 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -530,6 +530,14 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1 MPFS_SCB_SMODE_DENY=1 + # SBI vendor extension (serial, PUF keys, fenced System Controller services). + microchip_mpfs250_m_sbi_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub MPFS_SBI_VENDOR=1 MPFS_SCB_SMODE_DENY=1 MPFS_SBI_SC_FLASH=1 # Encrypted disk image with the AES key unwrapped by the PUF-derived KEK. microchip_mpfs250_m_enc_kek_test: uses: ./.github/workflows/test-build-riscv.yml diff --git a/Makefile b/Makefile index 0c1c429fbe..439ab648b8 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,9 @@ endif ifeq ($(SNVM_KEK),1) MPFS_SNVM_OBJ=1 endif +ifeq ($(MPFS_SBI_VENDOR),1) + MPFS_SNVM_OBJ=1 +endif ifeq ($(MPFS_SNVM_OBJ),1) OBJS+=./hal/mpfs250_snvm.o endif diff --git a/docs/polarfire_sbi_vendor.md b/docs/polarfire_sbi_vendor.md new file mode 100644 index 0000000000..fa7c86629b --- /dev/null +++ b/docs/polarfire_sbi_vendor.md @@ -0,0 +1,31 @@ +# PolarFire SoC: wolfBoot SBI vendor extension + +The M-mode wolfBoot stays resident as the SBI for the OS. With `MPFS_SBI_VENDOR=1` it also serves the Microchip vendor extension, EID `0x09000029` (`SBI_EXT_VENDOR_START | mvendorid 0x029`), the same number the Hart Software Services register, so an OS probes one extension for either firmware and tells them apart with `GET_VERSION`. The HSS function IDs for inter-hart communication, remoteproc, crypto and IPC are answered with `SBI_ERR_NOT_SUPPORTED`; `HSS_REBOOT` (0x10) performs a cold MSS reset. + +The definitions live in `include/riscv_sbi.h`; the handler is `sbi_hal_vendor_ecall()` in `hal/mpfs250.c`. Arguments travel in `a0..a2`, the SBI error returns in `a0` and the value in `a1`. Buffers are addresses in the caller's address space: M-mode reads and writes them through the caller's translation, byte by byte, with a trap vector that turns a bad address into `SBI_ERR_INVALID_ADDRESS` instead of a stopped hart. A buffer is at most 4096 bytes and, while the caller runs with paging off, must lie in DDR. + +| FID | Call | Arguments | Result | +|-----|------|-----------|--------| +| 0x40 | `GET_VERSION` | - | `a1` = 1 | +| 0x41 | `GET_SERIAL` | `a0` buffer, `a1` length >= 16 | the 16-byte device serial number | +| 0x42 | `GET_KEY` | `a0` purpose, `a1` buffer, `a2` length == 32 | a PUF-derived key; each purpose is served once per boot, a second call returns `SBI_ERR_DENIED` | +| 0x43 | `DIGEST_CHECK` | `a0` options word | starts the System Controller digest check | +| 0x44 | `IAP_AUTHENTICATE` | `a0` SPI directory index | starts IAP image authentication | +| 0x45 | `IAP_VERIFY` | `a0` SPI directory index | starts IAP verify | +| 0x46 | `IAP_PROGRAM` | `a0` SPI directory index | the device restarts on success; a return is the failure status in `a1` | +| 0x47 | `AUTO_UPDATE` | - | the device restarts on success; a return is the failure status in `a1` | +| 0x48 | `SC_FLASH_ERASE` | `a0` flash offset, `a1` length == one erase block (64 KB), both block aligned | erases that block of the System Controller's SPI flash; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x49 | `SC_FLASH_WRITE` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | programs the bytes; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4A | `SC_FLASH_READ` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | reads the bytes (any offset, the directory included); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4B | `SC_FLASH_SET_IMAGE` | `a0` directory index 1 (upgrade) or 2 (blank), `a1` image offset | rewrites the SPI directory block with that entry changed (entry 1 also zeroes entry 2); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4F | `SC_STATUS` | - | `SBI_ERR_ALREADY_STARTED` while the started service runs, else `a1` = 16-bit status with mailbox word 1 (the digest-check `DIGESTERR` bits) in the upper half | + +`GET_KEY` purposes name a fixed label; the caller never supplies a challenge, so the OS cannot ask the PUF for wolfBoot's own key-encryption key. Purpose 1 is the MRAM log key (`SHA-384("wolfBoot-mram-log-v1" || PUF(SHA-384("wolfBoot-mram-log-v1")[0..15]))[0..31]`); purpose 2 reproduces the earlier user-space `varlog` derivation. The key is device-bound: the same whether the golden or an operational image booted. + +The System Controller services (0x43-0x4F) are served only in a build with `MPFS_SCB_SMODE_DENY=1`, where the OS cannot reach the mailbox itself; in an unfenced build they return `SBI_ERR_DENIED` and the OS uses its own mailbox drivers, which the fence would otherwise race. The long-running ones are started inside the ecall and polled through `SC_STATUS`, because an ecall holds its hart in M-mode with interrupts off. The mailbox is one resource for all harts: a call that finds it taken returns `SBI_ERR_FAILED`. + +The fence also covers the System Controller's QSPI flash controller, so a fenced OS cannot stage a bitstream the way the mainline `mpfs-auto-update` driver does (through `mtd` on that controller). `MPFS_SBI_SC_FLASH=1` adds the flash calls (0x48-0x4B) for that build: the OS erases and writes the image region directly, reads back what it wants verified, and points the SPI directory's upgrade entry at the image with `SC_FLASH_SET_IMAGE` before `IAP_AUTHENTICATE` / `IAP_PROGRAM` or `AUTO_UPDATE`. The first erase block (the directory and the start of the design-info area) is never erased or written through `SC_FLASH_ERASE` / `SC_FLASH_WRITE`; `SET_IMAGE` reads it, changes the one entry (the golden entry 0 stays what it was) and writes it back, the same read-erase-write the OS driver performs. Each erase call takes one block and each transfer at most 4 KB, so no ecall holds its hart for more than the erase time of one block; the OS side loops. The flash layout the mainline driver uses (directory in the first 1 KB, design info at 1 KB, bitstream slots from 1 MB + 1 KB) is the OS's business; wolfBoot only protects the directory block. + +The Linux side is the `wolfboot-sbi` module in `tools/linux/wolfboot-sbi/`: `/dev/wolfboot` (root only) with one ioctl per call, see `wolfboot_sbi.h`. It probes the extension and `GET_VERSION`, so it unloads cleanly under the HSS, and it never touches the mailbox registers, so the mainline `mpfs-sys-controller`, `mpfs-auto-update` and `mpfs-rng` drivers keep working in an unfenced build. A caller of `IAP_PROGRAM` or `AUTO_UPDATE` syncs its filesystems first: the device restarts inside the call. + +Build: `MPFS_SBI_VENDOR=1` on the M-mode target (`RISCV_MMODE=1`); the key derivation is SHA-384 whatever the image hash. diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 2809f8e545..9f4249aed0 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -45,9 +45,13 @@ #include "hal.h" #include "gpt.h" #include "fdt.h" -#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) +#if defined(SNVM_KEYSTORE_PROVISION) || defined(SNVM_ENCKEY_PROVISION) || \ + defined(MPFS_SBI_VENDOR) #include "mpfs250_snvm.h" #endif +#if defined(MPFS_SBI_VENDOR) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#include "riscv_sbi.h" +#endif #ifdef MPFS_ATHENA #include @@ -1114,16 +1118,24 @@ static int mpfs_scb_mailbox_busy(void) return (SCBCTRL_REG(SERVICES_SR_OFFSET) & SERVICES_SR_BUSY_MASK); } -/* System Controller service, polling mode, mailbox word offset 0. Returns the - * 16-bit service status (0 = success) or a negative transport error. */ -static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, +/* Once the OS runs, the mailbox is driven from ecalls, where a console + * write could block on a UART the OS has reconfigured. */ +static int mpfs_scb_quiet; + +/* Raise a System Controller service. The request bytes go to mailbox word + * 0; index is the SERVICES_CR mailbox-offset field, which carries the SPI + * directory index for the IAP services. Returns 0 once the request is + * accepted, or a negative transport error. */ +static int mpfs_scb_start(uint8_t opcode, uint32_t index, const uint8_t *req, uint32_t req_len) { uint32_t cmd, words, rem, i, v; int timeout; if (mpfs_scb_mailbox_busy()) { - wolfBoot_printf("SCB mailbox busy\n"); + if (!mpfs_scb_quiet) { + wolfBoot_printf("SCB mailbox busy\n"); + } return -2; } @@ -1149,9 +1161,10 @@ static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, /* Ensure mailbox writes land before the request is raised. */ __asm__ volatile("fence w,w" ::: "memory"); - /* Command: opcode in [22:16] (SERVICES_CR_COMMAND_SHIFT), REQ in bit 0; - * the mailbox word-offset field is left 0. */ + /* Command: opcode in [22:16] (SERVICES_CR_COMMAND_SHIFT), the mailbox + * offset / SPI index in [15:7], REQ in bit 0. */ cmd = (((uint32_t)(opcode & 0x7Fu)) << SERVICES_CR_COMMAND_SHIFT) | + ((index & 0x1FFu) << SERVICES_CR_MBOX_SHIFT) | SERVICES_CR_REQ_MASK; SCBCTRL_REG(SERVICES_CR_OFFSET) = cmd; @@ -1162,23 +1175,51 @@ static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB request timeout\n"); + if (!mpfs_scb_quiet) { + wolfBoot_printf("SCB request timeout\n"); + } return -3; } + return 0; +} + +/* The 16-bit status of the service in progress. */ +static int mpfs_scb_status(void) +{ + return (int)((SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) + & 0xFFFFu); +} + +/* Wait for the service to complete. Returns its status (0 = success) or a + * negative transport error. Uses the larger completion bound: nonce/PUF + * take much longer than serial/sNVM-read. */ +static int mpfs_scb_wait(void) +{ + int timeout = MPFS_SCB_BUSY_TIMEOUT; - /* Wait for busy bit to clear (service complete). Uses the larger - * completion bound: nonce/PUF take much longer than serial/sNVM-read. */ - timeout = MPFS_SCB_BUSY_TIMEOUT; while (mpfs_scb_mailbox_busy() && (timeout > 0)) { timeout--; } if (timeout == 0) { - wolfBoot_printf("SCB busy timeout\n"); + if (!mpfs_scb_quiet) { + wolfBoot_printf("SCB busy timeout\n"); + } return -4; } + return mpfs_scb_status(); +} - return (int)((SCBCTRL_REG(SERVICES_SR_OFFSET) >> SERVICES_SR_STATUS_SHIFT) - & 0xFFFFu); +/* System Controller service, polling mode, mailbox word offset 0. Returns the + * 16-bit service status (0 = success) or a negative transport error. */ +static int mpfs_scb_request(uint8_t opcode, const uint8_t *req, + uint32_t req_len) +{ + int ret = mpfs_scb_start(opcode, 0, req, req_len); + + if (ret != 0) { + return ret; + } + return mpfs_scb_wait(); } /* Read len response bytes from the mailbox at byte offset off (mb word 0). */ @@ -1190,6 +1231,404 @@ static void mpfs_scb_read(uint8_t *out, uint32_t off, uint32_t len) } } +#if defined(MPFS_SBI_VENDOR) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +/* SBI vendor extension (include/riscv_sbi.h) served to the OS. Everything + * here runs inside an ecall on one hart with interrupts off: the System + * Controller services that take seconds are started here and polled by the + * OS through SC_STATUS, the short ones complete in place. */ + +/* The mailbox is one resource for all harts. Taken for the life of a + * started service, released when SC_STATUS reports it complete. */ +static volatile uint32_t sbi_scb_lock; +/* Set while a started (asynchronous) service owns the lock, so SC_STATUS + * releases only that one and never a lock a synchronous call is holding. */ +static volatile uint32_t sbi_scb_started; +/* GET_KEY purposes already handed out this boot, one bit each. */ +static uint32_t sbi_key_served; + +static int sbi_scb_trylock(void) +{ + return (__atomic_exchange_n(&sbi_scb_lock, 1U, __ATOMIC_ACQ_REL) == 0U); +} + +static void sbi_scb_unlock(void) +{ + __atomic_store_n(&sbi_scb_lock, 0U, __ATOMIC_RELEASE); +} + +/* The challenge seed and hash label behind each purpose. The OS names a + * purpose, never a challenge, so the PUF cannot be asked for the KEK. */ +static const struct { + unsigned long purpose; + const char *seed; + const char *label; +} sbi_key_purposes[] = { + { SBI_WOLF_KEY_MRAM_LOG, "wolfBoot-mram-log-v1", "wolfBoot-mram-log-v1" }, + { SBI_WOLF_KEY_VARLOG, "varlog", "wolfLUKS-key-v1:varlog" }, +}; + +static long sbi_vendor_get_key(unsigned long purpose, unsigned long buf, + unsigned long len) +{ + uint8_t key[SBI_WOLF_KEY_LEN]; + unsigned int i; + int ret; + + for (i = 0; i < sizeof(sbi_key_purposes) / sizeof(sbi_key_purposes[0]); + i++) { + if (sbi_key_purposes[i].purpose == purpose) { + break; + } + } + if ((i == sizeof(sbi_key_purposes) / sizeof(sbi_key_purposes[0])) || + (len != SBI_WOLF_KEY_LEN)) { + return SBI_ERR_INVALID_PARAM; + } + if ((sbi_key_served & (1U << i)) != 0U) { + return SBI_ERR_DENIED; + } + if (!sbi_smode_range_ok(buf, len)) { + return SBI_ERR_INVALID_ADDRESS; + } + if (!sbi_scb_trylock()) { + return SBI_ERR_FAILED; + } + ret = mpfs_puf_purpose_key(sbi_key_purposes[i].seed, + sbi_key_purposes[i].label, key); + sbi_scb_unlock(); + if (ret != 0) { + return SBI_ERR_FAILED; + } + ret = sbi_copy_to_smode(buf, key, sizeof(key)); + memset(key, 0, sizeof(key)); + if (ret != 0) { + /* the caller never saw the key, so its one serving still stands */ + return SBI_ERR_INVALID_ADDRESS; + } + sbi_key_served |= (1U << i); + return SBI_SUCCESS; +} + +/* Services that reach the System Controller are served only when the OS + * cannot drive the mailbox itself: unfenced, its own driver would race + * these requests. */ +#ifdef MPFS_SCB_SMODE_DENY +#ifdef MPFS_SBI_SC_FLASH +/* The System Controller's SPI flash for the OS. The fence that keeps the + * OS off the mailbox also covers the flash controller (0x37020100), so under + * it this is the OS's only way to stage a bitstream. The first erase block + * holds the SPI directory the System Controller boots from: its golden entry + * is never written, and the block changes only through SET_IMAGE, which + * edits the upgrade or blank entry in a copy and writes the rest back as it + * was. Every other offset is the OS's. An erase call takes one block and a + * write or read at most SBI_SMODE_BUF_MAX bytes, so an ecall stays well + * under a second with the hart's interrupts masked. The mailbox lock is + * held throughout: the System Controller reads this flash for its own + * services. */ +int qspi_init(void); + +#define SBI_SC_FLASH_UPGRADE_INDEX 1UL +#define SBI_SC_FLASH_BLANK_INDEX 2UL + +static int sbi_sc_flash_ready; +static uint8_t sbi_sc_flash_buf[SBI_SMODE_BUF_MAX]; +static uint32_t sbi_sc_flash_block[FLASH_SECTOR_SIZE / sizeof(uint32_t)]; + +static long sbi_sc_flash_take(void) +{ + if (!sbi_scb_trylock()) { + return SBI_ERR_FAILED; + } + if (!sbi_sc_flash_ready) { + if (qspi_init() != 0) { + sbi_scb_unlock(); + return SBI_ERR_FAILED; + } + sbi_sc_flash_ready = 1; + } + return SBI_SUCCESS; +} + +/* Inside the flash and clear of the directory block. */ +static int sbi_sc_flash_range_ok(unsigned long off, unsigned long len) +{ + return (len != 0UL) && (off >= (unsigned long)FLASH_SECTOR_SIZE) && + (off <= (unsigned long)FLASH_DEVICE_SIZE) && + (len <= (unsigned long)FLASH_DEVICE_SIZE - off); +} + +static long sbi_vendor_sc_flash_erase(unsigned long off, unsigned long len) +{ + long err; + + if (!sbi_sc_flash_range_ok(off, len) || + (len != (unsigned long)FLASH_SECTOR_SIZE) || + ((off & ((unsigned long)FLASH_SECTOR_SIZE - 1UL)) != 0UL)) { + return SBI_ERR_INVALID_PARAM; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + if (ext_flash_erase(off, (int)len) != (int)len) { + err = SBI_ERR_FAILED; + } + sbi_scb_unlock(); + return err; +} + +static long sbi_vendor_sc_flash_write(unsigned long off, unsigned long buf, + unsigned long len) +{ + long err; + + if (!sbi_sc_flash_range_ok(off, len) || (len > SBI_SMODE_BUF_MAX)) { + return SBI_ERR_INVALID_PARAM; + } + if (!sbi_smode_range_ok(buf, len)) { + return SBI_ERR_INVALID_ADDRESS; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + /* the bounce buffer is shared between harts: copy under the lock */ + if (sbi_copy_from_smode(sbi_sc_flash_buf, buf, len) != 0) { + err = SBI_ERR_INVALID_ADDRESS; + } + else if (ext_flash_write(off, sbi_sc_flash_buf, (int)len) != (int)len) { + err = SBI_ERR_FAILED; + } + sbi_scb_unlock(); + return err; +} + +static long sbi_vendor_sc_flash_read(unsigned long off, unsigned long buf, + unsigned long len) +{ + long err; + + if ((len == 0UL) || (len > SBI_SMODE_BUF_MAX) || + (off > (unsigned long)FLASH_DEVICE_SIZE) || + (len > (unsigned long)FLASH_DEVICE_SIZE - off)) { + return SBI_ERR_INVALID_PARAM; + } + if (!sbi_smode_range_ok(buf, len)) { + return SBI_ERR_INVALID_ADDRESS; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + if (ext_flash_read(off, sbi_sc_flash_buf, (int)len) != (int)len) { + err = SBI_ERR_FAILED; + } + else if (sbi_copy_to_smode(buf, sbi_sc_flash_buf, len) != 0) { + err = SBI_ERR_INVALID_ADDRESS; + } + sbi_scb_unlock(); + return err; +} + +/* Point directory entry 1 (upgrade) or 2 (blank) at an image offset. + * Setting the upgrade entry also zeroes the blank one, as the OS driver + * does, so a single-image System Controller does not object. Nothing is + * written when the entries already read that way. */ +static long sbi_vendor_sc_flash_set_image(unsigned long index, + unsigned long address) +{ + uint8_t *block = (uint8_t *)sbi_sc_flash_block; + long err; + + if (((index != SBI_SC_FLASH_UPGRADE_INDEX) && + (index != SBI_SC_FLASH_BLANK_INDEX)) || + (address > (unsigned long)FLASH_DEVICE_SIZE)) { + return SBI_ERR_INVALID_PARAM; + } + err = sbi_sc_flash_take(); + if (err != SBI_SUCCESS) { + return err; + } + if (ext_flash_read(0, block, FLASH_SECTOR_SIZE) != FLASH_SECTOR_SIZE) { + sbi_scb_unlock(); + return SBI_ERR_FAILED; + } + if ((sbi_sc_flash_block[index] == (uint32_t)address) && + ((index != SBI_SC_FLASH_UPGRADE_INDEX) || + (sbi_sc_flash_block[SBI_SC_FLASH_BLANK_INDEX] == 0U))) { + sbi_scb_unlock(); + return SBI_SUCCESS; + } + sbi_sc_flash_block[index] = (uint32_t)address; + if (index == SBI_SC_FLASH_UPGRADE_INDEX) { + sbi_sc_flash_block[SBI_SC_FLASH_BLANK_INDEX] = 0U; + } + if ((ext_flash_erase(0, FLASH_SECTOR_SIZE) != FLASH_SECTOR_SIZE) || + (ext_flash_write(0, block, FLASH_SECTOR_SIZE) != FLASH_SECTOR_SIZE)) { + err = SBI_ERR_FAILED; + } + sbi_scb_unlock(); + return err; +} +#endif /* MPFS_SBI_SC_FLASH */ + +static long sbi_vendor_sc_start(uint8_t opcode, uint32_t index, + const uint8_t *req, uint32_t req_len) +{ + if (!sbi_scb_trylock()) { + return SBI_ERR_FAILED; + } + if (mpfs_scb_start(opcode, index, req, req_len) != 0) { + sbi_scb_unlock(); + return SBI_ERR_FAILED; + } + __atomic_store_n(&sbi_scb_started, 1U, __ATOMIC_RELEASE); + return SBI_SUCCESS; +} + +/* The IAP program and auto-update services restart the device on success, + * so a return from here is the failure status. */ +static long sbi_vendor_sc_run(uint8_t opcode, uint32_t index, + unsigned long *val) +{ + long err = sbi_vendor_sc_start(opcode, index, NULL, 0); + + if (err != SBI_SUCCESS) { + return err; + } + *val = (unsigned long)mpfs_scb_wait(); + __atomic_store_n(&sbi_scb_started, 0U, __ATOMIC_RELEASE); + sbi_scb_unlock(); + return SBI_ERR_FAILED; +} +#endif + +long sbi_hal_vendor_ecall(unsigned long fid, unsigned long *regs, + unsigned long *val) +{ + unsigned long a0 = regs[SBI_REG_A0]; + unsigned long a1 = regs[SBI_REG_A1]; + unsigned long a2 = regs[SBI_REG_A2]; + long err; + + mpfs_scb_quiet = 1; + switch (fid) { + case SBI_WOLF_FID_GET_VERSION: + *val = SBI_WOLF_VERSION; + err = SBI_SUCCESS; + break; + case SBI_MCHP_FID_HSS_REBOOT: + SYSREG_MSS_RESET_CR = 0xDEAD; + while (1) { } + case SBI_WOLF_FID_GET_SERIAL: { + uint8_t serial[DEVICE_SERIAL_NUMBER_SIZE]; + int ret; + + if (a1 < sizeof(serial)) { + err = SBI_ERR_INVALID_PARAM; + break; + } + if (!sbi_smode_range_ok(a0, sizeof(serial))) { + err = SBI_ERR_INVALID_ADDRESS; + break; + } + if (!sbi_scb_trylock()) { + err = SBI_ERR_FAILED; + break; + } + ret = mpfs_read_serial_number(serial); + sbi_scb_unlock(); + if (ret != 0) { + err = SBI_ERR_FAILED; + break; + } + err = (sbi_copy_to_smode(a0, serial, sizeof(serial)) == 0) ? + SBI_SUCCESS : SBI_ERR_INVALID_ADDRESS; + break; + } + case SBI_WOLF_FID_GET_KEY: + err = sbi_vendor_get_key(a0, a1, a2); + break; +#ifdef MPFS_SCB_SMODE_DENY + case SBI_WOLF_FID_DIGEST_CHECK: { + uint8_t req[4]; + + req[0] = (uint8_t)a0; + req[1] = (uint8_t)(a0 >> 8); + req[2] = (uint8_t)(a0 >> 16); + req[3] = (uint8_t)(a0 >> 24); + err = sbi_vendor_sc_start(SYS_SERV_CMD_DIGEST_CHECK, 0, req, + sizeof(req)); + break; + } + case SBI_WOLF_FID_IAP_AUTHENTICATE: + err = sbi_vendor_sc_start(SYS_SERV_CMD_IAP_IMAGE_AUTH, + (uint32_t)a0, NULL, 0); + break; + case SBI_WOLF_FID_IAP_VERIFY: + err = sbi_vendor_sc_start(SYS_SERV_CMD_IAP_VERIFY_IDX, + (uint32_t)a0, NULL, 0); + break; + case SBI_WOLF_FID_IAP_PROGRAM: + err = sbi_vendor_sc_run(SYS_SERV_CMD_IAP_PROGRAM_IDX, (uint32_t)a0, + val); + break; + case SBI_WOLF_FID_AUTO_UPDATE: + err = sbi_vendor_sc_run(SYS_SERV_CMD_AUTO_UPDATE, 0, val); + break; +#ifdef MPFS_SBI_SC_FLASH + case SBI_WOLF_FID_SC_FLASH_ERASE: + err = sbi_vendor_sc_flash_erase(a0, a1); + break; + case SBI_WOLF_FID_SC_FLASH_WRITE: + err = sbi_vendor_sc_flash_write(a0, a1, a2); + break; + case SBI_WOLF_FID_SC_FLASH_READ: + err = sbi_vendor_sc_flash_read(a0, a1, a2); + break; + case SBI_WOLF_FID_SC_FLASH_SET_IMAGE: + err = sbi_vendor_sc_flash_set_image(a0, a1); + break; +#endif + case SBI_WOLF_FID_SC_STATUS: + if (mpfs_scb_mailbox_busy()) { + err = SBI_ERR_ALREADY_STARTED; + break; + } + *val = (unsigned long)mpfs_scb_status() | + ((unsigned long)SCBMBOX_REG(4) << 32); + /* A synchronous service also leaves the mailbox idle between its + * own requests; release only the lock a started service holds. */ + if (__atomic_exchange_n(&sbi_scb_started, 0U, __ATOMIC_ACQ_REL) + != 0U) { + sbi_scb_unlock(); + } + err = SBI_SUCCESS; + break; +#else + case SBI_WOLF_FID_DIGEST_CHECK: + case SBI_WOLF_FID_IAP_AUTHENTICATE: + case SBI_WOLF_FID_IAP_VERIFY: + case SBI_WOLF_FID_IAP_PROGRAM: + case SBI_WOLF_FID_AUTO_UPDATE: + case SBI_WOLF_FID_SC_FLASH_ERASE: + case SBI_WOLF_FID_SC_FLASH_WRITE: + case SBI_WOLF_FID_SC_FLASH_READ: + case SBI_WOLF_FID_SC_FLASH_SET_IMAGE: + case SBI_WOLF_FID_SC_STATUS: + err = SBI_ERR_DENIED; + break; +#endif + default: + err = SBI_ERR_NOT_SUPPORTED; + break; + } + mpfs_scb_quiet = 0; + (void)a2; + return err; +} +#endif /* MPFS_SBI_VENDOR && WOLFBOOT_MMODE_SMODE_BOOT */ + /* Read 16-byte device serial number via SCB system service (opcode 0x00). */ int mpfs_read_serial_number(uint8_t *serial) { @@ -1579,7 +2018,7 @@ static int mpfs_scb_wait_clear(uint32_t reg_offset, uint32_t mask, return (timeout == 0) ? -1 : 0; } -#ifdef EXT_FLASH +#if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) /* ========================================================================== * QSPI Flash Controller Implementation * @@ -2326,7 +2765,7 @@ static int test_ext_flash(void) } #endif /* TEST_EXT_FLASH */ -#else /* !EXT_FLASH */ +#else /* !EXT_FLASH && !MPFS_SBI_SC_FLASH */ /* Stubs for when QSPI is disabled */ void ext_flash_lock(void) diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 5dd9c6869e..379e0bb7cb 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -227,6 +227,7 @@ /* Control Register bits */ #define SERVICES_CR_REQ_MASK 0x01u +#define SERVICES_CR_MBOX_SHIFT 7 /* mailbox word offset, or the SPI index */ #define SERVICES_CR_COMMAND_SHIFT 16 /* Status Register bits */ @@ -242,6 +243,16 @@ #define SYS_SERV_CMD_SNVM_READ 0x18u #define SYS_SERV_CMD_PUF_EMULATION 0x20u #define SYS_SERV_CMD_NONCE 0x21u +/* Bitstream/IAP services. The SPI directory index of 0x22/0x42/0x44 rides + * in the SERVICES_CR mailbox-offset field, not in the mailbox. */ +#define SYS_SERV_CMD_IAP_IMAGE_AUTH 0x22u /* index in CR[14:7] */ +#define SYS_SERV_CMD_BITSTREAM_AUTH 0x23u /* SPI address in mailbox */ +#define SYS_SERV_CMD_IAP_PROGRAM_IDX 0x42u /* restarts the device on success */ +#define SYS_SERV_CMD_IAP_PROGRAM_ADDR 0x43u +#define SYS_SERV_CMD_IAP_VERIFY_IDX 0x44u +#define SYS_SERV_CMD_IAP_VERIFY_ADDR 0x45u +#define SYS_SERV_CMD_AUTO_UPDATE 0x46u /* restarts the device on success */ +#define SYS_SERV_CMD_DIGEST_CHECK 0x47u /* options word in, DIGESTERR at byte 4 */ /* sNVM service parameters (see Microchip system services spec) */ #define MPFS_SNVM_MODULE_MAX 221 /* modules 0..220 */ @@ -417,6 +428,16 @@ typedef struct { /* DTIM bytes hal_init() zeroes at boot (SBI shared block + hart mailboxes). */ #define MPFS_DTIM_BOOT_CLEAR_SIZE 0x200U + +/* Per-hart M-mode trap stacks (src/riscv_sbi.c, src/vector_riscv.S): one + * 4 KB (1 << SBI_MSTACK_SHIFT) stack per hart in the OS-invisible reserved + * DDR (hss-buffer@103fc00000, nomap in the stock dtb); hart h's top is + * SBI_MSTACK_BASE + (h + 1) << SBI_MSTACK_SHIFT. No UL suffix: used from + * assembly. */ +#ifndef SBI_MSTACK_BASE +#define SBI_MSTACK_BASE 0x103FC00000 +#endif +#define SBI_MSTACK_SHIFT 12 /* DTIM pair (count, ~count) just above the cleared block, so it survives an * MSS reset: DDR training restarts by reset since the last accepted training. */ #define MPFS_DTIM_DDR_RESET_CNT_ADDR (0x01000000UL + MPFS_DTIM_BOOT_CLEAR_SIZE) @@ -751,7 +772,7 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #endif #endif /* __ASSEMBLER__ */ -#ifdef EXT_FLASH +#if defined(EXT_FLASH) || defined(MPFS_SBI_SC_FLASH) /* QSPI Flash Controller * * Two CoreQSPI v2 controllers with identical register layouts: diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c index 541522fcd2..40bac699a5 100644 --- a/hal/mpfs250_snvm.c +++ b/hal/mpfs250_snvm.c @@ -285,35 +285,28 @@ int snvm_keystore_provision(void) #endif /* SNVM_KEYSTORE_PROVISION && !WOLFBOOT_NO_SIGN */ -#ifdef SNVM_KEK - -/* Fixed PUF challenge. Any constant works: the same challenge returns the - * same device-unique response across cold boots, anchoring a stable KEK. */ -static const uint8_t snvm_kek_challenge[MPFS_PUF_CHALLENGE_LEN] = { - 0x77, 0x6f, 0x6c, 0x66, 0x42, 0x6f, 0x6f, 0x74, - 0x4b, 0x45, 0x4b, 0x76, 0x31, 0x00, 0x00, 0x00 -}; -/* Domain-separation label hashed with the PUF response. */ -static const char snvm_kek_label[] = "wolfBoot-sNVM-KEK-v1"; +#if defined(SNVM_KEK) || defined(MPFS_SBI_VENDOR) -int mpfs_puf_kek(uint8_t *kek) +/* out = SHA384(label || PUF(challenge))[0..31]: a device-unique key bound to + * the label and the challenge. The label is hashed without its terminator. */ +int mpfs_puf_derive(const char *label, const uint8_t *challenge, uint8_t *out) { uint8_t resp[MPFS_PUF_RESPONSE_LEN]; uint8_t digest[WC_SHA384_DIGEST_SIZE]; wc_Sha384 sha; int ret; - if (kek == NULL) { + if ((label == NULL) || (challenge == NULL) || (out == NULL)) { return -1; } - ret = mpfs_puf_emulation(snvm_kek_challenge, 0, resp); + ret = mpfs_puf_emulation(challenge, 0, resp); if (ret != 0) { return -1; } ret = wc_InitSha384(&sha); if (ret == 0) { - ret = wc_Sha384Update(&sha, (const byte *)snvm_kek_label, - (word32)(sizeof(snvm_kek_label) - 1)); + ret = wc_Sha384Update(&sha, (const byte *)label, + (word32)strlen(label)); if (ret == 0) { ret = wc_Sha384Update(&sha, resp, (word32)sizeof(resp)); } @@ -324,15 +317,57 @@ int mpfs_puf_kek(uint8_t *kek) /* the context buffered the whole sub-block message, PUF response included */ wc_ForceZero(&sha, sizeof(sha)); } - /* KEK = first 256 bits of SHA-384(label || PUF response). */ if (ret == 0) { - memcpy(kek, digest, SNVM_KEK_LEN); + memcpy(out, digest, MPFS_PUF_DERIVED_LEN); } wc_ForceZero(resp, sizeof(resp)); wc_ForceZero(digest, sizeof(digest)); return (ret == 0) ? 0 : -1; } +int mpfs_puf_purpose_key(const char *seed, const char *label, uint8_t *out) +{ + uint8_t digest[WC_SHA384_DIGEST_SIZE]; + uint8_t challenge[MPFS_PUF_CHALLENGE_LEN]; + wc_Sha384 sha; + int ret; + + if ((seed == NULL) || (label == NULL) || (out == NULL)) { + return -1; + } + ret = wc_InitSha384(&sha); + if (ret == 0) { + ret = wc_Sha384Update(&sha, (const byte *)seed, (word32)strlen(seed)); + if (ret == 0) { + ret = wc_Sha384Final(&sha, digest); + } + wc_Sha384Free(&sha); + } + if (ret != 0) { + return -1; + } + memcpy(challenge, digest, sizeof(challenge)); + return mpfs_puf_derive(label, challenge, out); +} +#endif /* SNVM_KEK || MPFS_SBI_VENDOR */ + +#ifdef SNVM_KEK + +/* Fixed PUF challenge. Any constant works: the same challenge returns the + * same device-unique response across cold boots, anchoring a stable KEK. */ +static const uint8_t snvm_kek_challenge[MPFS_PUF_CHALLENGE_LEN] = { + 0x77, 0x6f, 0x6c, 0x66, 0x42, 0x6f, 0x6f, 0x74, + 0x4b, 0x45, 0x4b, 0x76, 0x31, 0x00, 0x00, 0x00 +}; +/* Domain-separation label hashed with the PUF response. */ +static const char snvm_kek_label[] = "wolfBoot-sNVM-KEK-v1"; + +int mpfs_puf_kek(uint8_t *kek) +{ + return mpfs_puf_derive(snvm_kek_label, snvm_kek_challenge, kek); +} + + int snvm_kek_wrap(const uint8_t *key, uint32_t keysz, uint8_t *out, uint32_t outsz) { diff --git a/hal/mpfs250_snvm.h b/hal/mpfs250_snvm.h index 3e5bb2892e..7b173801fa 100644 --- a/hal/mpfs250_snvm.h +++ b/hal/mpfs250_snvm.h @@ -138,6 +138,15 @@ int snvm_enckey_provision(void); #endif /* SNVM_KEK */ +#if defined(SNVM_KEK) || defined(MPFS_SBI_VENDOR) +#define MPFS_PUF_DERIVED_LEN 32 +/* out[32] = SHA384(label || PUF(challenge[16]))[0..31]; the primitive behind + * the KEK and the keys served to the OS over SBI. */ +int mpfs_puf_derive(const char *label, const uint8_t *challenge, uint8_t *out); +/* The same with challenge = SHA384(seed)[0..15]. */ +int mpfs_puf_purpose_key(const char *seed, const char *label, uint8_t *out); +#endif + #if defined(SNVM_ENCKEY_PROVISION) && !defined(SNVM_KEK) #error "SNVM_ENCKEY_PROVISION needs SNVM_KEK (the key is wrapped with the PUF KEK)" #endif diff --git a/include/riscv_sbi.h b/include/riscv_sbi.h new file mode 100644 index 0000000000..59c22683f2 --- /dev/null +++ b/include/riscv_sbi.h @@ -0,0 +1,97 @@ +/* riscv_sbi.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* Interface between the M-mode SBI runtime (src/riscv_sbi.c) and a platform + * HAL that serves the vendor extension. */ + +#ifndef RISCV_SBI_H +#define RISCV_SBI_H + +#include + +#define SBI_SUCCESS 0L +#define SBI_ERR_FAILED (-1L) +#define SBI_ERR_NOT_SUPPORTED (-2L) +#define SBI_ERR_INVALID_PARAM (-3L) +#define SBI_ERR_DENIED (-4L) +#define SBI_ERR_INVALID_ADDRESS (-5L) +#define SBI_ERR_ALREADY_AVAIL (-6L) +#define SBI_ERR_ALREADY_STARTED (-7L) + +/* Register-frame indices of the argument registers (regs[N] == xN). */ +#define SBI_REG_A0 10 +#define SBI_REG_A1 11 +#define SBI_REG_A2 12 + +/* Vendor extension: SBI_EXT_VENDOR_START | Microchip mvendorid 0x029, the + * EID the Hart Software Services use, so an OS probes one number for both + * firmwares and tells them apart with SBI_WOLF_FID_GET_VERSION. */ +#define SBI_EXT_VENDOR_MCHP 0x09000029UL + +/* HSS function IDs on that extension. Only HSS_REBOOT is served here (as a + * cold reset); the IHC, remoteproc, crypto and IPC calls are not. */ +#define SBI_MCHP_FID_HSS_REBOOT 0x10UL + +/* wolfBoot function IDs. a0..a2 carry the arguments, a0 returns the SBI + * error, a1 the value. S-mode buffers are passed as addresses in the + * caller's address space. */ +#define SBI_WOLF_FID_GET_VERSION 0x40UL /* -> a1 = SBI_WOLF_VERSION */ +#define SBI_WOLF_FID_GET_SERIAL 0x41UL /* (buf, len>=16) */ +#define SBI_WOLF_FID_GET_KEY 0x42UL /* (purpose, buf, len==32), once per boot */ +#define SBI_WOLF_FID_DIGEST_CHECK 0x43UL /* (options) start, poll SC_STATUS */ +#define SBI_WOLF_FID_IAP_AUTHENTICATE 0x44UL /* (spi index) start, poll SC_STATUS */ +#define SBI_WOLF_FID_IAP_VERIFY 0x45UL /* (spi index) start, poll SC_STATUS */ +#define SBI_WOLF_FID_IAP_PROGRAM 0x46UL /* (spi index) the device restarts on success */ +#define SBI_WOLF_FID_AUTO_UPDATE 0x47UL /* () the device restarts on success */ +/* System Controller SPI flash, fenced builds with MPFS_SBI_SC_FLASH only: + * offsets are flash offsets, the first erase block (the SPI directory) is + * reachable only through SET_IMAGE. */ +#define SBI_WOLF_FID_SC_FLASH_ERASE 0x48UL /* (offset, len == one erase block) */ +#define SBI_WOLF_FID_SC_FLASH_WRITE 0x49UL /* (offset, buf, len <= SBI_SMODE_BUF_MAX) */ +#define SBI_WOLF_FID_SC_FLASH_READ 0x4AUL /* (offset, buf, len <= SBI_SMODE_BUF_MAX) */ +#define SBI_WOLF_FID_SC_FLASH_SET_IMAGE 0x4BUL /* (directory index 1|2, image offset) */ +#define SBI_WOLF_FID_SC_STATUS 0x4FUL /* -> a1 = status | (mailbox word 1 << 32) */ + +#define SBI_WOLF_VERSION 1UL + +/* GET_KEY purposes: each names a fixed label, so the caller never chooses + * what the PUF is asked. */ +#define SBI_WOLF_KEY_MRAM_LOG 1UL /* "wolfBoot-mram-log-v1" */ +#define SBI_WOLF_KEY_VARLOG 2UL /* "wolfLUKS-key-v1:varlog" (the earlier user-space derivation) */ +#define SBI_WOLF_KEY_LEN 32UL + +/* Buffers an S-mode caller hands to the vendor calls are bounded. */ +#define SBI_SMODE_BUF_MAX 4096UL + +/* Platform hook serving SBI_EXT_VENDOR_MCHP. regs is the trap frame + * (SBI_REG_A0..A2 in, written back by the caller), *val the a1 result. + * Weak default: SBI_ERR_NOT_SUPPORTED. */ +long sbi_hal_vendor_ecall(unsigned long fid, unsigned long *regs, + unsigned long *val); + +/* Copies between an S-mode address (translated like the trapped context) + * and M-mode memory. 0 on success, -1 when the access faulted or the range + * is not acceptable (sbi_smode_range_ok). */ +int sbi_smode_range_ok(unsigned long addr, unsigned long len); +int sbi_copy_from_smode(uint8_t *dst, unsigned long src, unsigned long len); +int sbi_copy_to_smode(unsigned long dst, const uint8_t *src, unsigned long len); + +#endif /* RISCV_SBI_H */ diff --git a/options.mk b/options.mk index b7d999b4e9..4ed9d19dbb 100644 --- a/options.mk +++ b/options.mk @@ -129,6 +129,36 @@ ifeq ($(MPFS_SCB_SMODE_DENY),1) -DWOLFBOOT_PMP_DENY_SIZE=0x1000UL endif +# PolarFire SoC: the SBI vendor extension (EID 0x09000029, the HSS one) for +# the OS: firmware version, device serial, PUF-derived keys by purpose, and +# the System Controller digest/IAP/auto-update services behind the mailbox +# fence. M-mode only; the key derivation is SHA-384 whatever the image hash. +ifeq ($(MPFS_SBI_VENDOR),1) + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifneq ($(RISCV_MMODE),1) + $(error MPFS_SBI_VENDOR=1 needs the M-mode target (RISCV_MMODE=1)) + endif + endif + CFLAGS+=-D"MPFS_SBI_VENDOR" + ifneq ($(HASH),SHA384) + AUX_HASH_ALGOS+=sha384 + endif +endif + +# PolarFire SoC: System Controller SPI flash erase/write/read for the OS over +# the vendor extension. Only meaningful behind the mailbox fence, which also +# fences the flash controller; compiles the SC CoreQSPI driver into a build +# that boots from elsewhere (EXT_FLASH=0). +ifeq ($(MPFS_SBI_SC_FLASH),1) + ifneq ($(MPFS_SBI_VENDOR)$(MPFS_SCB_SMODE_DENY),11) + $(error MPFS_SBI_SC_FLASH=1 requires MPFS_SBI_VENDOR=1 and MPFS_SCB_SMODE_DENY=1) + endif + ifeq ($(EXT_FLASH),1) + $(error MPFS_SBI_SC_FLASH=1 is for a build that does not boot from the SC flash (EXT_FLASH=0)) + endif + CFLAGS+=-D"MPFS_SBI_SC_FLASH" -D"MPFS_SC_SPI" +endif + # Field recovery: when both operational slots are given up on and the golden # slot is about to boot, zero the sNVM page holding the image key and the head # of both operational slots (hal_disk_failover). The golden image must be a diff --git a/src/riscv_sbi.c b/src/riscv_sbi.c index 6e9be57925..73e02b66b6 100644 --- a/src/riscv_sbi.c +++ b/src/riscv_sbi.c @@ -41,6 +41,7 @@ #ifdef TARGET_mpfs250 #include "hal/mpfs250.h" +#include "riscv_sbi.h" #endif /* ---- platform glue (MPFS250) ------------------------------------------- */ @@ -80,13 +81,7 @@ #define MIP_SSIP (1UL << IRQ_S_SOFT) /* bit 1 */ #define MIE_MTIP (1UL << IRQ_M_TIMER) /* bit 7 */ -/* ---- SBI constants ------------------------------------------------------ */ -#define SBI_SUCCESS 0L -#define SBI_ERR_FAILED (-1L) -#define SBI_ERR_NOT_SUPPORTED (-2L) -#define SBI_ERR_INVALID_PARAM (-3L) -#define SBI_ERR_ALREADY_AVAIL (-6L) - +/* ---- SBI constants (error codes in riscv_sbi.h) ------------------------- */ /* Extension IDs (EIDs) */ #define SBI_EXT_0_1_SET_TIMER 0x00 #define SBI_EXT_0_1_CONSOLE_PUTCHAR 0x01 @@ -230,7 +225,16 @@ void sbi_hart_mark_started(unsigned long hartid) #ifndef SBI_MSTACK_BASE #define SBI_MSTACK_BASE 0x103FC00000UL #endif -#define SBI_MSTACK_SIZE 4096UL +#ifndef SBI_MSTACK_SHIFT +#define SBI_MSTACK_SHIFT 12 +#endif +#define SBI_MSTACK_SIZE (1UL << SBI_MSTACK_SHIFT) + +/* Remote-fence completion wait, in CLINT ticks (1 MHz on PolarFire). */ +#ifndef RTC_CLOCK_FREQ +#define RTC_CLOCK_FREQ 1000000UL +#endif +#define SBI_IPI_WAIT_TICKS ((uint64_t)RTC_CLOCK_FREQ) /* Arm this hart's M-mode trap stack; call just before entering S-mode. */ void sbi_mscratch_init(unsigned long hartid) @@ -241,19 +245,148 @@ void sbi_mscratch_init(unsigned long hartid) } } -/* Copy bytes from an S-mode pointer (virtual once paging is on) into an - * M-mode buffer using the MPRV trick: with mstatus.MPRV set and MPP=S - * (true inside a trap taken from S-mode), M-mode loads are translated - * exactly like S-mode accesses. */ -static void sbi_copy_from_smode(uint8_t *dst, unsigned long src, - unsigned long len) +/* Accesses to the trapped context's memory use the MPRV trick: with + * mstatus.MPRV set and MPP=S (true inside a trap taken from S-mode), M-mode + * loads and stores are translated exactly like S-mode accesses. A bad + * address then faults in M-mode, so for the byte below mtvec is pointed at + * sbi_mprv_trap, which records the fault for this hart, steps over the + * access and returns; the default vector would treat the fault as fatal and + * park the hart. */ +volatile unsigned long sbi_mprv_fault[MPFS_NUM_HARTS]; + +__asm__( + ".pushsection .text\n" + ".align 2\n" + ".type sbi_mprv_trap, @function\n" + "sbi_mprv_trap:\n" + " csrr t0, mepc\n" + " addi t0, t0, 4\n" + " csrw mepc, t0\n" + " li t0, 0x20000\n" + " csrc mstatus, t0\n" + " la t0, sbi_mprv_fault\n" + " csrr t1, mhartid\n" + " slli t1, t1, 3\n" + " add t0, t0, t1\n" + " li t1, 1\n" + " sd t1, 0(t0)\n" + " mret\n" + ".size sbi_mprv_trap, .-sbi_mprv_trap\n" + ".popsection\n" +); + +static unsigned long sbi_hartid(void) +{ + unsigned long id; + __asm__ volatile("csrr %0, mhartid" : "=r"(id)); + return id; +} + +static int sbi_mprv_lb(unsigned long a, uint8_t *v) +{ + unsigned long mtvec_save, mstatus_save, tmp, val; + unsigned long hart = sbi_hartid(); + + sbi_mprv_fault[hart] = 0; + __asm__ volatile( + "la %[tmp], sbi_mprv_trap\n" + "csrrw %[mtvec], mtvec, %[tmp]\n" + "csrrs %[mstatus], mstatus, %[mprv]\n" + ".option push\n" + ".option norvc\n" + "lbu %[val], 0(%[addr])\n" + ".option pop\n" + "csrw mstatus, %[mstatus]\n" + "csrw mtvec, %[mtvec]\n" + : [mtvec] "=&r"(mtvec_save), [mstatus] "=&r"(mstatus_save), + [tmp] "=&r"(tmp), [val] "=&r"(val) + : [addr] "r"(a), [mprv] "r"(MSTATUS_MPRV_BIT) + : "memory", "t0", "t1"); + *v = (uint8_t)val; + return (sbi_mprv_fault[hart] != 0UL) ? -1 : 0; +} + +static int sbi_mprv_sb(unsigned long a, uint8_t v) +{ + unsigned long mtvec_save, mstatus_save, tmp; + unsigned long hart = sbi_hartid(); + + sbi_mprv_fault[hart] = 0; + __asm__ volatile( + "la %[tmp], sbi_mprv_trap\n" + "csrrw %[mtvec], mtvec, %[tmp]\n" + "csrrs %[mstatus], mstatus, %[mprv]\n" + ".option push\n" + ".option norvc\n" + "sb %[val], 0(%[addr])\n" + ".option pop\n" + "csrw mstatus, %[mstatus]\n" + "csrw mtvec, %[mtvec]\n" + : [mtvec] "=&r"(mtvec_save), [mstatus] "=&r"(mstatus_save), + [tmp] "=&r"(tmp) + : [addr] "r"(a), [val] "r"((unsigned long)v), + [mprv] "r"(MSTATUS_MPRV_BIT) + : "memory", "t0", "t1"); + return (sbi_mprv_fault[hart] != 0UL) ? -1 : 0; +} + +/* A caller's buffer must be bounded and, with paging off (early boot), + * must be DDR: an M-mode access to a device window would not fault the way + * a bad translation does. */ +int sbi_smode_range_ok(unsigned long addr, unsigned long len) +{ + unsigned long satp; + + if ((len == 0UL) || (len > SBI_SMODE_BUF_MAX) || (addr + len < addr)) { + return 0; + } + __asm__ volatile("csrr %0, satp" : "=r"(satp)); + if ((satp >> 60) == 0UL) { + if ((addr >= DDR_BASE_CACHED) && + (addr + len <= DDR_BASE_CACHED + 0x40000000UL)) { + return 1; + } + if ((addr >= 0x1000000000UL) && + (addr + len <= 0x1000000000UL + DDR_SIZE)) { + return 1; + } + return 0; + } + return 1; +} + +int sbi_copy_from_smode(uint8_t *dst, unsigned long src, unsigned long len) { unsigned long i; - csr_set_bits(mstatus, MSTATUS_MPRV_BIT); + for (i = 0; i < len; i++) { - dst[i] = ((const volatile uint8_t *)src)[i]; + if (sbi_mprv_lb(src + i, &dst[i]) != 0) { + return -1; + } } - csr_clr_bits(mstatus, MSTATUS_MPRV_BIT); + return 0; +} + +int sbi_copy_to_smode(unsigned long dst, const uint8_t *src, unsigned long len) +{ + unsigned long i; + + for (i = 0; i < len; i++) { + if (sbi_mprv_sb(dst + i, src[i]) != 0) { + return -1; + } + } + return 0; +} + +/* Platform hook for the vendor extension; weak default declines. */ +long __attribute__((weak)) sbi_hal_vendor_ecall(unsigned long fid, + unsigned long *regs, unsigned long *val) +{ + (void)fid; + (void)regs; + (void)val; + return SBI_ERR_NOT_SUPPORTED; } #define MSTATUS_MXR_BIT (1UL << 19) @@ -604,7 +737,7 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, { unsigned long i; unsigned long h; - uint32_t spin; + uint64_t deadline; long err = SBI_SUCCESS; /* SBI v0.2: hart_mask_base == -1 selects all harts (see sbi_post_ipi). */ if (base == (unsigned long)-1) { @@ -622,17 +755,19 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, } /* A target can be inside a console write or another ecall for tens * of milliseconds; a fence that gives up early leaves the OS with - * stale TLBs on that hart, so wait well past that (about 1 s). - * M-mode interrupts are masked in here, so service any fence the - * target (or a third hart) posts to us meanwhile: two harts fencing - * each other would otherwise each wait for the other and time out. */ - spin = 400000000U; + * stale TLBs on that hart, so wait well past that: 1 s of CLINT + * time, the one clock every hart shares. A hart that never answers + * then costs the caller a second, not the OS its interrupts for + * minutes. M-mode interrupts are masked in here, so service any + * fence the target (or a third hart) posts to us meanwhile: two + * harts fencing each other would otherwise each wait for the other + * and time out. */ + deadline = CLINT_MTIME + SBI_IPI_WAIT_TICKS; while ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - - ticket[h]) < 0 && spin > 0U) { + ticket[h]) < 0 && (int64_t)(CLINT_MTIME - deadline) < 0) { if (sbi_ipi_ops[self] != 0U) { sbi_ipi_irq(self); } - spin--; } if ((int32_t)(__atomic_load_n(&sbi_ipi_done[h], __ATOMIC_ACQUIRE) - ticket[h]) < 0) { @@ -723,6 +858,9 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) case SBI_EXT_HSM: case SBI_EXT_SRST: case SBI_EXT_DBCN: +#ifdef MPFS_SBI_VENDOR + case SBI_EXT_VENDOR_MCHP: +#endif val = 1; break; default: val = 0; break; @@ -834,7 +972,10 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) if (c > sizeof(cbuf)) { c = sizeof(cbuf); } - sbi_copy_from_smode(cbuf, gp + k, c); + if (sbi_copy_from_smode(cbuf, gp + k, c) != 0) { + err = SBI_ERR_INVALID_ADDRESS; + break; + } for (j = 0; j < c; j++) { if (!sbi_putc((char)cbuf[j])) { k = n; @@ -857,6 +998,12 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) } break; +#ifdef MPFS_SBI_VENDOR + case SBI_EXT_VENDOR_MCHP: + err = sbi_hal_vendor_ecall(fid, regs, &val); + break; +#endif + case SBI_EXT_SRST: /* system_reset(type, reason). No console output here: by now the OS * owns the UARTs and a blocked printf leaves the hart stuck in the @@ -898,7 +1045,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) /* a0 = S-mode pointer to a hart mask; fetch via MPRV. */ if (regs[A0] != 0UL) { unsigned long lmask = 0; - sbi_copy_from_smode((uint8_t *)&lmask, regs[A0], + (void)sbi_copy_from_smode((uint8_t *)&lmask, regs[A0], sizeof(unsigned long)); sbi_post_ipi(lmask, 0, SBI_IPI_OP_SSIP, hartid, ticket); } @@ -911,7 +1058,7 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) __asm__ volatile("sfence.vma" ::: "memory"); if (regs[A0] != 0UL) { unsigned long fmask = 0; - sbi_copy_from_smode((uint8_t *)&fmask, regs[A0], + (void)sbi_copy_from_smode((uint8_t *)&fmask, regs[A0], sizeof(unsigned long)); sbi_post_ipi(fmask, 0, (eid == SBI_EXT_0_1_REMOTE_FENCE_I) ? diff --git a/src/vector_riscv.S b/src/vector_riscv.S index 5c45d1ec76..86ce97d190 100644 --- a/src/vector_riscv.S +++ b/src/vector_riscv.S @@ -24,6 +24,14 @@ #ifdef TARGET_mpfs250 #include "hal/mpfs250.h" #endif +/* Per-hart M-mode trap stacks (src/riscv_sbi.c); the platform header may + * place them elsewhere. */ +#ifndef SBI_MSTACK_BASE +#define SBI_MSTACK_BASE 0x103FC00000 +#endif +#ifndef SBI_MSTACK_SHIFT +#define SBI_MSTACK_SHIFT 12 +#endif /* ============================================================================ * Trap Entry/Exit Macros @@ -82,19 +90,32 @@ STORE x31, 31 * REGBYTES(sp) #ifdef WOLFBOOT_RISCV_MMODE - /* Frame slot 0 (x0's unused slot) = mscratch at entry: the trapped - * sp when we switched to the M-stack, 0 for an M-mode fallback or a - * nested trap. Zero mscratch while inside the handler so a nested - * trap keeps descending on the CURRENT stack instead of reusing the - * consumed mscratch value as a stack (which caused an infinite - * trap-in-trap recursion). Slot 2 = the trapped sp. x1 is already - * saved in slot 1, so reuse it as a scratch register. */ - csrr x1, mscratch - STORE x1, 0 * REGBYTES(sp) - csrw mscratch, x0 - bnez x1, 101f - addi x1, sp, 32 * REGBYTES + /* Slot 2 = the trapped sp: what mscratch holds after the switch above + * (any value, 0 included), or sp + frame when no switch happened. + * Slot 0 (x0's unused slot) = 1 when this frame switched to the + * M-stack, which is the case exactly when it sits at this hart's + * M-stack top; 0 for an M-mode fallback or a nested trap. The flag + * is derived from the frame address, never from the trapped sp: a + * context trapping with sp == 0 must still re-arm mscratch on exit + * (trap_exit), or the next trap from S-mode runs the entry on an + * untranslated virtual sp and faults forever. Zero mscratch while + * inside the handler so a nested trap keeps descending on the CURRENT + * stack. x1, t0 and t1 are already saved, so they serve as scratch. */ + csrr t0, mhartid + addi t0, t0, 1 + slli t0, t0, SBI_MSTACK_SHIFT + li t1, SBI_MSTACK_BASE + add t0, t0, t1 + addi t1, sp, 32 * REGBYTES + csrrw x1, mscratch, x0 + bne t1, t0, 101f + li t0, 1 + STORE t0, 0 * REGBYTES(sp) + j 102f 101: + STORE x0, 0 * REGBYTES(sp) + mv x1, t1 +102: STORE x1, 2 * REGBYTES(sp) /* The M-mode handler's C code addresses small globals gp-relative: diff --git a/tools/linux/wolfboot-sbi/Makefile b/tools/linux/wolfboot-sbi/Makefile new file mode 100644 index 0000000000..e2cfd817e6 --- /dev/null +++ b/tools/linux/wolfboot-sbi/Makefile @@ -0,0 +1,14 @@ +# Out-of-tree build of the wolfboot-sbi module against a kernel build tree: +# make KDIR=/path/to/kernel/build ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- +obj-m := wolfboot_sbi.o + +KDIR ?= /lib/modules/$(shell uname -r)/build + +all: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules + +modules_install: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules_install + +clean: + $(MAKE) -C $(KDIR) M=$(CURDIR) clean diff --git a/tools/linux/wolfboot-sbi/wolfboot_sbi.c b/tools/linux/wolfboot-sbi/wolfboot_sbi.c new file mode 100644 index 0000000000..bd58bd34bc --- /dev/null +++ b/tools/linux/wolfboot-sbi/wolfboot_sbi.c @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * wolfboot-sbi: Linux front end for the wolfBoot SBI vendor extension on + * PolarFire SoC. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * /dev/wolfboot (root only) exposes what the M-mode wolfBoot serves on the + * Microchip vendor extension: the firmware version, the device serial + * number, PUF-derived keys by purpose, and the System Controller digest, + * IAP and auto-update services when the hardware mailbox is fenced off + * from S-mode. Nothing here touches the mailbox registers, so the + * mainline System Controller drivers keep working in an unfenced build. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "wolfboot_sbi.h" + +#define SBI_EXT_VENDOR_MCHP 0x09000029UL + +#define SBI_MCHP_FID_HSS_REBOOT 0x10UL +#define SBI_WOLF_FID_GET_VERSION 0x40UL +#define SBI_WOLF_FID_GET_SERIAL 0x41UL +#define SBI_WOLF_FID_GET_KEY 0x42UL +#define SBI_WOLF_FID_DIGEST_CHECK 0x43UL +#define SBI_WOLF_FID_IAP_AUTHENTICATE 0x44UL +#define SBI_WOLF_FID_IAP_VERIFY 0x45UL +#define SBI_WOLF_FID_IAP_PROGRAM 0x46UL +#define SBI_WOLF_FID_AUTO_UPDATE 0x47UL +#define SBI_WOLF_FID_SC_FLASH_ERASE 0x48UL +#define SBI_WOLF_FID_SC_FLASH_WRITE 0x49UL +#define SBI_WOLF_FID_SC_FLASH_READ 0x4AUL +#define SBI_WOLF_FID_SC_FLASH_SET_IMAGE 0x4BUL +#define SBI_WOLF_FID_SC_STATUS 0x4FUL + +#define SBI_WOLF_VERSION_MIN 1UL + +/* asm/sbi.h has these from 6.9 on */ +#ifndef SBI_ERR_DENIED +#define SBI_ERR_DENIED (-4) +#endif +#ifndef SBI_ERR_INVALID_ADDRESS +#define SBI_ERR_INVALID_ADDRESS (-5) +#endif +#ifndef SBI_ERR_ALREADY_STARTED +#define SBI_ERR_ALREADY_STARTED (-7) +#endif + +static DEFINE_MUTEX(wolfboot_sbi_lock); + +static int wolfboot_sbi_errno(long sbi_err) +{ + switch (sbi_err) { + case SBI_SUCCESS: + return 0; + case SBI_ERR_NOT_SUPPORTED: + return -EOPNOTSUPP; + case SBI_ERR_INVALID_PARAM: + return -EINVAL; + case SBI_ERR_DENIED: + return -EPERM; + case SBI_ERR_INVALID_ADDRESS: + return -EFAULT; + case SBI_ERR_ALREADY_STARTED: + return -EBUSY; + default: + return -EIO; + } +} + +static long wolfboot_sbi_call(unsigned long fid, unsigned long a0, + unsigned long a1, unsigned long a2, + unsigned long *value) +{ + struct sbiret ret; + + ret = sbi_ecall(SBI_EXT_VENDOR_MCHP, fid, a0, a1, a2, 0, 0, 0); + if (value) + *value = ret.value; + return ret.error; +} + +static long wolfboot_sbi_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + void __user *uarg = (void __user *)arg; + unsigned long value = 0; + long err; + int rc; + + if (mutex_lock_interruptible(&wolfboot_sbi_lock)) + return -ERESTARTSYS; + + switch (cmd) { + case WOLFBOOT_SBI_IOC_GET_VERSION: { + u32 v; + + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_VERSION, 0, 0, 0, + &value); + rc = wolfboot_sbi_errno(err); + v = (u32)value; + if (!rc && copy_to_user(uarg, &v, sizeof(v))) + rc = -EFAULT; + break; + } + case WOLFBOOT_SBI_IOC_GET_SERIAL: { + u8 *serial = kzalloc(WOLFBOOT_SBI_SERIAL_LEN, GFP_KERNEL); + + if (!serial) { + rc = -ENOMEM; + break; + } + /* M-mode writes the buffer through this hart's translation. */ + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_SERIAL, + (unsigned long)serial, + WOLFBOOT_SBI_SERIAL_LEN, 0, NULL); + rc = wolfboot_sbi_errno(err); + if (!rc && copy_to_user(uarg, serial, WOLFBOOT_SBI_SERIAL_LEN)) + rc = -EFAULT; + kfree(serial); + break; + } + case WOLFBOOT_SBI_IOC_GET_KEY: { + struct wolfboot_sbi_key *req; + + req = kzalloc(sizeof(*req), GFP_KERNEL); + if (!req) { + rc = -ENOMEM; + break; + } + if (copy_from_user(req, uarg, sizeof(*req))) { + rc = -EFAULT; + } else { + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_KEY, + req->purpose, + (unsigned long)req->key, + WOLFBOOT_SBI_KEY_LEN, NULL); + rc = wolfboot_sbi_errno(err); + if (!rc && copy_to_user(uarg, req, sizeof(*req))) + rc = -EFAULT; + } + kfree_sensitive(req); + break; + } + case WOLFBOOT_SBI_IOC_DIGEST_CHECK: + case WOLFBOOT_SBI_IOC_IAP_AUTH: + case WOLFBOOT_SBI_IOC_IAP_VERIFY: + case WOLFBOOT_SBI_IOC_IAP_PROGRAM: { + unsigned long fid; + u32 a0; + + if (copy_from_user(&a0, uarg, sizeof(a0))) { + rc = -EFAULT; + break; + } + switch (cmd) { + case WOLFBOOT_SBI_IOC_DIGEST_CHECK: + fid = SBI_WOLF_FID_DIGEST_CHECK; + break; + case WOLFBOOT_SBI_IOC_IAP_AUTH: + fid = SBI_WOLF_FID_IAP_AUTHENTICATE; + break; + case WOLFBOOT_SBI_IOC_IAP_VERIFY: + fid = SBI_WOLF_FID_IAP_VERIFY; + break; + default: + /* The device restarts inside the call on success; the + * caller syncs its filesystems first. */ + fid = SBI_WOLF_FID_IAP_PROGRAM; + break; + } + err = wolfboot_sbi_call(fid, a0, 0, 0, NULL); + rc = wolfboot_sbi_errno(err); + break; + } + case WOLFBOOT_SBI_IOC_AUTO_UPDATE: + err = wolfboot_sbi_call(SBI_WOLF_FID_AUTO_UPDATE, 0, 0, 0, NULL); + rc = wolfboot_sbi_errno(err); + break; + case WOLFBOOT_SBI_IOC_SC_FLASH_ERASE: { + struct wolfboot_sbi_flash_range r; + u32 off; + + if (copy_from_user(&r, uarg, sizeof(r))) { + rc = -EFAULT; + break; + } + if (!r.len || (r.offset % WOLFBOOT_SBI_FLASH_ERASE_BLOCK) || + (r.len % WOLFBOOT_SBI_FLASH_ERASE_BLOCK) || + r.offset > U32_MAX - r.len) { + rc = -EINVAL; + break; + } + rc = 0; + /* one erase block per ecall: each holds its hart for the erase */ + for (off = r.offset; off < r.offset + r.len && !rc; + off += WOLFBOOT_SBI_FLASH_ERASE_BLOCK) { + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_ERASE, off, + WOLFBOOT_SBI_FLASH_ERASE_BLOCK, 0, + NULL); + rc = wolfboot_sbi_errno(err); + } + break; + } + case WOLFBOOT_SBI_IOC_SC_FLASH_WRITE: + case WOLFBOOT_SBI_IOC_SC_FLASH_READ: { + struct wolfboot_sbi_flash_io io; + u8 __user *ubuf; + u8 *bounce; + u32 done = 0; + + if (copy_from_user(&io, uarg, sizeof(io))) { + rc = -EFAULT; + break; + } + if (!io.len || io.offset > U32_MAX - io.len) { + rc = -EINVAL; + break; + } + ubuf = (u8 __user *)(uintptr_t)io.buf; + bounce = kzalloc(WOLFBOOT_SBI_FLASH_CHUNK, GFP_KERNEL); + if (!bounce) { + rc = -ENOMEM; + break; + } + rc = 0; + while (done < io.len && !rc) { + u32 n = min_t(u32, io.len - done, WOLFBOOT_SBI_FLASH_CHUNK); + + if (cmd == WOLFBOOT_SBI_IOC_SC_FLASH_WRITE) { + if (copy_from_user(bounce, ubuf + done, n)) { + rc = -EFAULT; + break; + } + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_WRITE, + io.offset + done, + (unsigned long)bounce, n, + NULL); + rc = wolfboot_sbi_errno(err); + } else { + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_READ, + io.offset + done, + (unsigned long)bounce, n, + NULL); + rc = wolfboot_sbi_errno(err); + if (!rc && copy_to_user(ubuf + done, bounce, n)) + rc = -EFAULT; + } + done += n; + } + kfree(bounce); + break; + } + case WOLFBOOT_SBI_IOC_SC_FLASH_SET_IMAGE: { + struct wolfboot_sbi_flash_image im; + + if (copy_from_user(&im, uarg, sizeof(im))) { + rc = -EFAULT; + break; + } + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_FLASH_SET_IMAGE, im.index, + im.address, 0, NULL); + rc = wolfboot_sbi_errno(err); + break; + } + case WOLFBOOT_SBI_IOC_SC_STATUS: { + struct wolfboot_sbi_sc_status st; + + err = wolfboot_sbi_call(SBI_WOLF_FID_SC_STATUS, 0, 0, 0, &value); + rc = wolfboot_sbi_errno(err); + st.status = (u32)(value & 0xFFFFUL); + st.mbox1 = (u32)(value >> 32); + if (!rc && copy_to_user(uarg, &st, sizeof(st))) + rc = -EFAULT; + break; + } + default: + rc = -ENOTTY; + break; + } + + mutex_unlock(&wolfboot_sbi_lock); + return rc; +} + +static const struct file_operations wolfboot_sbi_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = wolfboot_sbi_ioctl, + .compat_ioctl = compat_ptr_ioctl, +}; + +static struct miscdevice wolfboot_sbi_dev = { + .minor = MISC_DYNAMIC_MINOR, + .name = "wolfboot", + .fops = &wolfboot_sbi_fops, + .mode = 0600, +}; + +static int __init wolfboot_sbi_init(void) +{ + unsigned long value = 0; + long err; + + if (sbi_probe_extension(SBI_EXT_VENDOR_MCHP) <= 0) + return -ENODEV; + /* The HSS answers the same extension with NOT_SUPPORTED here. */ + err = wolfboot_sbi_call(SBI_WOLF_FID_GET_VERSION, 0, 0, 0, &value); + if (err != SBI_SUCCESS || value < SBI_WOLF_VERSION_MIN) + return -ENODEV; + + return misc_register(&wolfboot_sbi_dev); +} + +static void __exit wolfboot_sbi_exit(void) +{ + misc_deregister(&wolfboot_sbi_dev); +} + +module_init(wolfboot_sbi_init); +module_exit(wolfboot_sbi_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("wolfSSL Inc."); +MODULE_DESCRIPTION("wolfBoot SBI vendor extension front end for PolarFire SoC"); diff --git a/tools/linux/wolfboot-sbi/wolfboot_sbi.h b/tools/linux/wolfboot-sbi/wolfboot_sbi.h new file mode 100644 index 0000000000..2c4e003e73 --- /dev/null +++ b/tools/linux/wolfboot-sbi/wolfboot_sbi.h @@ -0,0 +1,91 @@ +/* wolfboot_sbi.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* User-space interface of the wolfboot-sbi Linux module: /dev/wolfboot + * exposes the wolfBoot SBI vendor calls (include/riscv_sbi.h). */ + +#ifndef WOLFBOOT_SBI_UAPI_H +#define WOLFBOOT_SBI_UAPI_H + +#include +#include + +#define WOLFBOOT_SBI_SERIAL_LEN 16 +#define WOLFBOOT_SBI_KEY_LEN 32 + +#define WOLFBOOT_SBI_KEY_MRAM_LOG 1 +#define WOLFBOOT_SBI_KEY_VARLOG 2 + +struct wolfboot_sbi_key { + __u32 purpose; + __u8 key[WOLFBOOT_SBI_KEY_LEN]; +}; + +/* A started System Controller service; poll with SC_STATUS until it does + * not return EBUSY. status is the 16-bit service status, mbox1 the second + * mailbox word (the DIGESTERR bits for a digest check). */ +struct wolfboot_sbi_sc_status { + __u32 status; + __u32 mbox1; +}; + +/* System Controller SPI flash (fenced builds with MPFS_SBI_SC_FLASH). Offsets + * are flash offsets; the first erase block (the SPI directory) is reachable + * only through SET_IMAGE, which points entry 1 (upgrade) or 2 (blank) at an + * image. The module splits a range into one-erase-block erases and 4 KB + * transfers, each its own ecall. */ +#define WOLFBOOT_SBI_FLASH_ERASE_BLOCK 65536 +#define WOLFBOOT_SBI_FLASH_CHUNK 4096 +#define WOLFBOOT_SBI_FLASH_UPGRADE_INDEX 1 +#define WOLFBOOT_SBI_FLASH_BLANK_INDEX 2 + +struct wolfboot_sbi_flash_range { + __u32 offset; + __u32 len; +}; + +struct wolfboot_sbi_flash_io { + __u32 offset; + __u32 len; + __u64 buf; /* user buffer */ +}; + +struct wolfboot_sbi_flash_image { + __u32 index; + __u32 address; +}; + +#define WOLFBOOT_SBI_IOC_MAGIC 'W' +#define WOLFBOOT_SBI_IOC_GET_VERSION _IOR(WOLFBOOT_SBI_IOC_MAGIC, 0, __u32) +#define WOLFBOOT_SBI_IOC_GET_SERIAL _IOR(WOLFBOOT_SBI_IOC_MAGIC, 1, __u8[WOLFBOOT_SBI_SERIAL_LEN]) +#define WOLFBOOT_SBI_IOC_GET_KEY _IOWR(WOLFBOOT_SBI_IOC_MAGIC, 2, struct wolfboot_sbi_key) +#define WOLFBOOT_SBI_IOC_DIGEST_CHECK _IOW(WOLFBOOT_SBI_IOC_MAGIC, 3, __u32) +#define WOLFBOOT_SBI_IOC_IAP_AUTH _IOW(WOLFBOOT_SBI_IOC_MAGIC, 4, __u32) +#define WOLFBOOT_SBI_IOC_IAP_VERIFY _IOW(WOLFBOOT_SBI_IOC_MAGIC, 5, __u32) +#define WOLFBOOT_SBI_IOC_IAP_PROGRAM _IOW(WOLFBOOT_SBI_IOC_MAGIC, 6, __u32) +#define WOLFBOOT_SBI_IOC_AUTO_UPDATE _IO(WOLFBOOT_SBI_IOC_MAGIC, 7) +#define WOLFBOOT_SBI_IOC_SC_STATUS _IOR(WOLFBOOT_SBI_IOC_MAGIC, 8, struct wolfboot_sbi_sc_status) +#define WOLFBOOT_SBI_IOC_SC_FLASH_ERASE _IOW(WOLFBOOT_SBI_IOC_MAGIC, 9, struct wolfboot_sbi_flash_range) +#define WOLFBOOT_SBI_IOC_SC_FLASH_WRITE _IOW(WOLFBOOT_SBI_IOC_MAGIC, 10, struct wolfboot_sbi_flash_io) +#define WOLFBOOT_SBI_IOC_SC_FLASH_READ _IOWR(WOLFBOOT_SBI_IOC_MAGIC, 11, struct wolfboot_sbi_flash_io) +#define WOLFBOOT_SBI_IOC_SC_FLASH_SET_IMAGE _IOW(WOLFBOOT_SBI_IOC_MAGIC, 12, struct wolfboot_sbi_flash_image) + +#endif /* WOLFBOOT_SBI_UAPI_H */ From 836d4d81cedf63693549f224c58f14902c5f955a Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 8 Oct 2026 19:29:33 -0700 Subject: [PATCH 7/9] PolarFire SoC: boot mode 2 User Boot Loader stub verifying the eNVM wolfBoot --- .github/workflows/test-configs.yml | 14 + Makefile | 28 ++ arch.mk | 14 + config/examples/polarfire_mpfs250_ubl.config | 77 +++++ docs/Targets.md | 84 ++++++ hal/mpfs250-m.ld | 19 +- hal/mpfs250.c | 8 + hal/mpfs250.h | 8 + hal/mpfs250_ubl.c | 297 +++++++++++++++++++ hal/mpfs250_ubl.h | 44 +++ hal/mpfs250_ubl.ld | 90 ++++++ src/boot_riscv_start.S | 74 ++++- tools/scripts/mpfs_bootmode2.sh | 148 +++++++++ 13 files changed, 901 insertions(+), 4 deletions(-) create mode 100644 config/examples/polarfire_mpfs250_ubl.config create mode 100644 hal/mpfs250_ubl.c create mode 100644 hal/mpfs250_ubl.h create mode 100644 hal/mpfs250_ubl.ld create mode 100755 tools/scripts/mpfs_bootmode2.sh diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index ad515e99c9..1eaea65e0b 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -521,6 +521,20 @@ jobs: config-file: ./config/examples/polarfire_mpfs250_m.config pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 IMAGE_HEADER_SIZE=12288 WOLFBOOT_SECTOR_SIZE=0x4000 + # Boot mode 2: the eNVM wolfBoot linked above the signed manifest and + # signed as a wolfBoot image, and the sNVM UBL stub that verifies it. + microchip_mpfs250_m_signed_envm_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_m.config + pre-build: sh tools/ci/gen_mpfs_libero_stub.sh tools/ci/mpfs_libero_stub + make-args: LIBERO_FPGA_CONFIG_DIR=tools/ci/mpfs_libero_stub MPFS_ENVM_MANIFEST=1 wolfboot_envm_signed.bin + microchip_mpfs250_ubl_test: + uses: ./.github/workflows/test-build-riscv.yml + with: + arch: riscv64 + config-file: ./config/examples/polarfire_mpfs250_ubl.config # Trust anchor served from sNVM with the production hardenings: ROM-flag # enforcement and the S-mode fence around the System Controller mailbox. microchip_mpfs250_m_snvm_test: diff --git a/Makefile b/Makefile index 439ab648b8..14ba8f0d40 100644 --- a/Makefile +++ b/Makefile @@ -516,6 +516,11 @@ ifeq ($(TARGET),rp2350) MAIN_TARGET:=include/target.h keytools wolfboot_signing_private_key.der pico-sdk-info endif +ifeq ($(TARGET),mpfs250_ubl) + # Boot mode 2 stub: verifies the eNVM wolfBoot, there is no application + MAIN_TARGET:=wolfboot.bin +endif + ifeq ($(FLASH_OTP_KEYSTORE),1) MAIN_TARGET+=tools/keytools/otp/otp-keystore-primer.bin @@ -769,6 +774,29 @@ factory_wstage1.bin: $(BINASSEMBLE) stage1/loader_stage1.bin wolfboot.bin $(BOOT wolfboot_stage1.bin: wolfboot.elf stage1/loader_stage1.bin $(Q) cp stage1/loader_stage1.bin wolfboot_stage1.bin +# PolarFire SoC boot mode 2: the eNVM wolfBoot signed as a wolfBoot image for +# the UBL stub (TARGET=mpfs250_ubl) to verify. The signed bytes are the +# image without its manifest placeholder (MPFS_ENVM_MANIFEST=1 links one in +# the last eNVM page); the manifest header the sign tool produces is then +# written into that placeholder, so wolfboot_envm_signed.elf programs image +# and manifest in one mpfsBootmodeProgrammer --bootmode 1 run. The signed +# bytes are what that programmer writes: a plain objcopy of the ELF, section +# alignment gaps zero-filled. MPFS_UBL_KEY is the stub's signing key. +MPFS_UBL_KEY?=$(PRIVATE_KEY) +MPFS_ENVM_VERSION?=1 +wolfboot_envm_signed.bin: wolfboot.elf $(MPFS_UBL_KEY) keytools_check + @echo "\t[SIGN] $@" + $(Q)(test "$(MPFS_ENVM_MANIFEST)" = "1") || \ + (echo "wolfboot_envm_signed.bin needs MPFS_ENVM_MANIFEST=1" && false) + $(Q)$(OBJCOPY) -O binary --gap-fill 0x00 -R .envm_manifest wolfboot.elf \ + wolfboot_envm.bin + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) wolfboot_envm.bin \ + $(MPFS_UBL_KEY) $(MPFS_ENVM_VERSION) + $(Q)mv wolfboot_envm_v$(MPFS_ENVM_VERSION)_signed.bin $@ + $(Q)head -c $(IMAGE_HEADER_SIZE) $@ > wolfboot_envm_manifest.bin + $(Q)$(OBJCOPY) --update-section .envm_manifest=wolfboot_envm_manifest.bin \ + wolfboot.elf wolfboot_envm_signed.elf + wolfboot.elf: include/target.h $(LSCRIPT) $(OBJS) $(BINASSEMBLE) $(WOLFBOOT_SIGN_KEY_DEP) FORCE $(Q)(test $(SIGN) = NONE) || (test $(FLASH_OTP_KEYSTORE) = 1) || (test "$(WOLFBOOT_NO_KEYSTORE)" = "1") || (test "$(SNVM_KEYSTORE)" = "1") || (grep -q $(SIGN_ALG) src/keystore.c) || \ (echo "Key mismatch: please run 'make keysclean' to remove all keys if you want to change algorithm" && false) diff --git a/arch.mk b/arch.mk index 8c54816a73..d6a8aa0ef4 100644 --- a/arch.mk +++ b/arch.mk @@ -1234,6 +1234,11 @@ ifeq ($(ARCH),RISCV64) endif # Use M-mode specific linker script LSCRIPT_IN:=hal/$(TARGET)-m.ld + # Boot mode 2: reserve the last eNVM page for the signed manifest the UBL + # stub verifies (hal/mpfs250-m.ld); `make wolfboot_envm_signed.bin` fills it. + ifeq ($(MPFS_ENVM_MANIFEST),1) + CFLAGS+=-DMPFS_ENVM_MANIFEST + endif # LIBERO_FPGA_CONFIG_DIR supplies fpga_design_config.h and enables DDR init. # An S-mode OS runs from DDR, so an empty value would silently build a # bootloader with no DDR init at all -- fail instead. @@ -1285,6 +1290,15 @@ ifeq ($(ARCH),RISCV64) UPDATE_OBJS?=src/update_ram.o endif + # Boot mode 2 UBL stub: LIM-resident, verifies the eNVM wolfBoot manifest + # in place and jumps; no update path (hal/mpfs250_ubl.c). + ifeq ($(TARGET),mpfs250_ubl) + LSCRIPT_IN:=hal/mpfs250_ubl.ld + UPDATE_OBJS:= + # mcycle rate for hal_get_timer_us(): the stub runs at the reset clock + CFLAGS+=-DRISCV_SMODE_TIMER_FREQ=80000000UL + endif + # GCC 12+ extension detection (see RISCV 32-bit section above). # Extensions go in CFLAGS only; LDFLAGS uses the base march to avoid # GCC 15 multilib lookup failures from ISA string decomposition. diff --git a/config/examples/polarfire_mpfs250_ubl.config b/config/examples/polarfire_mpfs250_ubl.config new file mode 100644 index 0000000000..e4005cbd3d --- /dev/null +++ b/config/examples/polarfire_mpfs250_ubl.config @@ -0,0 +1,77 @@ +# PolarFire SoC MPFS250 boot mode 2 User Boot Loader (UBL) stub +# +# A first-stage verifier for the "user secure boot from sNVM" boot mode: +# 1. The System Controller authenticates (and optionally decrypts) the +# stub out of sNVM, copies it to the LIM (0x08000000) and releases +# all five harts at its entry point. +# 2. The E51 verifies the wolfBoot manifest in eNVM (ECC384 signature, +# SHA384 digest) against the public key compiled into the stub. +# 3. All five harts enter the eNVM wolfBoot at its reset vector, exactly +# as a boot mode 1 boot would; wolfBoot then boots the system from +# there (see polarfire_mpfs250_m.config). +# +# The eNVM wolfBoot is built with MPFS_ENVM_MANIFEST=1 and signed with +# `make wolfboot_envm_signed.bin`: the image stays at 0x20220100 and its +# manifest goes in eNVM page 0x1FC (0x2023FC00); see docs/Targets.md. +# +# Program sNVM + boot mode (requires SoftConsole / Libero SoC): +# java -jar mpfsBootmodeProgrammer.jar --bootmode 2 --snvm_page 0 \ +# --usk 0 --die MPFS250T --package FCG1152 --workdir $PWD wolfboot.elf + +ARCH?=RISCV64 +TARGET?=mpfs250_ubl +SIGN?=ECC384 +HASH?=SHA384 +IMAGE_HEADER_SIZE=512 +WOLFBOOT_VERSION?=1 +ARMORED?=0 +DEBUG?=0 +DEBUG_SYMBOLS?=1 +DEBUG_UART?=1 +VTOR?=1 +EXT_FLASH?=0 +SPI_FLASH?=0 +NO_XIP?=1 +NVM_FLASH_WRITEONCE?=0 +UART_FLASH?=0 +V?=0 +NO_MPU?=1 +RAM_CODE?=0 +SPMATH?=1 +SPMATHALL?=0 +DUALBANK_SWAP?=0 +PKA?=0 +ENCRYPT=0 +WOLFTPM?=0 +ELF?=0 + +# Size first: the stub must fit the authenticated sNVM pages (see +# hal/mpfs250_ubl.ld). Portable C crypto; the E51 is rv64imac. +OPTIMIZATION_LEVEL=s +NO_ASM?=1 + +# Runs on the E51 in M-mode at the reset clock, no HSS. +RISCV_MMODE?=1 + +# No update path: the stub verifies one manifest in place and jumps. +NO_LOADER=1 +WOLFBOOT_NO_PARTITIONS=1 + +# LIM placement. The System Controller copies the image to the address in +# the UBL header, which the programmer takes from the ELF. +WOLFBOOT_ORIGIN?=0x08000000 +BOOTLOADER_PARTITION_SIZE?=0x40000 +STACK_SIZE?=32k + +# The manifest header is detached from the image it signs: a page near the +# end of eNVM, image from the reset vector (hal/mpfs250.h). +WOLFBOOT_SELF_HEADER=1 +WOLFBOOT_PARTITION_SELF_HEADER_ADDRESS=0x2023FC00 +WOLFBOOT_SECTOR_SIZE?=0x200 + +# Informational: where the verified eNVM wolfBoot lives. +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x20220100 +WOLFBOOT_PARTITION_SIZE=0x1FD00 + +# Refuse a manifest older than this version (0 = any). +#CFLAGS_EXTRA+=-DMPFS_UBL_MIN_VERSION=1 diff --git a/docs/Targets.md b/docs/Targets.md index e7a09038ab..794c5485e2 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -987,6 +987,7 @@ programming, Memory Test Controller, LPDDR4 mode-register protocol) `hal/mpfs250.ld` - Linker script for S-mode (HSS-based boot) `hal/mpfs250-m.ld` - Linker script for M-mode (eNVM + L2 SRAM) `hal/mpfs250-hss.ld` - Linker script for S-mode (HSS with L2-LIM) +`hal/mpfs250_ubl.c` / `hal/mpfs250_ubl.ld` - Boot mode 2 User Boot Loader stub (sNVM-authenticated verifier of the eNVM wolfBoot) `hal/mpfs.dts` - Device tree source `hal/mpfs.yaml` - HSS payload generator configuration for use of DDR `hal/mpfs-l2lim.yaml` - HSS payload generator for the use of L2-LIM @@ -1323,6 +1324,89 @@ register table and composes the generic calls. Both compile only when `MPFS_DDR_ The System Controller SRAM-PUF, secure NVM (sNVM), and TeraFire crypto can anchor key material in hardware: serve the verification public keys from sNVM, derive a device-unique KEK from the PUF, and store the AES image-encryption key in sNVM wrapped by that KEK. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). +### PolarFire SoC boot mode 2: User Boot Loader stub + +Boot mode 2 ("user secure boot from sNVM") lets the System Controller authenticate the first code the MSS runs: it copies a User Boot Loader (UBL) out of authenticated sNVM pages into the LIM and releases all five harts at its entry. wolfBoot provides that UBL as a small verifier target, `TARGET=mpfs250_ubl`: it checks an ECC384/SHA384 wolfBoot manifest over the eNVM wolfBoot image against the public key compiled into the stub, then enters the eNVM wolfBoot at its reset vector on every hart exactly as a boot mode 1 boot would. The eNVM wolfBoot itself is unchanged and stays field-updatable; what the sNVM pages pin is the key that must have signed it. + +**eNVM layout** (`hal/mpfs250-m.ld`, `hal/mpfs250.h`) + +| Address | Content | +|---------|---------| +| `0x20220000` | boot ROM meta information (written by `mpfsBootmodeProgrammer`) | +| `0x20220100` | wolfBoot image, reset vector first | +| `0x2023FC00` | wolfBoot manifest header (`MPFS_ENVM_MANIFEST=1`), signed by the stub's key | + +The manifest is detached from the image it signs (the `WOLFBOOT_SELF_HEADER` form), so the image does not move and a boot mode 1 device boots the same eNVM contents. The last two eNVM pages stay free for the programmer. + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_ubl.config` | UBL stub configuration | +| `hal/mpfs250_ubl.c`, `hal/mpfs250_ubl.h` | stub HAL: watchdog hold, MMUART0 at the reset clock, manifest verify, hart hand-off | +| `hal/mpfs250_ubl.ld` | LIM linker script with the sNVM size limit | +| `src/boot_riscv_start.S` | `TARGET_mpfs250_ubl` startup: no copy, U54s park on a DTIM mailbox | +| `tools/scripts/mpfs_bootmode2.sh` | sNVM + boot mode programming on the secure dies, eNVM-only update | + +**Build the stub** (its own signing key; keep it out of the tree that signs application images): +```sh +cp config/examples/polarfire_mpfs250_ubl.config .config +make clean && make +# wolfboot.elf is the UBL; wolfboot_signing_private_key.der is the UBL key +``` +The image must fit the 221 authenticated sNVM pages (52,096 bytes after the 60-byte UBL header); the linker script enforces it. The sNVM pages above the stub stay available for the sNVM keystore and wrapped keys of [polarfire_snvm_puf.md](polarfire_snvm_puf.md). + +**Build and sign the eNVM wolfBoot** for the stub (any M-mode configuration): +```sh +cp config/examples/polarfire_mpfs250_m.config .config +make clean && make LIBERO_FPGA_CONFIG_DIR=... MPFS_ENVM_MANIFEST=1 \ + MPFS_UBL_KEY=/path/to/ubl/wolfboot_signing_private_key.der \ + wolfboot_envm_signed.bin +``` +`MPFS_ENVM_MANIFEST=1` links a manifest placeholder into the last used eNVM page; the rule signs the image without it (`wolfboot_envm.bin`, the ELF's loadable bytes with alignment gaps zero-filled, which is what the programmer writes), writes the resulting header into the placeholder and produces `wolfboot_envm_signed.elf`, which programs image and manifest together. `MPFS_UBL_KEY` defaults to the tree's own key, `MPFS_ENVM_VERSION` (default 1) is the manifest version, and `-DMPFS_UBL_MIN_VERSION=N` in the stub build refuses older manifests. + +**Program** (requires `SC_INSTALL_DIR` and `FPGENPROG`, see the M-mode section): +```sh +# 1. eNVM: image + manifest. This also selects boot mode 1, so the device +# boots the new eNVM contents before the stub is in place. +java -jar $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD \ + wolfboot_envm_signed.elf + +# 2. sNVM: the stub, and boot mode 2. Non-secure dies: the programmer +# itself (--bootmode 2 --snvm_page 0 --usk 0 wolfboot.elf). Secure dies +# (MPFS250TS...), which the programmer does not list but which an +# authenticated sNVM client requires: +tools/scripts/mpfs_bootmode2.sh --die MPFS250TS --workdir $PWD /path/to/ubl/wolfboot.elf +``` +The script runs the programmer in `--dryrun` for the UBL image and the User sNVM Key (USK), replays its `fpgenprog` steps for the requested die and programs. Keep the USK file it names: the sNVM pages can only be rewritten with it. + +The UBL lives in an authenticated sNVM client, which the System Controller keys from the device's sNVM Master Key (SMK) +and the client's 12-byte USK. A device whose SMK has never been initialized cannot hold such a client, and +`mpfsBootmodeProgrammer` never asks for one, so `--regenerate-smk` passes `--regenerate_smk` to the `snvm_client` step +and the generated bitstream carries a new SMK. A new SMK invalidates every authenticated or encrypted sNVM client +already on the device; wolfBoot's own keystore and wrapped image key are plaintext clients and are not SMK-keyed. + +Updating the eNVM wolfBoot under boot mode 2 goes through the same script with `--envm`, which programs the eNVM client only and leaves the boot mode and sNVM alone: +```sh +tools/scripts/mpfs_bootmode2.sh --envm --die MPFS250TS --workdir $PWD wolfboot_envm_signed.elf +``` +`mpfsBootmodeProgrammer --bootmode 1` restores boot mode 1. + +**Expected serial output:** +``` +wolfBoot UBL 2.10.0: eNVM image 0x20220100, manifest 0x2023FC00 +UBL: eNVM wolfBoot signature OK (1051 ms), entering 0x20220100 +UBL: 5 of 5 harts released +wolfBoot Version: ... +``` +A manifest that does not verify prints which check failed (`no manifest`, `digest mismatch`, `signature invalid`) and `UBL: eNVM wolfBoot verify FAILED`; the stub panics and nothing in eNVM runs. + +**Notes:** +- The stub runs at the reset clock and changes no clock, L2 way or peripheral the eNVM wolfBoot expects to find in its reset state. It holds the five MSS watchdogs in the no-reset, maximum-window state for the verify and puts the E51's back before the jump, so the eNVM wolfBoot's own watchdog snapshot is what the boot ROM left. +- The U54s wait in the stub on a DTIM mailbox (`hal/mpfs250_ubl.h`) for the verified entry, acknowledge and jump; the E51 follows last. All five harts then run the eNVM startup as in boot mode 1. +- Immutability of the stub is a Libero decision: the ROM flag on its sNVM pages and a frozen boot configuration. wolfBoot does not program either. + ### PolarFire testing This section describes how to build the test-application, create a custom uSD with required partitions and copying signed test-application to uSD partitions. diff --git a/hal/mpfs250-m.ld b/hal/mpfs250-m.ld index b73ccc53f9..3cfde6bf1c 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -9,10 +9,13 @@ * 3. Jumps to L2_SCRATCH for execution * * The first 0x100 bytes of eNVM are reserved for the boot ROM secure boot - * meta information added by mpfsBootmodeProgrammer. + * meta information added by mpfsBootmodeProgrammer. A page near the end + * holds the signed manifest of this image when the boot mode 2 stub verifies + * it (MPFS_ENVM_MANIFEST=1, see hal/mpfs250_ubl.c); it is filled in after + * the link and left out of the signed bytes. * * Memory regions: - * FLASH_ENVM - Embedded NVM (128KB - 0x100 for header) + * FLASH_ENVM - Embedded NVM (128KB - 0x100 for header - 1KB at the end) * L2_SCRATCH - L2 Scratchpad SRAM (256KB) - execution and data */ @@ -24,7 +27,10 @@ MEMORY { /* The first 0x100 bytes of eNVM are used for boot ROM secure boot meta information * This offset is added by mpfsBootmodeProgrammer (bootmode 1) */ - FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 + FLASH_ENVM (rx) : ORIGIN = 0x20220100, LENGTH = 128k - 0x100 - 0x400 + /* Signed manifest of the eNVM image, verified by the boot mode 2 stub. + * The last two pages stay free: mpfsBootmodeProgrammer keeps them. */ + ENVM_MANIFEST (r) : ORIGIN = 0x2023FC00, LENGTH = 0x200 /* L2 scratchpad. MPFS250 hardwires ways 8-11, so 4 x 128 KB is the * ceiling the startup pin loop can make resident; ASSERT below enforces it. */ @@ -110,6 +116,13 @@ SECTIONS _end_bss = .; _end = .; } > L2_SCRATCH + + /* Placeholder the signed build overwrites with the manifest header; + * empty, and absent from the ELF, unless MPFS_ENVM_MANIFEST=1. */ + .envm_manifest : + { + KEEP(*(.envm_manifest)) + } > ENVM_MANIFEST } /* Heap starts after BSS (between _end and stack) */ diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 9f4249aed0..5e9c607bd6 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -98,6 +98,14 @@ uint32_t mpfs_apb_clk_hz = MSS_APB_AHB_CLK; * A wrong value here skews timing-sensitive paths (e.g. SD power-up). */ uint32_t mpfs_cpu_freq_mhz = MPFS_CPU_FREQ_RESET_MHZ; +#ifdef MPFS_ENVM_MANIFEST +/* Last eNVM page: the signed manifest of this image, written in after the + * link by `make wolfboot_envm_signed.bin` and checked by the boot mode 2 + * stub (hal/mpfs250_ubl.c). Erased-state fill until then. */ +const uint8_t __attribute__((section(".envm_manifest"), used)) + mpfs_envm_manifest[IMAGE_HEADER_SIZE] = { [0 ... IMAGE_HEADER_SIZE - 1] = 0xFF }; +#endif + /* Saved boot ROM watchdog values, restored in hal_prepare_boot(). */ static uint32_t mpfs_wdt_default_mvrp = 0; static uint32_t mpfs_wdt_default_ctrl = 0; diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 379e0bb7cb..44e5b4a8c4 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -61,6 +61,14 @@ #define MPFS_DDR_MAX_OUTER_RETRY 6U #endif +/* eNVM: boot ROM meta information in page 0, the wolfBoot image from page 1 + * (hal/mpfs250-m.ld), and in the boot mode 2 flow its signed manifest two + * pages below the end, verified by the UBL stub (hal/mpfs250_ubl.c). */ +#define MPFS_ENVM_BASE 0x20220000UL +#define MPFS_ENVM_SIZE 0x20000UL +#define MPFS_ENVM_IMAGE_ADDR (MPFS_ENVM_BASE + 0x100UL) +#define MPFS_ENVM_MANIFEST_ADDR (MPFS_ENVM_BASE + MPFS_ENVM_SIZE - 0x400UL) + /* Hardware Base Address */ #define SYSREG_BASE 0x20002000 diff --git a/hal/mpfs250_ubl.c b/hal/mpfs250_ubl.c new file mode 100644 index 0000000000..4888d0711e --- /dev/null +++ b/hal/mpfs250_ubl.c @@ -0,0 +1,297 @@ +/* mpfs250_ubl.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* PolarFire SoC boot mode 2 User Boot Loader (UBL) stub. + * + * Authenticated by the System Controller out of sNVM and run from the LIM, + * this verifies the signed wolfBoot manifest in eNVM and enters wolfBoot at + * its reset vector on every hart, as a boot mode 1 boot would. It touches + * no clock, cache way or peripheral the eNVM wolfBoot does not expect to + * find in its reset state. */ + +#include +#include + +#include "image.h" +#include "loader.h" +#include "hal.h" +#include "printf.h" +#include "hal/mpfs250.h" +#include "hal/mpfs250_ubl.h" +#include "wolfboot/version.h" + +#define UBL_MAGIC (*(volatile uint32_t *)(uintptr_t)MPFS_UBL_MAGIC_ADDR) +#define UBL_ENTRY (*(volatile uint64_t *)(uintptr_t)MPFS_UBL_ENTRY_ADDR) +#define UBL_ENTRY_INV (*(volatile uint64_t *)(uintptr_t)MPFS_UBL_ENTRY_INV_ADDR) +#define UBL_ACK(h) (*(volatile uint32_t *)(uintptr_t)(MPFS_UBL_ACK_BASE + \ + (h) * 4)) + +/* Wait for the parked harts to acknowledge the published entry. */ +#define UBL_ACK_WAIT_US 50000U + +static const unsigned long wdt_bases[MPFS_NUM_HARTS] = { + MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, + MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE +}; + +/* Boot ROM E51 watchdog state, restored before the jump so the eNVM + * wolfBoot's hal_init() snapshots what the ROM left. */ +static uint32_t ubl_wdt_mvrp; +static uint32_t ubl_wdt_ctrl; + +static uint64_t rdcycle(void) +{ + uint64_t c; + __asm__ volatile("rdcycle %0" : "=r"(c)); + return c; +} + +void hal_init(void) +{ + unsigned int w; + + ubl_wdt_mvrp = MSS_WDT_MVRP(MSS_WDT_E51_BASE); + ubl_wdt_ctrl = MSS_WDT_CONTROL(MSS_WDT_E51_BASE); + + /* Same sequence as the eNVM wolfBoot: no device reset on timeout and the + * widest window, so the signature check cannot be cut short. */ + for (w = 0; w < MPFS_NUM_HARTS; w++) { + MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; + MSS_WDT_TIME(wdt_bases[w]) = 0x00FFFFFFUL; + MSS_WDT_MVRP(wdt_bases[w]) = 0x00FFFFFFUL; + MSS_WDT_CONTROL(wdt_bases[w]) = 0; + MSS_WDT_REFRESH(wdt_bases[w]) = 0xDEADC0DEU; + } + +#ifdef DEBUG_UART + SYSREG_SUBBLK_CLOCK_CR |= (MSS_PERIPH_MMUART0 << DEBUG_UART_PORT); + SYSREG_SOFT_RESET_CR &= ~(MSS_PERIPH_MMUART0 << DEBUG_UART_PORT); + uart_init(); +#endif +} + +void hal_prepare_boot(void) +{ + MSS_WDT_REFRESH(MSS_WDT_E51_BASE) = 0xDEADC0DEU; + MSS_WDT_MVRP(MSS_WDT_E51_BASE) = ubl_wdt_mvrp; + MSS_WDT_CONTROL(MSS_WDT_E51_BASE) = ubl_wdt_ctrl; +} + +/* The manifest is verified in place; there is no flash backend. */ +void hal_flash_unlock(void) +{ +} + +void hal_flash_lock(void) +{ +} + +int hal_flash_write(uint32_t address, const uint8_t *data, int len) +{ + (void)address; + (void)data; + (void)len; + return -1; +} + +int hal_flash_erase(uint32_t address, int len) +{ + (void)address; + (void)len; + return -1; +} + +#ifdef DEBUG_UART +/* The MMUART sequence of hal/mpfs250.c, at the compile-time reset clock: + * a fix there belongs here too. */ +static void uart_config_baud(unsigned long base, uint32_t baudrate) +{ + const uint64_t pclk = MSS_APB_AHB_CLK; + uint32_t div_x128 = (uint32_t)((8UL * pclk) / baudrate); + uint32_t div_x64 = div_x128 / 2u; + uint32_t div_int = div_x64 / 64u; + uint32_t div_frac = div_x64 - (div_int * 64u); + div_frac += (div_x128 - (div_int * 128u)) - (div_frac * 2u); + if (div_frac > 63u) + div_frac = 63u; + if (div_int > (uint32_t)UINT16_MAX) + return; + MMUART_LCR(base) |= DLAB_MASK; + MMUART_DMR(base) = (uint8_t)(div_int >> 8); + MMUART_DLR(base) = (uint8_t)div_int; + MMUART_LCR(base) &= ~DLAB_MASK; + if (div_int > 1u) { + MMUART_MM0(base) |= EFBR_MASK; + MMUART_DFR(base) = (uint8_t)div_frac; + } else { + MMUART_MM0(base) &= ~EFBR_MASK; + } +} + +void uart_init(void) +{ + const unsigned long base = DEBUG_UART_BASE; + + MMUART_MM0(base) &= ~ELIN_MASK; + MMUART_MM1(base) &= ~EIRD_MASK; + MMUART_MM2(base) &= ~EERR_MASK; + MMUART_IER(base) = 0u; + MMUART_FCR(base) = CLEAR_RX_FIFO_MASK | CLEAR_TX_FIFO_MASK | RXRDY_TXRDYN_EN_MASK; + MMUART_MCR(base) &= ~(LOOP_MASK | RLOOP_MASK); + MMUART_MCR(base) |= RTS_MASK; + MMUART_MM1(base) &= ~(E_MSB_TX_MASK | E_MSB_RX_MASK); + MMUART_MM2(base) &= ~(EAFM_MASK | ESWM_MASK); + MMUART_MM0(base) &= ~(ETTG_MASK | ERTO_MASK | EFBR_MASK); + MMUART_GFR(base) = 0u; + MMUART_TTG(base) = 0u; + MMUART_RTO(base) = 0u; + uart_config_baud(base, 115200); + MMUART_LCR(base) = MSS_UART_DATA_8_BITS | MSS_UART_NO_PARITY | MSS_UART_ONE_STOP_BIT; +} + +void uart_write(const char* buf, unsigned int sz) +{ + uint32_t pos = 0; + while (sz-- > 0) { + char c = buf[pos++]; + if (c == '\n') { + while ((MMUART_LSR(DEBUG_UART_BASE) & MSS_UART_THRE) == 0); + MMUART_THR(DEBUG_UART_BASE) = '\r'; + } + while ((MMUART_LSR(DEBUG_UART_BASE) & MSS_UART_THRE) == 0); + MMUART_THR(DEBUG_UART_BASE) = c; + } +} + +static void uart_drain(void) +{ + while ((MMUART_LSR(DEBUG_UART_BASE) & MSS_UART_TEMT) == 0); +} +#endif /* DEBUG_UART */ + +/* Hand the verified entry to the parked harts, then take it last. */ +static void __attribute__((noreturn)) ubl_enter(uintptr_t entry) +{ + uint64_t start; + unsigned int h, acked; + + hal_prepare_boot(); +#ifdef DEBUG_UART + uart_drain(); +#endif + + for (h = 1; h < MPFS_NUM_HARTS; h++) { + UBL_ACK(h) = 0; + } + UBL_ENTRY = entry; + UBL_ENTRY_INV = ~(uint64_t)entry; + __asm__ volatile("fence iorw, iorw" ::: "memory"); + UBL_MAGIC = MPFS_UBL_MAGIC; + __asm__ volatile("fence iorw, iorw" ::: "memory"); + + start = rdcycle(); + do { + acked = 0; + for (h = 1; h < MPFS_NUM_HARTS; h++) { + acked += (UBL_ACK(h) != 0); + } + } while ((acked < MPFS_NUM_HARTS - 1) && + ((rdcycle() - start) < + (uint64_t)UBL_ACK_WAIT_US * MPFS_CPU_FREQ_RESET_MHZ)); +#ifdef DEBUG_UART + wolfBoot_printf("UBL: %u of %u harts released\n", acked + 1, + (unsigned int)MPFS_NUM_HARTS); + uart_drain(); +#endif + + __asm__ volatile("fence.i" ::: "memory"); + ((void (*)(void))entry)(); + while (1) { + } +} + +void main(void) +{ + struct wolfBoot_image img; + uint8_t *manifest = (uint8_t *)(uintptr_t)MPFS_ENVM_MANIFEST_ADDR; + uint8_t *image = (uint8_t *)(uintptr_t)MPFS_ENVM_IMAGE_ADDR; +#ifdef DEBUG_UART + uint64_t start = rdcycle(); +#endif + int ret; + + /* Invalidate whatever the mailbox held before the U54s can act on it */ + UBL_MAGIC = 0; + UBL_ENTRY = 0; + UBL_ENTRY_INV = 0; + __asm__ volatile("fence iorw, iorw" ::: "memory"); + + hal_init(); + + wolfBoot_printf("wolfBoot UBL %s: eNVM image 0x%08lx, manifest 0x%08lx\n", + LIBWOLFBOOT_VERSION_STRING, + (unsigned long)(uintptr_t)image, + (unsigned long)(uintptr_t)manifest); + + /* The manifest sits in the last eNVM page, the image it signs from the + * reset vector up to it. */ + ret = wolfBoot_open_self_address(&img, manifest, image); + if (ret != 0) { + wolfBoot_printf("UBL: no manifest at 0x%08lx\n", + (unsigned long)(uintptr_t)manifest); + } + else if (img.fw_size > (uint32_t)(manifest - image)) { + wolfBoot_printf("UBL: manifest size %u exceeds eNVM\n", + (unsigned int)img.fw_size); + ret = -1; + } + if (ret == 0) { + ret = wolfBoot_verify_integrity(&img); + if (ret != 0) { + wolfBoot_printf("UBL: eNVM wolfBoot digest mismatch\n"); + } + } + if (ret == 0) { + ret = wolfBoot_verify_authenticity(&img); + if (ret != 0) { + wolfBoot_printf("UBL: eNVM wolfBoot signature invalid\n"); + } + } +#ifdef MPFS_UBL_MIN_VERSION + if ((ret == 0) && + (wolfBoot_get_blob_version(manifest) < MPFS_UBL_MIN_VERSION)) { + wolfBoot_printf("UBL: eNVM wolfBoot version 0x%x below floor 0x%x\n", + (unsigned int)wolfBoot_get_blob_version(manifest), + (unsigned int)MPFS_UBL_MIN_VERSION); + ret = -1; + } +#endif + if ((ret != 0) || (img.signature_ok != 1) || (img.sha_ok != 1)) { + wolfBoot_printf("UBL: eNVM wolfBoot verify FAILED (%d)\n", ret); + wolfBoot_panic(); + } + + wolfBoot_printf("UBL: eNVM wolfBoot signature OK (%u ms), entering 0x%08lx\n", + (unsigned int)((rdcycle() - start) / + (MPFS_CPU_FREQ_RESET_MHZ * 1000U)), + (unsigned long)(uintptr_t)img.fw_base); + ubl_enter((uintptr_t)img.fw_base); +} diff --git a/hal/mpfs250_ubl.h b/hal/mpfs250_ubl.h new file mode 100644 index 0000000000..c73a43dcb7 --- /dev/null +++ b/hal/mpfs250_ubl.h @@ -0,0 +1,44 @@ +/* mpfs250_ubl.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* PolarFire SoC boot mode 2 User Boot Loader (UBL) stub. + * + * The System Controller releases all five harts at the stub entry. The + * E51 verifies the eNVM wolfBoot manifest; the U54s park in + * src/boot_riscv_start.S on a DTIM mailbox until the E51 publishes the + * verified entry. The mailbox sits at the top of the E51 DTIM, above + * everything the eNVM wolfBoot uses (hal/mpfs250.h). No UL suffixes: + * also used from assembly. */ + +#ifndef MPFS250_UBL_H +#define MPFS250_UBL_H + +/* DTIM content is undefined at power-on and the U54s may read the mailbox + * before the E51 has cleared it, so the entry is valid only with its + * complement and the magic beside it; the E51 writes the magic last. */ +#define MPFS_UBL_MAGIC_ADDR 0x01001F00 /* u32 MPFS_UBL_MAGIC */ +#define MPFS_UBL_ENTRY_ADDR 0x01001F08 /* u64 entry */ +#define MPFS_UBL_ENTRY_INV_ADDR 0x01001F10 /* u64 ~entry */ +#define MPFS_UBL_MAGIC 0x55424C21 /* "UBL!" */ +/* One 32-bit acknowledgement word per hart, written before it jumps. */ +#define MPFS_UBL_ACK_BASE 0x01001F20 + +#endif /* MPFS250_UBL_H */ diff --git a/hal/mpfs250_ubl.ld b/hal/mpfs250_ubl.ld new file mode 100644 index 0000000000..f52414e6e9 --- /dev/null +++ b/hal/mpfs250_ubl.ld @@ -0,0 +1,90 @@ +/* PolarFire SoC MPFS250 boot mode 2 User Boot Loader (UBL) linker script + * + * The stub lives in authenticated sNVM pages. The System Controller copies + * it to the LIM address in the UBL header (taken from this ELF) and releases + * all five harts at _reset, so VMA == LMA and nothing is relocated at start. + * + * Memory region: + * LIM - L2 Loosely Integrated Memory at 0x08000000. Only the ways not + * enabled as cache back it; at reset that is almost all of it. + */ + +OUTPUT_ARCH( "riscv" ) + +ENTRY( _reset ) + +MEMORY +{ + LIM (rwx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @BOOTLOADER_PARTITION_SIZE@ +} + +PROVIDE(STACK_SIZE = @STACK_SIZE@); + +/* sNVM holds 221 pages of 236 bytes for the UBL, 60 of which go to the UBL + * header the programmer prepends, so the image copied by the System + * Controller (.init + .text + .data, .bss is NOLOAD) is capped here. */ +MPFS_UBL_IMAGE_MAX = 221 * 236 - 60; + +ASSERT(ORIGIN(LIM) >= 0x08000000 && ORIGIN(LIM) + LENGTH(LIM) <= 0x081E0000, + "UBL must lie inside the LIM 0x08000000..0x081DFFFF") + +SECTIONS +{ + .init : ALIGN(0x10) + { + _start_text = .; + KEEP(*(.init)) + . = ALIGN(0x10); + } > LIM + + .text : ALIGN(0x10) + { + _start_vector = .; + KEEP(*(.isr_vector)) + KEEP(*(.trap_vector)) + . = ALIGN(0x10); + *(.text*) + *(.rodata*) + *(.srodata*) + . = ALIGN(8); + _end_text = .; + } > LIM + + .data : ALIGN(0x10) + { + _start_data = .; + *(.data*) + . = ALIGN(4); + _global_pointer = . + 0x800; + *(.sdata*) + . = ALIGN(4); + KEEP(*(.keystore*)) + . = ALIGN(8); + _end_data = .; + } > LIM + + .bss (NOLOAD) : ALIGN(0x10) + { + _start_bss = .; + *(.bss*) + *(.sbss*) + *(COMMON) + . = ALIGN(8); + _end_bss = .; + _end = .; + } > LIM +} + +/* Nothing is copied at start: the System Controller places the whole image. */ +_stored_data = ADDR(.data); + +PROVIDE(_start_heap = _end); +PROVIDE(_end_stack = ORIGIN(LIM) + LENGTH(LIM)); +PROVIDE(__global_pointer$ = _global_pointer); + +ASSERT(_start_text == ORIGIN(LIM), + "ERROR: UBL entry must be the first byte of the image") +ASSERT(_end_data - _start_text <= MPFS_UBL_IMAGE_MAX, + "ERROR: UBL image exceeds the sNVM budget (221 pages)") +ASSERT(_end <= _end_stack - STACK_SIZE, + "ERROR: UBL .bss overlaps the stack") diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 650cbb7032..0396994b32 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -24,6 +24,9 @@ #ifdef TARGET_mpfs250 #include "hal/mpfs250.h" #endif +#ifdef TARGET_mpfs250_ubl +#include "hal/mpfs250_ubl.h" +#endif /* RV64 M-mode: direct boot from eNVM; a0=hartid from CSR @@ -34,7 +37,76 @@ _reset: #if __riscv_xlen == 64 -#ifdef WOLFBOOT_RISCV_MMODE +#if defined(WOLFBOOT_RISCV_MMODE) && defined(TARGET_mpfs250_ubl) + /* Boot mode 2 UBL stub: the System Controller has already copied the + * whole image to the LIM and released all five harts here, so VMA == LMA + * and nothing is copied. The E51 verifies the eNVM wolfBoot manifest; + * the U54s park on the DTIM mailbox until it publishes the entry, then + * every hart enters the eNVM reset vector as in a boot mode 1 boot. */ + csrr a0, mhartid + mv tp, a0 + + li t0, MSTATUS_MIE + csrc mstatus, t0 + csrw mie, zero + csrw mip, zero + csrw mscratch, zero + csrw mcause, zero + csrw mepc, zero + csrw pmpcfg0, zero + csrw pmpcfg2, zero + + la t0, trap_vector_table + csrw mtvec, t0 + + bnez a0, .L_ubl_secondary_wait + + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + la sp, _end_stack + li t0, -16 + and sp, sp, t0 + mv s0, sp + + /* Clear .bss: the System Controller copies only the image bytes */ + la t0, _start_bss + la t1, _end_bss +.L_ubl_bss_clear: + beq t0, t1, .L_ubl_bss_clear_done + sd zero, 0(t0) + addi t0, t0, 8 + j .L_ubl_bss_clear +.L_ubl_bss_clear_done: + + mv a0, tp + j main + +.L_ubl_secondary_wait: + li t1, MPFS_UBL_MAGIC_ADDR + li t3, MPFS_UBL_MAGIC +.L_ubl_wait_entry: + lwu t2, 0(t1) + bne t2, t3, .L_ubl_wait_entry + ld t0, (MPFS_UBL_ENTRY_ADDR - MPFS_UBL_MAGIC_ADDR)(t1) + ld t2, (MPFS_UBL_ENTRY_INV_ADDR - MPFS_UBL_MAGIC_ADDR)(t1) + not t2, t2 + bne t0, t2, .L_ubl_wait_entry + beqz t0, .L_ubl_wait_entry + + /* Acknowledge, then jump; the E51 waits for the acks before it follows */ + li t1, MPFS_UBL_ACK_BASE + slli t2, a0, 2 + add t1, t1, t2 + li t2, 1 + sw t2, 0(t1) + fence iorw, iorw + fence.i + jr t0 + +#elif defined(WOLFBOOT_RISCV_MMODE) /* RV64 M-mode: first code from eNVM at 0x20220100 */ call .L_clear_ras /* flush stale RAS prediction state */ diff --git a/tools/scripts/mpfs_bootmode2.sh b/tools/scripts/mpfs_bootmode2.sh new file mode 100755 index 0000000000..7828d66b5e --- /dev/null +++ b/tools/scripts/mpfs_bootmode2.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# +# PolarFire SoC boot mode 2 programming helper. +# +# Two jobs mpfsBootmodeProgrammer cannot do on its own: +# +# mpfs_bootmode2.sh [options] wolfboot.elf +# Program the User Boot Loader (the TARGET=mpfs250_ubl build) into sNVM +# and select boot mode 2 on a die the programmer does not list (the +# secure "S" parts, e.g. MPFS250TS, which an authenticated sNVM client +# needs). The programmer is run in --dryrun to produce the UBL image +# (its 60-byte header, SHA-256 and Intel hex) and the USK; the fpgenprog +# steps it logged are replayed in a project for the requested die. +# +# mpfs_bootmode2.sh --envm [options] wolfboot_envm_signed.elf +# Program the eNVM only (the signed wolfBoot and its manifest) and leave +# the boot mode and sNVM alone, i.e. update the eNVM wolfBoot under boot +# mode 2. The programmer always selects boot mode 1 when it programs +# eNVM, so its envm_client step is replayed without the boot-mode step. +# +# Boot mode 1 is restored with the programmer itself: +# java -jar mpfsBootmodeProgrammer.jar --bootmode 1 --die MPFS250T ... wolfboot.elf +# +# Options: +# --die DIE fpgenprog die (default MPFS250TS) +# --package PKG package (default FCG1152) +# --snvm-page N first sNVM page of the UBL (default 0) +# --usk KEY 12-byte hex USK, or 0 for a random one (default 0) +# --encrypt store the UBL as authenticated ciphertext +# --regenerate-smk also generate a new sNVM Master Key. The authenticated +# sNVM client the UBL lives in cannot be created on a device +# whose SMK has never been initialized, and the programmer +# never asks for one. A new SMK invalidates every existing +# authenticated or encrypted sNVM client on the device. +# --workdir DIR output directory (default: current directory) +# --dryrun generate everything, do not program +# +# Requires SC_INSTALL_DIR (SoftConsole) and FPGENPROG (Libero fpgenprog). + +set -e + +DIE=MPFS250TS +PACKAGE=FCG1152 +SNVM_PAGE=0 +USK=0 +ENCRYPT="" +SMK=0 +WORKDIR="$PWD" +DRYRUN=0 +ENVM=0 + +while [ $# -gt 1 ]; do + case "$1" in + --die) DIE="$2"; shift 2 ;; + --package) PACKAGE="$2"; shift 2 ;; + --snvm-page) SNVM_PAGE="$2"; shift 2 ;; + --usk) USK="$2"; shift 2 ;; + --encrypt) ENCRYPT="--encrypt"; shift ;; + --regenerate-smk) SMK=1; shift ;; + --workdir) WORKDIR="$2"; shift 2 ;; + --dryrun) DRYRUN=1; shift ;; + --envm) ENVM=1; shift ;; + *) echo "unknown option: $1" >&2; exit 1 ;; + esac +done +ELF="${1:-}" +[ -n "$ELF" ] && [ -f "$ELF" ] || { echo "usage: $0 [--envm] [options] wolfboot.elf" >&2; exit 1; } +[ -n "${SC_INSTALL_DIR:-}" ] || { echo "SC_INSTALL_DIR is not set" >&2; exit 1; } +[ -x "${FPGENPROG:-}" ] || { echo "FPGENPROG is not an executable" >&2; exit 1; } + +JAVA="$SC_INSTALL_DIR/eclipse/jre/bin/java" +JAR="$SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar" +# The programmer knows the non-secure die names only; it is just generating +# the image here, so hand it the base die. +JAR_DIE="${DIE%S}" +mkdir -p "$WORKDIR" +WORKDIR="$(cd "$WORKDIR" && pwd)" +NAME="$(basename "$ELF" .elf)" +# The programmer takes the ELF relative to its working directory. +cmp -s "$ELF" "$WORKDIR/$NAME.elf" 2>/dev/null || cp "$ELF" "$WORKDIR/$NAME.elf" + +if [ "$ENVM" = 1 ]; then + MODE=1 + STEP=envm_client + GEN="$WORKDIR/bootmode1" + JAR_ARGS="--bootmode 1" +else + MODE=2 + STEP=snvm_client + GEN="$WORKDIR/bootmode2" + JAR_ARGS="--bootmode 2 --snvm_page $SNVM_PAGE --usk $USK $ENCRYPT" +fi +PROJ="$WORKDIR/bootmode${MODE}_$DIE/fpgenprogProject" +LOG="$GEN/debugLog.txt" + +echo "[1/3] generating the image with mpfsBootmodeProgrammer (dry run)" +# Boot mode 2 on a non-secure die fails at bitstream generation; the image +# and the USK are already written by then. +# shellcheck disable=SC2086 +"$JAVA" -jar "$JAR" $JAR_ARGS --die "$JAR_DIE" --package "$PACKAGE" --dryrun \ + --workdir "$WORKDIR" "$NAME.elf" > "$WORKDIR/bootmode${MODE}_jar.log" 2>&1 || true +[ -f "$LOG" ] && grep -q "fpgenprog $STEP " "$LOG" || { + echo "mpfsBootmodeProgrammer produced no image; see $WORKDIR/bootmode${MODE}_jar.log" >&2 + exit 1 +} + +# The exact arguments the programmer used for a step, pointed at the new project. +cmd_args() { + grep -o "fpgenprog $1 [^\"]*" "$LOG" | tail -1 | sed "s/^fpgenprog $1 //" | \ + sed "s|--location [^ ]*|--location $PROJ|" +} +CLIENT_ARGS="$(cmd_args $STEP)" +if [ "$SMK" = 1 ]; then + [ "$MODE" = 2 ] || { echo "--regenerate-smk applies to boot mode 2 only" >&2; exit 1; } + echo "note: generating a new sNVM Master Key; authenticated sNVM clients" + echo " already on this device will no longer authenticate" + CLIENT_ARGS="$CLIENT_ARGS --regenerate_smk" +fi +BOOT_INFO_ARGS="" +[ "$MODE" = 2 ] && BOOT_INFO_ARGS="$(cmd_args mss_boot_info)" + +echo "[2/3] generating the bitstream for $DIE" +rm -rf "$PROJ" +mkdir -p "$(dirname "$PROJ")" +"$FPGENPROG" new_project --location "$PROJ" --target_die "$DIE" --target_package "$PACKAGE" +if [ -n "$BOOT_INFO_ARGS" ]; then + # shellcheck disable=SC2086 + "$FPGENPROG" mss_boot_info $BOOT_INFO_ARGS +fi +# shellcheck disable=SC2086 +"$FPGENPROG" $STEP $CLIENT_ARGS +"$FPGENPROG" generate_bitstream --location "$PROJ" + +if [ "$MODE" = 2 ]; then + USKFILE="$(ls "$GEN"/"$NAME"-bm2-usk.txt 2>/dev/null | head -1)" + [ -n "$USKFILE" ] && echo "USK (keep it: a UBL update needs it): $USKFILE" +fi +if [ "$DRYRUN" = 1 ]; then + echo "[3/3] dry run: not programming" + exit 0 +fi +echo "[3/3] programming" +"$FPGENPROG" run_action --location "$PROJ" --action PROGRAM +if [ "$MODE" = 2 ]; then + echo "done: boot mode 2, UBL in sNVM from page $SNVM_PAGE" +else + echo "done: eNVM programmed, boot mode unchanged" +fi From 8113ad2cf1900e22581191ea0d27a6dfaff48660 Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 9 Oct 2026 08:29:29 -0700 Subject: [PATCH 8/9] PolarFire SoC: QSPI clock divider knob and watchdog hand-off to the OS --- docs/Targets.md | 16 +++++++++++++++- hal/mpfs250.c | 16 ++++++++++++---- hal/mpfs250.h | 30 ++++++++++++++++++++++++++---- options.mk | 13 +++++++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/docs/Targets.md b/docs/Targets.md index 794c5485e2..711a58fec7 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -961,6 +961,8 @@ These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS |------|---------|-------------| | `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | | `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | +| `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | +| `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | #### Stack overflow detection @@ -1087,6 +1089,14 @@ Example single-shot build: `cp config/examples/polarfire_mpfs250_qspi.config .co Notes: - Both modes support full read, write, and erase operations. +- SCK is the controller clock divided by the CoreQSPI `CLKRATE` field, SCK = PCLK / (2 * CLKRATE). + `MPFS_QSPI_CLK_DIV` selects the divider by its `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` + (`QSPI_CLK_DIV_2` through `QSPI_CLK_DIV_30`, CLKRATE 1..15). The default `QSPI_CLK_DIV_8` is + about 18.75 MHz from the 150 MHz APB clock, where read throughput stops improving: from there on + the polled FIFO transfer rather than SCK sets the rate, so a faster SCK buys nothing. The MSS QSPI + PCLK is the MSS APB clock, which is 40 MHz at the M-mode reset clock until wolfBoot's PLL + bring-up, so an M-mode build that starts QSPI before `mss_pll_init()` runs SCK that much slower; + the SC QSPI is clocked by the System Controller and is unaffected by the MSS PLL. - For QSPI-based boot flows, disable SD/eMMC in the config (`DISK_SDCARD=0`, `DISK_EMMC=0`) unless you explicitly want wolfBoot to load from disk and the application from QSPI. - The MSS QSPI path expects external flash on the MSS QSPI pins; the SC QSPI path is for @@ -1305,7 +1315,11 @@ MAC addresses from the device serial number, and all five MSS watchdog nodes are **Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via -the dtb fixup so the two never conflict. +the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS +instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms +them at probe and something on the OS side (a watchdog daemon, or the design's own fabric +scheme) has to service them from then on. A design with its own fabric watchdog needs neither +setting; wolfBoot never touches fabric logic. **Hand-off / SMP flow:** secondary harts park in eNVM until the E51 signals image-copy completion (DTIM gate flag), then park in a WFI loop. The boot hart is released with a staged diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 5e9c607bd6..3589b8545c 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -323,11 +323,13 @@ int sbi_hal_hart_start(unsigned long hartid, unsigned long saddr, void __attribute__((noreturn)) hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) { +#ifndef MPFS_WDT_NODES_KEEP static const unsigned long park_wdt_bases[5] = { MSS_WDT_E51_BASE, MSS_WDT_U54_1_BASE, MSS_WDT_U54_2_BASE, MSS_WDT_U54_3_BASE, MSS_WDT_U54_4_BASE }; unsigned int w; +#endif (void)hartid; /* the calling E51 hart is not the kernel boot hart */ @@ -378,9 +380,11 @@ hal_smode_boot(unsigned long entry, unsigned long hartid, unsigned long dtb) * it). With MVRP at maximum a refresh is always permitted. */ __asm__ volatile("csrw mie, %0" :: "r"(0x80UL)); /* MTIE */ while (1) { +#ifndef MPFS_WDT_NODES_KEEP for (w = 0; w < 5U; w++) { MSS_WDT_REFRESH(park_wdt_bases[w]) = 0xDEADC0DEU; } +#endif CLINT_MTIMECMP_REG(MPFS_FIRST_HART) = CLINT_MTIME_REG + (5UL * RTC_CLOCK_FREQ); __asm__ volatile("wfi"); @@ -1799,7 +1803,8 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) int off, ret; uint8_t device_serial_number[DEVICE_SERIAL_NUMBER_SIZE]; uint8_t mac_addr[6]; -#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ + (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) /* Nodes disabled below. Watchdogs: the MSS WDTs always count; the OS * driver arms them at probe but nothing pings them (no userspace * watchdog daemon in the default image), so the system would reset @@ -1807,8 +1812,10 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) * status and the parked E51 monitor keeps them refreshed. Re-enable * when an OS-side petting story exists. */ static const char *const cpu_off[] = { +#ifndef MPFS_WDT_NODES_KEEP "watchdog@20001000", "watchdog@20101000", "watchdog@20103000", "watchdog@20105000", "watchdog@20107000", +#endif #ifdef MPFS_SCB_SMODE_DENY /* PMP fences S-mode off the System Controller mailbox: the OS * drivers for it would only fault. */ @@ -1845,7 +1852,8 @@ static int mpfs_dts_fixup_inplace(void* dts_addr, uint32_t capacity) (void)wolfBoot_disk_dts_fixup(&ctx); #endif -#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) +#if defined(MPFS_DDR_INIT) && defined(WOLFBOOT_MMODE_SMODE_BOOT) && \ + (!defined(MPFS_WDT_NODES_KEEP) || defined(MPFS_SCB_SMODE_DENY)) /* Disable the MSS watchdog dtb nodes BEFORE the serial-number read: this * does not depend on the serial number and MUST run even if the SC serial * read below fails. Otherwise the OS watchdog driver would arm them at @@ -2099,13 +2107,13 @@ int qspi_init(void) QSPI_IEN = 0; /* Configure QSPI Control Register: - * - Clock divider for ~5MHz (conservative) + * - Clock divider (MPFS_QSPI_CLK_DIV, ~5 MHz by default) * - CPOL=1 (clock idle high) for SPI Mode 3 * - Sample on SCK edge * - Enable controller */ QSPI_CONTROL = - (QSPI_CLK_DIV_30 << QSPI_CTRL_CLKRATE_OFFSET) | + (MPFS_QSPI_CLK_DIV << QSPI_CTRL_CLKRATE_OFFSET) | QSPI_CTRL_CLKIDLE | QSPI_CTRL_SAMPLE_SCK | QSPI_CTRL_EN; diff --git a/hal/mpfs250.h b/hal/mpfs250.h index 44e5b4a8c4..65198a89c3 100644 --- a/hal/mpfs250.h +++ b/hal/mpfs250.h @@ -784,8 +784,10 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); /* QSPI Flash Controller * * Two CoreQSPI v2 controllers with identical register layouts: - * SC QSPI (MPFS_SC_SPI=1, default): 0x37020100 -- fabric-connected flash - * MSS QSPI (MPFS_SC_SPI=0): 0x21000000 -- MSS QSPI pins + * SC QSPI (-DMPFS_SC_SPI, every shipped QSPI config): 0x37020100, + * the System Controller's flash + * MSS QSPI (MPFS_SC_SPI undefined): 0x21000000, + * the MSS QSPI pins */ /* QSPI Controller Base Address */ @@ -846,14 +848,34 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #define QSPI_STATUS_TXAVAIL (1u << 3) #define QSPI_STATUS_READY (1u << 7) -/* QSPI Clock Configuration */ +/* QSPI Clock Configuration: SCK = PCLK / (2 * CLKRATE). The MSS QSPI PCLK + * is the APB clock, 150 MHz under HSS and 40 MHz at the M-mode reset clock + * (MSS_APB_AHB_CLK) until mss_pll_init() raises it; the SC QSPI has the + * System Controller's own clock. MPFS_QSPI_CLK_DIV picks the divider. */ #define QSPI_CLK_DIV_2 0x01u #define QSPI_CLK_DIV_4 0x02u #define QSPI_CLK_DIV_6 0x03u -#define QSPI_CLK_DIV_8 0x04u +#define QSPI_CLK_DIV_8 0x04u /* ~18.75 MHz from 150 MHz APB */ #define QSPI_CLK_DIV_10 0x05u #define QSPI_CLK_DIV_12 0x06u +#define QSPI_CLK_DIV_14 0x07u +#define QSPI_CLK_DIV_16 0x08u +#define QSPI_CLK_DIV_18 0x09u +#define QSPI_CLK_DIV_20 0x0Au +#define QSPI_CLK_DIV_22 0x0Bu +#define QSPI_CLK_DIV_24 0x0Cu +#define QSPI_CLK_DIV_26 0x0Du +#define QSPI_CLK_DIV_28 0x0Eu #define QSPI_CLK_DIV_30 0x0Fu /* ~5 MHz from 150 MHz APB */ +/* Divide by 8 is where read throughput stops improving: the polled FIFO + * transfer, not SCK, is the limit from there on. Anything slower only costs + * time, anything faster buys nothing. */ +#ifndef MPFS_QSPI_CLK_DIV +#define MPFS_QSPI_CLK_DIV QSPI_CLK_DIV_8 +#endif +#if (MPFS_QSPI_CLK_DIV < 0x01u) || (MPFS_QSPI_CLK_DIV > 0x0Fu) +#error MPFS_QSPI_CLK_DIV must be a CLKRATE of 1..15 (QSPI_CLK_DIV_2..QSPI_CLK_DIV_30) +#endif /* Micron MT25QL01G Flash Commands */ #define QSPI_CMD_READ_ID_OPCODE 0x9Fu /* JEDEC ID Read */ diff --git a/options.mk b/options.mk index 4ed9d19dbb..187dc86f89 100644 --- a/options.mk +++ b/options.mk @@ -184,6 +184,19 @@ ifneq ($(DISK_BOOT_TRIES),) CFLAGS+=-DDISK_BOOT_TRIES=$(DISK_BOOT_TRIES) endif +# PolarFire SoC: QSPI SCK divider (a QSPI_CLK_DIV_* name from hal/mpfs250.h, +# default QSPI_CLK_DIV_8). +ifneq ($(MPFS_QSPI_CLK_DIV),) + CFLAGS+=-DMPFS_QSPI_CLK_DIV=$(MPFS_QSPI_CLK_DIV) +endif + +# PolarFire SoC: leave the MSS watchdog nodes enabled in the dtb and stop the +# parked E51 from refreshing them, so the OS owns the watchdogs after the +# hand-off. Something on the OS side must then service them. +ifeq ($(MPFS_WDT_NODES_KEEP),1) + CFLAGS+=-D"MPFS_WDT_NODES_KEEP" +endif + # PolarFire SoC: PUF-derived KEK + RFC 3394 AES key-wrap for sNVM-stored keys. ifeq ($(SNVM_KEK),1) # The KEK only ever wraps the image-encryption key, so it rides on the From 909c74bd7bd35cbc1539b0255bbdd32d474d2941 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 14 Oct 2026 14:18:31 -0700 Subject: [PATCH 9/9] PolarFire SoC: consolidate the documentation into docs/PolarFire.md with a quick start --- config/examples/polarfire_mpfs250_m.config | 2 +- docs/PolarFire.md | 1438 ++++++++++++++++++++ docs/README.md | 1 + docs/Targets.md | 1079 +-------------- docs/encrypted_partitions.md | 2 +- docs/keystore.md | 2 +- docs/polarfire_sbi_vendor.md | 31 - docs/polarfire_snvm_puf.md | 153 --- hal/mpfs250.c | 2 +- 9 files changed, 1472 insertions(+), 1238 deletions(-) create mode 100644 docs/PolarFire.md delete mode 100644 docs/polarfire_sbi_vendor.md delete mode 100644 docs/polarfire_snvm_puf.md diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index faf4e143cb..33c26eb329 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -116,7 +116,7 @@ WOLFBOOT_RAMBOOT_MAX_SIZE=0x08000000 # Use update_disk loader (partition A/B numbering instead of flash addresses). # BOOT_PART_A / BOOT_PART_B are 0-indexed GPT entry numbers. GPT partitions -# in our SD card layout (see tools/scripts/program-sdcard.sh): +# in the SD card layout of docs/PolarFire.md: # index 0 (parted "boot" 1 MiB - 33 MiB) -> active boot FIT # index 1 (parted "update" 33 MiB - 65 MiB) -> inactive/update slot # index 2 (parted "rootfs" 65 MiB - end) -> Linux rootfs diff --git a/docs/PolarFire.md b/docs/PolarFire.md new file mode 100644 index 0000000000..b51bb450d9 --- /dev/null +++ b/docs/PolarFire.md @@ -0,0 +1,1438 @@ +# Microchip PolarFire SoC + +Everything wolfBoot does on PolarFire SoC, in one document: the quick start, every supported boot configuration, and the device-specific features (sNVM/PUF key storage, the SBI vendor extension, boot mode 2). The PolarFire SoC is a 64-bit RISC-V SoC with a five-core CPU cluster (1x E51 monitor core and 4x U54 application cores) and FPGA fabric; wolfBoot is tested on MPFS250. + +wolfBoot runs here either as a payload of Microchip's Hart Software Services or as a full replacement for it, in which case it owns LPDDR4 bring-up, image verification and the SBI the OS runs on. [Targets.md](Targets.md#microchip-polarfire-soc) carries the summary tables; this document is the detail behind them. + +> **Filesystem boot:** this target can also read its signed image from a **file** on a read-only FAT32 or ext4 partition instead of from raw offset 0. Build with `DISK_FS=fat32|ext4|both` and set `BOOT_FILE_A` / `BOOT_FILE_B`. See [Disk boot from a read-only filesystem (FAT32 / ext4)](compile.md#disk-boot-from-a-read-only-filesystem-fat32--ext4). + +## Contents + +- [Quick start](#quick-start) +- [Features](#features) +- [Boot configurations](#boot-configurations) +- [M-mode build flags](#m-mode-build-flags) +- [Building wolfBoot](#building-wolfboot) +- [QSPI](#qspi) +- [HSS S-mode with L2-LIM (no DDR)](#hss-s-mode-with-l2-lim-no-ddr) +- [M-mode (bare-metal eNVM boot)](#m-mode-bare-metal-envm-boot) +- [M-mode + DDR: booting Linux (minimal SBI)](#m-mode--ddr-booting-linux-minimal-sbi) +- [Boot mode 2: User Boot Loader stub](#boot-mode-2-user-boot-loader-stub) +- [Encryption](#encryption) +- [PQC (ML-DSA)](#pqc-ml-dsa) +- [Writing a card](#writing-a-card) +- [Building Hart Software Services (HSS)](#building-hart-software-services-hss) +- [Building Yocto-SDK Linux](#building-yocto-sdk-linux) +- [Debugging](#debugging) +- [Example boot output](#example-boot-output) +- [Performance comparison](#performance-comparison) +- [Benchmarks](#benchmarks) +- [Source files](#source-files) +- [SBI vendor extension for the OS](#sbi-vendor-extension-for-the-os) +- [Hardware root of trust: PUF KEK, sNVM keystore, wrapped image key](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key) + +## Quick start + +Three ways to get wolfBoot running on an MPFS250T board, shortest first. Each path is self-contained: build, program, and what the console must print for the step to have worked. The sections after this one are the reference material behind them. + +| Path | What it proves | eNVM | Reversible by | +|---|---|---|---| +| [1. HSS payload](#path-1-run-wolfboot-as-an-hss-payload) | wolfBoot verifies and boots an image on this board | untouched (HSS stays) | swapping the SD card | +| [2. wolfBoot replaces HSS](#path-2-wolfboot-replaces-hss-and-boots-linux) | the full configuration: M-mode, LPDDR4, signed FIT, Linux | reprogrammed | reprogramming HSS | +| [3. Yocto](#path-3-let-yocto-build-it) | the same as path 2, built and packaged by bitbake | reprogrammed | reprogramming HSS | + +Path 1 needs no FPGA design files and never writes the device, so it is the right first contact with an unfamiliar board. Path 2 is the configuration the target is built for. + +### Before you start + +| Need | Where it comes from | +|---|---| +| RISC-V bare-metal toolchain | SoftConsole (`/opt/Microchip/SoftConsole-v2022.2-RISC-V-747/riscv-unknown-elf-gcc/bin`), or any `riscv64-unknown-elf-gcc` | +| `mpfsBootmodeProgrammer.jar` + a JRE | SoftConsole (`extras/mpfs/`, `eclipse/jre/bin/java`); paths 2 and 3 only | +| FlashPro programmer | on-board on the Video Kit, over the same USB cable as the consoles | +| `dtc`, `mkimage` | `device-tree-compiler`, `u-boot-tools` | +| `hss-payload-generator` | `tools/hss-payload-generator` in the [hart-software-services](https://github.com/polarfire-soc/hart-software-services) tree; path 1 only | +| `fpga_design_config/` for the board | an HSS build (`build/boards//fpga_design_config`) or a Libero MSS Configurator export; paths 2 and 3 only | + +```sh +git clone --recursive https://github.com/wolfSSL/wolfBoot.git +cd wolfBoot +export PATH=/opt/Microchip/SoftConsole-v2022.2-RISC-V-747/riscv-unknown-elf-gcc/bin:$PATH +``` + +Two things that cause most first-attempt failures: + +* **Keys.** The first build generates `wolfboot_signing_private_key.der` and compiles its public half into `src/keystore.c`, so the bootloader only accepts images signed with that key. Back the private key up before anything else; `make keysclean` discards both halves and the next build makes a new pair, which invalidates every image already signed. +* **`make clean` between paths.** Object files are configuration-specific. Reusing them across configs produces link errors naming symbols that are compiled out (`undefined reference to wolfBoot_get_dts_size`) or, worse, a bootloader built half from each config. + +Console: the E51 prints on MMUART0, the first of the four USB-serial interfaces the board exposes, 115200 8N1. + +### Path 1: run wolfBoot as an HSS payload + +HSS keeps the eNVM and loads wolfBoot as its S-mode payload from the SD card, so nothing about the device changes and the original card restores the board exactly. + +```sh +cp config/examples/polarfire_mpfs250.config .config +make clean +make +dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb +hss-payload-generator -c hal/mpfs.yaml wolfboot-payload.bin +``` + +`make` produces `wolfboot.elf`, the raw `wolfboot.bin`, a signed test application and `factory.bin`. The payload generator reads `wolfboot.elf` and `hal/mpfs.dtb` as named in `hal/mpfs.yaml`, so build the dtb first; the output name is yours to choose (about 63 KB for a 61 KB wolfBoot). Do not call it `wolfboot.bin` or it overwrites the raw binary. + +Write `wolfboot-payload.bin` to the card's HSS boot partition - the GPT entry with type GUID `21686148-6449-6E6F-744E-656564454649`, or sector 0 on a card with no GPT. With the card in the board, HSS's `usbdmsc` command exports it over USB; `lsblk` then names the partition: + +```sh +sudo dd if=wolfboot-payload.bin of=/dev/sdX1 bs=512 +sudo cmp wolfboot-payload.bin /dev/sdX1 +``` + +Power-cycle. HSS prints its own banner, then wolfBoot's: + +``` +wolfBoot Version: ... +``` + +That line is the whole point of the path: the board runs wolfBoot. Signing an image and putting it in a slot for wolfBoot to verify follows the same steps as path 2. + +### Path 2: wolfBoot replaces HSS and boots Linux + +wolfBoot runs from eNVM on the E51, brings up the PLLs and LPDDR4, loads a signed FIT (kernel + dtb) from the SD card, verifies it with ECDSA P-384 over SHA-384, and hands a U54 into S-mode while staying resident as the SBI. + +#### Build + +```sh +cp config/examples/polarfire_mpfs250_m.config .config +make clean +make LIBERO_FPGA_CONFIG_DIR=/path/to/boards/mpfs-video-kit/fpga_design_config wolfboot.elf +``` + +About 74 KB of text. The build stops if `LIBERO_FPGA_CONFIG_DIR` is empty: an S-mode OS runs from DDR, and those headers carry the board's DDR, clock and SEG settings, which cannot be guessed. Add `CFLAGS_EXTRA+=-DDEBUG_DDR` for register-level training traces during bring-up. + +#### Card + +Three GPT partitions: the active boot slot, the update slot, and the rootfs. GPT entries 0, 1 and 2 are what `BOOT_PART_A`, `BOOT_PART_B` and the kernel's root device refer to. + +```sh +sudo parted -s /dev/sdX mklabel gpt \ + mkpart boot 1MiB 33MiB \ + mkpart update 33MiB 65MiB \ + mkpart rootfs 65MiB 100% +``` + +Sign the FIT the Linux build produced, then write both: + +```sh +./tools/keytools/sign --ecc384 --sha384 fitImage wolfboot_signing_private_key.der 1 +sudo dd if=fitImage_v1_signed.bin of=/dev/sdX1 bs=1M status=progress +sudo dd if=core-image-...rootfs.ext4 of=/dev/sdX3 bs=4M status=progress +sync +``` + +The signed file is the input plus a 512-byte manifest header. Slot sizes above suit a 19 MB kernel FIT; grow them for a larger image, or for a FIT that carries the whole rootfs as an initramfs. For where the Linux FIT itself comes from, see [Building Yocto-SDK Linux](#building-yocto-sdk-linux). + +#### Program the eNVM + +Keep a copy of what you are about to replace - see "Restoring HSS" below - and do a `--dryrun` pass first. + +```sh +export SC_INSTALL_DIR=/opt/Microchip/SoftConsole-v2022.2-RISC-V-747 + +$SC_INSTALL_DIR/eclipse/jre/bin/java -jar \ + $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD wolfboot.elf +``` + +`--bootmode 1` is non-secure boot from eNVM: the System Controller releases the harts at the eNVM reset vector. The die and package must match the part; `--die` defaults to `MPFS250T_ES` and `--package` to `FCVG484`, neither of which is the Video Kit. + +#### Boot + +``` +wolfBoot Version: ... +Running on E51 (hart 0) in M-mode +DDR: Training+MTC PASS +DDR: Initialization COMPLETE +Valid GPT partition table +Versions, A:1 B:0 +Checking image integrity... +Verifying image signature... +Firmware Valid. +M->S handoff: entry=0x80200000 hart=0 dtb=0x8A000000 +[ 0.000000] Linux version ... +[ 0.000000] SBI implementation ID=0x776f6c66 Version=0x1 +... +login: +``` + +Four lines are the ones to check. `DDR: Initialization COMPLETE` means the memory came up; `Firmware Valid.` means the signature verified against the keystore in the image; `M->S handoff` means wolfBoot released a U54; and `SBI implementation ID=0x776f6c66` - ASCII `"wolf"` - means the kernel is talking to wolfBoot's own SBI rather than OpenSBI. The full annotated log is in [Example boot output](#example-boot-output). + +### Path 3: let Yocto build it + +The `meta-wolfssl` layer builds the bootloader, signs the FIT with the same keytools and writes a `.wic` with the partition layout above. Set `WOLFBOOT_CONFIG = "polarfire_mpfs250_m.config"`, point `WOLFBOOT_EXTRA_MAKE_FLAGS` at the `fpga_design_config` directory and supply a pre-generated ECC384 key through `WOLFBOOT_SIGNING_KEY`; the recipes deliberately never generate one. The PolarFire section of `recipes-wolfssl/wolfboot/README.md` in that layer has the full `local.conf` block. The eNVM programming step is still the `mpfsBootmodeProgrammer` command from path 2. + +### Restoring HSS + +Programming the eNVM is not one-way, and the boot mode does not change: both images are boot mode 1. + +```sh +$SC_INSTALL_DIR/eclipse/jre/bin/java -jar \ + $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD hss-envm-wrapper.elf +``` + +`hss-envm-wrapper.elf` comes from an HSS build for the board (`make BOARD=mpfs-video-kit`, output in `build/`). Build it before you need it. + +### Secure dies + +The programmer's `--die` list covers `MPFS025T` through `MPFS460T` and their `_ES` variants only - a secure ("S") die is not in it. For those parts `tools/scripts/mpfs_bootmode2.sh` drives Libero's `fpgenprog` directly: `--bootmode 1` for the eNVM boot above, `--bootmode 2` to put a signed User Boot Loader in sNVM ahead of it. See [Boot mode 2: User Boot Loader stub](#boot-mode-2-user-boot-loader-stub). + +### When it does not work + +| Symptom | Cause | +|---|---| +| `undefined reference to ...` after switching config | stale objects; `make clean` and rebuild | +| no output at all | wrong serial interface - the E51 is on MMUART0, the first of the four USB-serial interfaces | +| `Invalid elf, falling back to raw binary` | normal: a FIT is not an ELF, and wolfBoot says so before parsing it as a FIT | +| `SD Card: Card init failed (-1)` | the card was not ready: re-seat it and power-cycle. The board routes card-detect through the fabric, so this build assumes a card is present (`SDHCI_FORCE_CARD_DETECT`) and cannot report its absence | +| `Firmware Invalid` or a version of 0 | the image was signed with a key whose public half is not the one in `src/keystore.c`; re-sign, or rebuild the bootloader with the matching key | +| banner, then silence | the FIT loaded but Linux has no console: the dtb's own `/chosen/bootargs` wins unless `LINUX_BOOTARGS` is set at build time. See [bootargs](compile.md#linux-kernel-command-line-bootargs) | +| DDR training stops or reports a failure | build with `CFLAGS_EXTRA+=-DDEBUG_DDR` and compare against the board's Libero settings | + +### Next + +* [All seven boot configurations](#boot-configurations) - SD, eMMC, QSPI, L2-LIM with no DDR, M-mode with and without DDR, and the boot mode 2 stub. +* [Build flags](#m-mode-build-flags) - watchdog policy, QSPI clock divider, stack-overflow detection. +* [Hardware root of trust](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key) - keystore in sNVM, PUF-derived KEK, wrapped image-encryption key. +* [SBI vendor extension](#sbi-vendor-extension-for-the-os) - device serial, PUF-derived keys and System Controller services for the OS. + +## Features + +* RISC-V 64-bit: E51 monitor core `rv64imac` (no FPU), U54 application cores `rv64gc` +* Five-core CPU: 1x E51 monitor + 4x U54 application cores +* Integrated DDR3/4, LPDDR3/4 controller and PHY +* eNVM, L2 scratchpad and LIM on-chip memories, any of which wolfBoot can run from +* System Controller with secure NVM, an SRAM-PUF and the TeraFire (Athena F5200) crypto engine +* MSS and System Controller QSPI controllers, SD/eMMC + +## Boot configurations + +Seven ready-to-use config templates cover all supported boot mode / storage / memory combinations: + +| Configuration | Config File | Boot Mode | Storage | Memory | HSS | +|---------------|-------------|-----------|---------|--------|-----| +| **SDCard** | `polarfire_mpfs250.config` | S-mode (U54 via HSS) | SD Card | DDR | Yes | +| **eMMC** | `polarfire_mpfs250.config` + `DISK_EMMC=1` | S-mode (U54 via HSS) | eMMC | DDR | Yes | +| **QSPI (S-mode)** | `polarfire_mpfs250_qspi.config` | S-mode (U54 via HSS) | MSS or SC QSPI | DDR | Yes | +| **QSPI + L2-LIM** | `polarfire_mpfs250_hss_l2lim.config` | S-mode (U54 via HSS) | SC QSPI | L2-LIM (no DDR) | Yes | +| **M-Mode (no HSS)** | `polarfire_mpfs250_m_qspi.config` | M-mode (E51, no HSS) | SC QSPI | L2 Scratchpad | No | +| **M-Mode + DDR** | `polarfire_mpfs250_m.config` | M-mode (E51, no HSS) | SD Card | LPDDR4 (DDR) | No | +| **Boot mode 2 UBL** | `polarfire_mpfs250_ubl.config` | System Controller -> sNVM stub | eNVM (verifies it) | L2-LIM | No | + +The **M-Mode + DDR** configuration brings up the LPDDR4 controller from +the E51 in M-mode (no HSS), then loads a signed FIT image from SD card, +verifies it (SHA384 + ECC384) and hands off to a U54 hart in S-mode. +wolfBoot includes a minimal SBI runtime (`src/riscv_sbi.c`) so the +hand-off target can be a Linux kernel: tested booting 4-CPU SMP Yocto +Linux to a login prompt in ~40 s from power-on on the MPFS250T Video +Kit. Because all +LIBERO_SETTING_\* values are board-specific, this build pulls them from +a Libero/HSS-generated `fpga_design_config.h` pointed at by the +`LIBERO_FPGA_CONFIG_DIR` makefile variable - typical sources are an +HSS Video Kit build at +`/build/boards/mpfs-video-kit/fpga_design_config` or a Libero MSS +Configurator export. Setting `LIBERO_FPGA_CONFIG_DIR` automatically +defines `MPFS_DDR_INIT` and adds the directory to the include path +(see `arch.mk`); when unset, the DDR HAL is excluded and the build +still produces a working M-mode wolfBoot without DDR. Add +`-DDEBUG_DDR` to `CFLAGS_EXTRA` for verbose register-level traces +during bring-up. + +For GitHub Actions, `tools/ci/gen_mpfs_libero_stub.sh` generates a +compile-only stub (every `LIBERO_SETTING_*` symbol referenced by the +HAL, defined to `0`; loop-bound settings to `1U`) so the M-Mode + DDR +build path stays under continuous integration. The generated header is +**not** committed and **not** runnable - real boards must point +`LIBERO_FPGA_CONFIG_DIR` at the actual Libero / HSS output. + +Key build settings that differ between configurations: + +| Setting | SDCard | eMMC | QSPI | L2-LIM | M-Mode | M-Mode + DDR | +|---------|--------|------|------|--------|--------|--------------| +| `WOLFBOOT_ORIGIN` | `0x80000000` | `0x80000000` | `0x80000000` | `0x08040000` | `0x0A000000` | `0x0A000000` | +| `WOLFBOOT_LOAD_ADDRESS` | `0x8E000000` | `0x8E000000` | `0x8E000000` | `0x08060000` | `0x0A010200` | `0x82000000` | +| `WOLFBOOT_LOAD_DTS_ADDRESS` | `0x8A000000` | `0x8A000000` | `0x8A000000` | - | - | `0x8A000000` | +| `EXT_FLASH` | 0 | 0 | 1 | 1 | 1 | 0 | +| `DISK_SDCARD` | 1 | 0 | 0 | 0 | 0 | 1 | +| `DISK_EMMC` | 0 | 1 | 0 | 0 | 0 | 0 | +| `MPFS_L2LIM` | - | - | - | 1 | - | - | +| `RISCV_MMODE` | - | - | - | - | 1 | 1 | +| `LIBERO_FPGA_CONFIG_DIR` | - | - | - | - | - | required | +| `WOLFBOOT_MMODE_SMODE_BOOT` | - | - | - | - | - | 1 | +| Linker script | `mpfs250.ld` | `mpfs250.ld` | `mpfs250.ld` | `mpfs250-hss.ld` | `mpfs250-m.ld` | `mpfs250-m.ld` | +| HSS YAML | `mpfs.yaml` | `mpfs.yaml` | `mpfs.yaml` | `mpfs-l2lim.yaml` | N/A | N/A | +| `ELF` output | 1 | 1 | 1 | 0 (raw .bin) | 1 | 1 | + +> **Note:** the MPFS250 U54/E51 cores have no RISC-V crypto extensions (Zknh), so none of the +> configurations use them. The SD-card, QSPI and M-Mode + DDR configs build the portable RISC-V +> SHA assembly (`NO_ASM=0`); the size-constrained L2-LIM and M-mode QSPI configs set `NO_ASM=1` +> and use the C implementations. + +## M-mode build flags + +These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS_EXTRA+=-D...`. + +| Flag | Default | Description | +|------|---------|-------------| +| `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | +| `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | +| `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | +| `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | + +### Stack overflow detection + +The trap handler in `src/boot_riscv.c` automatically detects stack overflow on synchronous exceptions (requires `DEBUG_BOOT`). When a trap fires with `SP < _main_hart_stack_bottom`, it prints: + +``` +TRAP: cause=2 epc=A000740 tval=0 + sp=A02FFE8 +STACK OVERFLOW: under by 24 +``` + +This is helpful for diagnosing illegal-instruction TRAPs at random valid `.text` addresses, which are the classic signature of stack overflow corrupting the return address. + +`STACK_SIZE` and `STACK_SIZE_PER_HART` come from the `.config` and are substituted into +`hal/mpfs250-m.ld`: 64 KB for the boot hart in `polarfire_mpfs250_m.config`, which is sized for an +ML-DSA-87 verify on top of two `IMAGE_HEADER_SIZE` stack buffers. An ECC384 + SHA384 verify peaks +at about 6 KB. + +## Building wolfBoot + +All build settings come from .config file. For this platform use `TARGET=mpfs250` and `ARCH=RISCV64`. + +See example configuration at `config/examples/polarfire_mpfs250.config`. + +```sh +# Setup .config (build settings) +cp config/examples/polarfire_mpfs250.config .config + +# build boot loader +make wolfboot.elf +``` + +To assemble this as a flashable image you need the 0x100 byte HART header added: + +```sh +git clone https://github.com/polarfire-soc/hart-software-services.git +cd hart-software-services +cd tools/hss-payload-generator +make +# install tool +sudo cp hss-payload-generator /usr/local/bin/ +``` + +The HSS MMC boot source looks for GPT with GUID "21686148-6449-6E6F-744E-656564454649" or sector "0" if no GPT found. That GUID is the default "BIOS" boot partition. + +The resulting image from `hss-payload-generator` can be directly placed into GPT BIOS partition. + +Use this command to assemble a bootable wolfboot image. The output name is yours to choose; do +not reuse `wolfboot.bin`, which is the raw binary the build just produced: + +```sh +hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot-payload.bin +``` + +You must generated the Device Tree Binary using: + +```sh +dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb +``` + +Write that payload to the card's HSS boot partition - see [Writing a card](#writing-a-card). + +## QSPI + +PolarFire SoC has two CoreQSPI v2 controllers with identical register layouts. The selection +is made at build time via `MPFS_SC_SPI` and affects which QSPI base address wolfBoot uses: + +```text + +-------------------+ +----------------------+ + | U54 cores | | U54 cores | + | (wolfBoot) | | (wolfBoot) | + +---------+---------+ +----------+-----------+ + | | + | direct register access | direct register access + | (MSS QSPI @ 0x2100_0000) | (SC QSPI @ 0x3702_0100) + v v + +-------------------+ +----------------------+ + | MSS QSPI IP | | SC QSPI IP | + | (CoreQSPI v2) | | (CoreQSPI v2) | + +---------+---------+ +----------+-----------+ + | | + v v + External QSPI flash Fabric-connected flash +``` + +Build options: + +- MSS QSPI controller (direct register access at 0x21000000, read/write/erase) + - `EXT_FLASH=1` + - Do not set `MPFS_SC_SPI` + - Example config: `config/examples/polarfire_mpfs250_qspi.config` with `CFLAGS_EXTRA` line removed. + +- SC QSPI controller (direct register access at 0x37020100, read/write/erase) + - `EXT_FLASH=1` + - `CFLAGS_EXTRA+=-DMPFS_SC_SPI` + - Example config: `config/examples/polarfire_mpfs250_qspi.config` as-is. + - Both controllers share the same CoreQSPI v2 register interface. + The only difference is that SC QSPI does not need MSS clock/reset setup. + +Notes: +- Both modes support full read, write, and erase operations. +- SCK is the controller clock divided by the CoreQSPI `CLKRATE` field, SCK = PCLK / (2 * CLKRATE). + `MPFS_QSPI_CLK_DIV` selects the divider by its `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` + (`QSPI_CLK_DIV_2` through `QSPI_CLK_DIV_30`, CLKRATE 1..15). The default `QSPI_CLK_DIV_8` is + about 18.75 MHz from the 150 MHz APB clock, where read throughput stops improving: from there on + the polled FIFO transfer rather than SCK sets the rate, so a faster SCK buys nothing. The MSS QSPI + PCLK is the MSS APB clock, which is 40 MHz at the M-mode reset clock until wolfBoot's PLL + bring-up, so an M-mode build that starts QSPI before `mss_pll_init()` runs SCK that much slower; + the SC QSPI is clocked by the System Controller and is unaffected by the MSS PLL. +- For QSPI-based boot flows, disable SD/eMMC in the config (`DISK_SDCARD=0`, `DISK_EMMC=0`) unless you + explicitly want wolfBoot to load from disk and the application from QSPI. +- The MSS QSPI path expects external flash on the MSS QSPI pins; the SC QSPI path is for + fabric-connected flash (design flash) accessed via the System Controller's QSPI instance. + +## HSS S-mode with L2-LIM (no DDR) + +wolfBoot can run in S-mode via HSS without DDR by targeting the on-chip **L2 Loosely Integrated +Memory (L2-LIM)**. HSS loads wolfBoot into L2-LIM on a U54 application core, and wolfBoot loads +the signed application from SC QSPI into L2-LIM as well. This is the configuration for systems +that keep HSS and run without DDR. + +**Features:** +* S-mode on U54 application core (hart 1), loaded by HSS +* wolfBoot and application both reside in L2-LIM, in the 1.25 MB above the HSS resident region + (`WOLFBOOT_ORIGIN=0x08040000`, `WOLFBOOT_L2LIM_SIZE=0x140000`) +* No DDR required +* SC QSPI flash for both wolfBoot payload and signed application image +* Raw binary output (`ELF=0`) required - ELF with debug symbols is too large for L2-LIM + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_hss_l2lim.config` | HSS S-mode + SC QSPI + L2-LIM | +| `hal/mpfs250-hss.ld` | Linker script for S-mode with L2-LIM | +| `hal/mpfs-l2lim.yaml` | HSS payload generator YAML for L2-LIM load target | + +**Build:** +```sh +cp config/examples/polarfire_mpfs250_hss_l2lim.config .config +make clean && make wolfboot.bin +dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb +hss-payload-generator -vvv -c ./hal/mpfs-l2lim.yaml wolfboot-payload.bin +``` + +Flash the HSS payload to the card's boot partition - see [Writing a card](#writing-a-card). + +**Build and sign the test application:** +```sh +make test-app/image_v1_signed.bin +``` + +**Flash the signed application to QSPI** using the UART programmer (`UART_QSPI_PROGRAM=1`, which +this config sets, over the U54 console on MMUART1; needs `pyserial`): +```sh +python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB1 \ + test-app/image_v1_signed.bin 0x20000 +``` + +**Notes:** +- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash contents are stable. +- `ELF=0` is required: the test-app linker script (`test-app/RISCV64-mpfs250.ld`) places `.init` + (containing `_reset()`) first so the raw binary entry point is at offset 0. The full ELF with + debug symbols exceeds L2-LIM capacity. +- wolfBoot is placed at `0x08040000` (above the HSS L2-LIM resident region) and the application + is loaded at `0x08060000`. The stack resides at the top of the 1.25 MB region. +- HSS must be built and programmed to eNVM separately (see [Building Hart Software Services](#building-hart-software-services-hss)). +- **LIM instruction fetch caveat:** Ensure `L2_WAY_ENABLE` leaves enough cache ways unallocated + to back the LIM SRAM region. See the M-mode section for a detailed explanation. +- UART output appears on MMUART1 (`/dev/ttyUSB1`), same as other S-mode configurations. + +## M-mode (bare-metal eNVM boot) + +wolfBoot supports running directly in Machine Mode (M-mode) on PolarFire SoC, replacing the Hart +Software Services (HSS) as the first-stage bootloader. wolfBoot runs on the E51 monitor core from +eNVM and loads a signed application from SC QSPI flash into L2 Scratchpad (on-chip RAM) -- no HSS +or DDR required. This is the minimal on-chip-only configuration. + +**Features:** +* Runs on E51 monitor core (hart 0) directly from eNVM +* Executes from L2 Scratchpad SRAM (256 KB at `0x0A000000`) +* Loads signed application from SC QSPI flash to L2 Scratchpad (`0x0A010200`) +* No HSS or DDR required - boots entirely from on-chip memory +* Parks and releases secondary U54 harts via CLINT IPI +* ECC384 + SHA384 signature verification + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_m_qspi.config` | M-mode + SC QSPI configuration | +| `hal/mpfs250-m.ld` | M-mode linker script (eNVM + L2 SRAM) | +| `hal/mpfs250.c` | HAL with QSPI driver, UART, L2 cache init | +| `src/boot_riscv_start.S` | M-mode assembly startup | + +**Boot flow:** +1. **eNVM reset vector** (`0x20220100`): CPU starts, startup code copies wolfBoot to L2 Scratchpad +2. **L2 Scratchpad execution** (`0x0A000000`): wolfBoot runs from scratchpad +3. **Hardware init**: L2 cache configuration, UART setup +4. **QSPI init**: SC QSPI controller (`0x37020100`), JEDEC ID read, 4-byte address mode +5. **Image load**: Read signed image from QSPI flash (`0x20000`) to L2 Scratchpad (`0x0A010200`) +6. **Verify & boot**: SHA384 integrity check, ECC384 signature verification, jump to app + +**Build:** +```sh +cp config/examples/polarfire_mpfs250_m_qspi.config .config +make clean && make wolfboot.elf +``` + +**Flash wolfBoot to eNVM** with `mpfsBootmodeProgrammer`, exactly as in +[quick start path 2](#path-2-wolfboot-replaces-hss-and-boots-linux) - the only difference is +which `wolfboot.elf` is programmed. Restoring HSS is the same command with the HSS ELF. + +**Build and sign the test application:** +```sh +make test-app/image_v1_signed.bin +``` + +**Flash the signed application to QSPI** using the UART programmer (`UART_QSPI_PROGRAM=1`, which +this config sets, over the E51 console on MMUART0; needs `pyserial`): +```sh +python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB0 \ + test-app/image_v1_signed.bin 0x20000 +``` + +The script: +1. Waits for wolfBoot to print the `QSPI-PROG: Press 'P'` prompt (power-cycle the board) +2. Sends `P` to enter programming mode +3. Transfers the binary in 256-byte ACK-driven chunks +4. wolfBoot erases, writes, and then continues booting the new image + +Use `0x20000` for the boot partition and `0x02000000` for the update partition. + +**QSPI partition layout** (Micron MT25QL01G, 128 MB): + +| Region | Address | Size | +|--------|---------|------| +| Boot partition | `0x00020000` | ~32 MB | +| Update partition | `0x02000000` | ~32 MB | +| Swap partition | `0x04000000` | 64 KB | + +**UART mapping:** + +| Hart | Core | MMUART | USB device | +|------|------|--------|------------| +| 0 | E51 | MMUART0 | /dev/ttyUSB0 | +| 1 | U54_1 | MMUART1 | /dev/ttyUSB1 | +| 2 | U54_2 | MMUART2 | N/A | +| 3 | U54_3 | MMUART3 | N/A | +| 4 | U54_4 | MMUART4 | N/A | + +**Expected serial output on successful boot:** +``` +wolfBoot Version: X.Y.Z (...) +Running on E51 (hart 0) in M-mode +QSPI: Using SC QSPI Controller (0x37020100) +QSPI: Flash ID = 0x20 0xBA 0x21 +QSPI-PROG: Press 'P' within 3s to program flash +QSPI-PROG: No trigger (got 0x00 ...), booting +Versions: Boot 1, Update 0 +... +Firmware Valid +Booting at 0x... +``` + +**Notes:** +- The E51 is `rv64imac` (no FPU or crypto extensions). wolfBoot is compiled with `NO_ASM=1` to + use portable C crypto implementations and `-march=rv64imac -mabi=lp64` for correct code + generation. The `rdtime` CSR instruction is not available in bare-metal M-mode; wolfBoot uses a + calibrated busy-loop for all delays (`udelay()` in `hal/mpfs250.c`). +- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash + contents are stable. +- The config uses `WOLFBOOT_LOAD_ADDRESS=0x0A010200` to place the application in L2 Scratchpad + above wolfBoot code (~64 KB at `0x0A000000`), with the stack at the top of the 256 KB region. +- **LIM instruction fetch limitation:** The on-chip LIM (`0x08000000`, 2 MB) is backed by L2 + cache ways. When `L2_WAY_ENABLE` is set to `0x0B` (all cache ways 0-7 active for caching), + no ways remain for LIM backing SRAM. Data reads from LIM work through the L2 cache, but + instruction fetch silently hangs - the CPU stalls with no trap generated. For this reason the + application is loaded into L2 Scratchpad (`0x0A000000`), which is always accessible regardless + of `L2_WAY_ENABLE`. To use LIM, reduce `L2_WAY_ENABLE` to free cache ways for LIM backing. +- **Strip debug symbols** before signing the test-app ELF. The debug build is ~150 KB but the + stripped ELF is ~5 KB. L2 Scratchpad has ~150 KB available between wolfBoot code and the stack: + `riscv64-unknown-elf-strip --strip-debug test-app/image.elf` +- **DDR support:** software LPDDR4 initialization is included via the **M-Mode + DDR** + configuration (`polarfire_mpfs250_m.config`, requires `LIBERO_FPGA_CONFIG_DIR`) for use cases + that require loading larger images (e.g. a Linux FIT) to DDR memory. See + [M-mode + DDR](#m-mode--ddr-booting-linux-minimal-sbi). + +## M-mode + DDR: booting Linux (minimal SBI) + +The **M-Mode + DDR** configuration (`config/examples/polarfire_mpfs250_m.config`) replaces both +HSS and OpenSBI: wolfBoot performs the LPDDR4 init/training on the E51, loads and verifies a +signed Yocto FIT image (kernel + dtb) from SD card into DDR, applies device-tree fixups, releases +U54 hart 1 into S-mode at the kernel entry, and then remains resident as a minimal M-mode SBI +runtime. Validated on the MPFS250T Video Kit: 4-CPU SMP Yocto Linux to login in ~40 s from +power-on. + +**Minimal SBI runtime** (`src/riscv_sbi.c`, a clean-room implementation of the OpenSBI/SBI spec; generic RISC-V with HAL hooks; enabled by +`WOLFBOOT_MMODE_SMODE_BOOT`): +* SBI v0.3 extensions: BASE, TIME (per-hart `mtimecmp`, MTIP-to-STIP injection), IPI (SSIP + injection via CLINT MSIP), RFENCE (remote `fence.i` / `sfence.vma` with completion wait), + HSM (`hart_start`/`hart_stop`/`hart_status` backed by per-hart start mailboxes), DBCN and the + legacy console putchar (shared with the wolfBoot UART), SRST (reset; a shutdown request + resets the device rather than powering it down). +* `rdtime` emulation: the U54/E51 have no `time` CSR, so `rdtime` is emulated from CLINT MTIME. + The M-mode HAL starts the MTIME time base before hand-off via SYSREG `RTC_CLOCK_CR` at 1 MHz + (`mpfs_enable_mtime()`), matching the device-tree `timebase-frequency`. +* Misaligned load/store emulation (not delegatable on these harts), including compressed forms, + for the kernel's unaligned copy tails. +* Per-hart M-mode trap stacks live in the `hss-buffer` reserved (nomap) DDR region; cross-hart + state (HSM mailboxes, IPI flags, the hart-release gate flag) lives in the E51 DTIM at + `0x01000000`, which is uncached and coherent for all harts. Cacheable L2-scratchpad memory + must not be used for cross-hart signalling (stores can be lost on dirty-line eviction). + +**Device-tree fixups** applied to the loaded dtb (`hal/mpfs250.c`): bootargs/root device +(the DTB's own `/chosen/bootargs` wins unless `LINUX_BOOTARGS`/`LINUX_BOOTARGS_ROOT` is set; +see "Linux kernel command line (bootargs)" in `docs/compile.md`), +MAC addresses from the device serial number, and all five MSS watchdog nodes are disabled. + +**Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot +be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked +E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via +the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS +instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms +them at probe and something on the OS side (a watchdog daemon, or the design's own fabric +scheme) has to service them from then on. A design with its own fabric watchdog needs neither +setting; wolfBoot never touches fabric logic. + +**Hand-off / SMP flow:** secondary harts park in eNVM until the E51 signals image-copy +completion (DTIM gate flag), then park in a WFI loop. The boot hart is released with a staged +mailbox {entry, dtb} plus MSIP; Linux brings up the remaining harts through SBI HSM +`hart_start`, which uses the same mailbox + MSIP path. The release path must stay fast +(no UART access): the kernel allows roughly one second for a started hart to come online. + +**Read-first requirement:** the LPDDR4 controller on this board needs the first CPU-side +transaction after DDR initialization to be a read. When a write comes first on a cold boot it +lands in DDR displaced by several bus beats, and every later cached-port write in that boot +follows it. `mpfs_ddr_init()` therefore reads one block through the non-cached alias before +anything is written, after which CPU, L2 write-back and SDMA writes all land where they should. + +**Driver structure:** the licensed Cadence DDR controller logic (controller CSR programming, the +Memory Test Controller engine, and the LPDDR4 mode-register protocol) lives in the +target-independent `src/ddr_cadence.c` / `include/ddr_cadence.h` (controller base overridable via +`DDR_CADENCE_CTRL_BASE`). The Microchip-specific PHY, PLL, clock mux and training, plus the +board's `LIBERO_SETTING_*` values, stay in `hal/mpfs250_ddr.c`, which builds the controller +register table and composes the generic calls. Both compile only when `MPFS_DDR_INIT` is set. + +## Boot mode 2: User Boot Loader stub + +Boot mode 2 ("user secure boot from sNVM") lets the System Controller authenticate the first code the MSS runs: it copies a User Boot Loader (UBL) out of authenticated sNVM pages into the LIM and releases all five harts at its entry. wolfBoot provides that UBL as a small verifier target, `TARGET=mpfs250_ubl`: it checks an ECC384/SHA384 wolfBoot manifest over the eNVM wolfBoot image against the public key compiled into the stub, then enters the eNVM wolfBoot at its reset vector on every hart exactly as a boot mode 1 boot would. The eNVM wolfBoot itself is unchanged and stays field-updatable; what the sNVM pages pin is the key that must have signed it. + +**eNVM layout** (`hal/mpfs250-m.ld`, `hal/mpfs250.h`) + +| Address | Content | +|---------|---------| +| `0x20220000` | boot ROM meta information (written by `mpfsBootmodeProgrammer`) | +| `0x20220100` | wolfBoot image, reset vector first | +| `0x2023FC00` | wolfBoot manifest header (`MPFS_ENVM_MANIFEST=1`), signed by the stub's key | + +The manifest is detached from the image it signs (the `WOLFBOOT_SELF_HEADER` form), so the image does not move and a boot mode 1 device boots the same eNVM contents. The last two eNVM pages stay free for the programmer. + +**Relevant files:** + +| File | Description | +|------|-------------| +| `config/examples/polarfire_mpfs250_ubl.config` | UBL stub configuration | +| `hal/mpfs250_ubl.c`, `hal/mpfs250_ubl.h` | stub HAL: watchdog hold, MMUART0 at the reset clock, manifest verify, hart hand-off | +| `hal/mpfs250_ubl.ld` | LIM linker script with the sNVM size limit | +| `src/boot_riscv_start.S` | `TARGET_mpfs250_ubl` startup: no copy, U54s park on a DTIM mailbox | +| `tools/scripts/mpfs_bootmode2.sh` | sNVM + boot mode programming on the secure dies, eNVM-only update | + +**Build the stub** (its own signing key; keep it out of the tree that signs application images): +```sh +cp config/examples/polarfire_mpfs250_ubl.config .config +make clean && make +# wolfboot.elf is the UBL; wolfboot_signing_private_key.der is the UBL key +``` +The image must fit the 221 authenticated sNVM pages (52,096 bytes after the 60-byte UBL header); the linker script enforces it. The sNVM pages above the stub stay available for the sNVM keystore and wrapped keys of [Hardware root of trust](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). + +**Build and sign the eNVM wolfBoot** for the stub (any M-mode configuration): +```sh +cp config/examples/polarfire_mpfs250_m.config .config +make clean && make LIBERO_FPGA_CONFIG_DIR=... MPFS_ENVM_MANIFEST=1 \ + MPFS_UBL_KEY=/path/to/ubl/wolfboot_signing_private_key.der \ + wolfboot_envm_signed.bin +``` +`MPFS_ENVM_MANIFEST=1` links a manifest placeholder into the last used eNVM page; the rule signs the image without it (`wolfboot_envm.bin`, the ELF's loadable bytes with alignment gaps zero-filled, which is what the programmer writes), writes the resulting header into the placeholder and produces `wolfboot_envm_signed.elf`, which programs image and manifest together. `MPFS_UBL_KEY` defaults to the tree's own key, `MPFS_ENVM_VERSION` (default 1) is the manifest version, and `-DMPFS_UBL_MIN_VERSION=N` in the stub build refuses older manifests. + +**Program** (requires `SC_INSTALL_DIR` and `FPGENPROG`, see the M-mode section): +```sh +# 1. eNVM: image + manifest. This also selects boot mode 1, so the device +# boots the new eNVM contents before the stub is in place. +java -jar $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ + --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD \ + wolfboot_envm_signed.elf + +# 2. sNVM: the stub, and boot mode 2. Non-secure dies: the programmer +# itself (--bootmode 2 --snvm_page 0 --usk 0 wolfboot.elf). Secure dies +# (MPFS250TS...), which the programmer does not list but which an +# authenticated sNVM client requires: +tools/scripts/mpfs_bootmode2.sh --die MPFS250TS --workdir $PWD /path/to/ubl/wolfboot.elf +``` +The script runs the programmer in `--dryrun` for the UBL image and the User sNVM Key (USK), replays its `fpgenprog` steps for the requested die and programs. Keep the USK file it names: the sNVM pages can only be rewritten with it. + +The UBL lives in an authenticated sNVM client, which the System Controller keys from the device's sNVM Master Key (SMK) +and the client's 12-byte USK. A device whose SMK has never been initialized cannot hold such a client, and +`mpfsBootmodeProgrammer` never asks for one, so `--regenerate-smk` passes `--regenerate_smk` to the `snvm_client` step +and the generated bitstream carries a new SMK. A new SMK invalidates every authenticated or encrypted sNVM client +already on the device; wolfBoot's own keystore and wrapped image key are plaintext clients and are not SMK-keyed. + +Updating the eNVM wolfBoot under boot mode 2 goes through the same script with `--envm`, which programs the eNVM client only and leaves the boot mode and sNVM alone: +```sh +tools/scripts/mpfs_bootmode2.sh --envm --die MPFS250TS --workdir $PWD wolfboot_envm_signed.elf +``` +`mpfsBootmodeProgrammer --bootmode 1` restores boot mode 1. + +**Expected serial output:** +``` +wolfBoot UBL 2.10.0: eNVM image 0x20220100, manifest 0x2023FC00 +UBL: eNVM wolfBoot signature OK (1051 ms), entering 0x20220100 +UBL: 5 of 5 harts released +wolfBoot Version: ... +``` +A manifest that does not verify prints which check failed (`no manifest`, `digest mismatch`, `signature invalid`) and `UBL: eNVM wolfBoot verify FAILED`; the stub panics and nothing in eNVM runs. + +**Notes:** +- The stub runs at the reset clock and changes no clock, L2 way or peripheral the eNVM wolfBoot expects to find in its reset state. It holds the five MSS watchdogs in the no-reset, maximum-window state for the verify and puts the E51's back before the jump, so the eNVM wolfBoot's own watchdog snapshot is what the boot ROM left. +- The U54s wait in the stub on a DTIM mailbox (`hal/mpfs250_ubl.h`) for the verified entry, acknowledge and jump; the E51 follows last. All five harts then run the eNVM startup as in boot mode 1. +- Immutability of the stub is a Libero decision: the ROM flag on its sNVM pages and a frozen boot configuration. wolfBoot does not program either. + +## Encryption + +PolarFire SoC boots from disk with `WOLFBOOT_NO_PARTITIONS`, so there is no partition-resident key store: the encryption key is held in RAM for the life of the boot. A production build keeps it in secure NVM instead, wrapped by the PUF-derived KEK - see [Hardware root of trust](#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). + +Enable encryption in your configuration with `ENCRYPT=1` and one of: `ENCRYPT_WITH_AES256=1`, `ENCRYPT_WITH_AES128=1`, or `ENCRYPT_WITH_CHACHA=1`. + +| Algorithm | Key Size | Nonce/IV Size | +|-----------|----------|---------------| +| ChaCha20 | 32 bytes | 12 bytes | +| AES-128 | 16 bytes | 16 bytes | +| AES-256 | 32 bytes | 16 bytes | + +The `libwolfboot` API provides the following functions for managing the encryption key: + +```c +int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce); +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce); +int wolfBoot_erase_encrypt_key(void); /* called automatically by wolfBoot_success() */ +``` + +To use your own implementation for getting the encryption key use `CUSTOM_ENCRYPT_KEY` and `OBJS_EXTRA=src/my_custom_encrypt_key.o`. +Then provide your own implementation of `int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce);` + +Example: + +```c +int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + int i; + /* Test key: "0123456789abcdef0123456789abcdef" (32 bytes for AES-256) */ + const char test_key[] = "0123456789abcdef0123456789abcdef"; + /* Test nonce: "0123456789abcdef" (16 bytes) */ + const char test_nonce[] = "0123456789abcdef"; + + for (i = 0; i < ENCRYPT_KEY_SIZE && i < (int)sizeof(test_key); i++) { + key[i] = (uint8_t)test_key[i]; + } + for (i = 0; i < ENCRYPT_NONCE_SIZE && i < (int)sizeof(test_nonce); i++) { + nonce[i] = (uint8_t)test_nonce[i]; + } + return 0; +} +``` + +To sign and encrypt an image, create a key file with the concatenated key and nonce, then use the sign tool: + +```sh +# Create key file (32-byte key + 16-byte nonce for AES-256) +printf "0123456789abcdef0123456789abcdef0123456789abcdef" > /tmp/enc_key.der + +# Sign and encrypt +./tools/keytools/sign --ecc384 --sha384 --aes256 --encrypt /tmp/enc_key.der \ + fitImage wolfboot_signing_private_key.der 1 +``` + +The result is `fitImage_v1_signed_and_encrypted.bin`, which gets placed into your OFP_A or OFP_B partitions. + +```sh +sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc2 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc2 +sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc3 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc3 +``` + +During boot, wolfBoot decrypts the image headers from disk to select the best candidate, loads and decrypts the full image to RAM, then verifies integrity and authenticity before booting. On successful boot, `wolfBoot_success()` clears the key from RAM. + +See the [Encrypted Partitions](encrypted_partitions.md) documentation for additional details. + +## PQC (ML-DSA) + +### Configuration + +Update your `.config` file with the following ML-DSA settings, or pass them as `make` arguments on top of `config/examples/polarfire_mpfs250_m.config` (the standalone M-mode E51 target verifies ML-DSA-87 this way, with SHA-384 as the image hash): + +```makefile +# ML-DSA 87 (Category 5) +SIGN=ML_DSA +HASH=SHA384 +ML_DSA_LEVEL=5 +IMAGE_SIGNATURE_SIZE=4627 +IMAGE_HEADER_SIZE=12288 +WOLFBOOT_SECTOR_SIZE?=0x4000 +``` + +**Important:** +- The `sign` tool requires `IMAGE_HEADER_SIZE` to be set as an environment variable, even if it's already configured in `.config`. This is because the sign tool reads the environment variable separately to determine the header size for padding. Without this, the sign tool may use a smaller default header size, causing a mismatch with wolfBoot's expected header size. +- The `WOLFBOOT_SECTOR_SIZE` must be larger than the `IMAGE_HEADER_SIZE`. + +### Signing and Encryption + +```sh +# Sign and Encrypt with PQ ML-DSA 5 (87) +# NOTE: IMAGE_HEADER_SIZE must match the value in .config +IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 --aes256 --encrypt /tmp/enc_key.der \ + fitImage wolfboot_signing_private_key.der 1 +``` + +**ML-DSA Parameter Reference:** + +| ML_DSA_LEVEL | Security Category | Signature Size | Private Key | Public Key | Recommended IMAGE_HEADER_SIZE | +|--------------|-------------------|----------------|-------------|------------|------------------------------ | +| 2 | Category 2 | 2420 | 2560 | 1312 | 8192 | +| 3 | Category 3 | 3309 | 4032 | 1952 | 8192 | +| 5 | Category 5 | 4627 | 4896 | 2592 | 12288 | + +For other ML-DSA levels, adjust `ML_DSA_LEVEL`, `IMAGE_SIGNATURE_SIZE`, and `IMAGE_HEADER_SIZE` accordingly in both `.config` and the signing command. + +## Writing a card + +Two card layouts are in use, one per boot flow. They are not interchangeable: the partition a +build reads from is fixed at compile time. + +| Layout | Used by | Partitions | +|---|---|---| +| HSS | every S-mode configuration (`polarfire_mpfs250.config`, `_qspi`, `_hss_l2lim`) | p1 8 MB BIOS boot (type GUID `21686148-6449-6E6F-744E-656564454649`) holding the HSS payload, p2/p3 64 MB `OFP_A`/`OFP_B` image slots, p4 rootfs | +| M-mode | `polarfire_mpfs250_m.config` (`BOOT_PART_A=0`, `BOOT_PART_B=1`) | p1 32 MB boot slot holding the signed FIT, p2 32 MB update slot, p3 rootfs; no BIOS partition, because nothing loads wolfBoot from the card | + +The [example boot output](#example-boot-output) below is the M-mode layout: three GPT entries, the +first two 32 MB. + +### HSS layout + +Replace `/dev/sdc` with the device `lsblk` reports. Adjust `+64M` for larger image slots. + +```sh +sudo fdisk /dev/sdc <.rootfs.ext4 of=/dev/sdc4 bs=4M status=progress +sync +``` + +Build and sign the test application first: + +```sh +make test-app/image.elf +./tools/keytools/sign --ecc384 --sha384 test-app/image.elf wolfboot_signing_private_key.der 1 +``` + +With the HSS layout, insert the card and let HSS start wolfBoot. If HSS defaults to eMMC, select +the card with `boot sdcard` at the HSS console, or build HSS with MMC disabled and SD enabled. + +## Building Hart Software Services (HSS) + +The Hart Software Services (HSS) is the PolarFire SoC zero-stage bootloader (E51 monitor core); it is required only for the HSS-based S-mode configurations above, not for the M-mode + DDR path which replaces it. + +```sh +git clone https://github.com/polarfire-soc/hart-software-services.git +cd hart-software-services +make clean +make BOARD=mpfs-video-kit +make BOARD=mpfs-video-kit program +``` + +## Building Yocto-SDK Linux + +The signed Yocto FIT image booted by wolfBoot is produced from Microchip's `meta-mchp` Yocto layer (BSP, drivers, and kernel for the board). + +See: +* https://github.com/linux4microchip/meta-mchp/blob/scarthgap/meta-mchp-common/README.md +* https://github.com/linux4microchip/meta-mchp/blob/scarthgap/meta-mchp-polarfire-soc/README.md +* https://github.com/polarfire-soc/polarfire-soc-documentation/blob/master/reference-designs-fpga-and-development-kits/mpfs-video-kit-embedded-software-user-guide.md + +Building mchp-base-image Yocto Linux: + +```sh +mkdir ../yocto-dev-polarfire +cd ../yocto-dev-polarfire +repo init -u https://github.com/linux4microchip/meta-mchp-manifest.git -b refs/tags/linux4microchip+fpga-2025.10 -m polarfire-soc/default.xml +repo sync +export TEMPLATECONF=${TEMPLATECONF:-../meta-mchp/meta-mchp-polarfire-soc/meta-mchp-polarfire-soc-bsp/conf/templates/default} +source openembedded-core/oe-init-build-env +# A Microchip base image with standard Linux utilities, as well as some Microchip apps and examples +MACHINE=mpfs-video-kit bitbake mchp-base-image +OR +# A Microchip base image with additional support for software development, including toolchains and debug tools +MACHINE=mpfs-video-kit bitbake mchp-base-image-sdk +``` + +Build images are output to: `./tmp-glibc/deploy/images/mpfs-video-kit/` + +### Custom FIT image, signing and copying to SDCard + +wolfBoot can either decompress a gzipped kernel at boot time (`GZIP=1`, +the default for `polarfire_mpfs250.config` and `polarfire_mpfs250_qspi.config`) +or accept a pre-decompressed kernel inside the FIT (`GZIP=0`). Pick one path. + +`src/gzip.c` decodes with lookup tables by default; `GZIP_SMALL=1` selects +its bit-serial decoder instead, with the same checks and return codes, a +few KB less code and no static tables, at a fraction of the rate. The +difference is largest on cores without a data cache, where the bit-serial +decoder can make the inflate the longest phase of a compressed boot. + +#### Option A - Compressed FIT (`GZIP=1`, default) + +Set `compression = "gzip"` and point `data` at the gzipped kernel directly +in `hal/mpfs250.its`: + +```dts +images { + kernel-1 { + data = /incbin/("../yocto-dev-polarfire/build/tmp-glibc/work/mpfs_video_kit-oe-linux/linux-mchp/6.12.22+git/build/linux.bin.gz"); + compression = "gzip"; + load = <0x80200000>; + entry = <0x80200000>; + hash-1 { algo = "sha256"; }; + ... + }; +}; +``` + +Then build the FIT directly - no manual `gzip -cdvk` step: + +```sh +mkimage -f hal/mpfs250.its fitImage +``` + +At boot, wolfBoot decompresses the kernel into `0x80200000` directly out of +the FIT `data` blob. The outer wolfBoot signature covers the whole FIT +(including the compressed `data`), and gzip's CRC32 + ISIZE trailer covers +the decompressed output; the per-image `hash-1` subnodes are not re-checked +at runtime as they would duplicate the outer signature. + +#### Option B - Uncompressed FIT (`GZIP=0`) + +Build wolfBoot with `GZIP=0` and pre-decompress the kernel on the host. +Keep `compression = "none"` in `hal/mpfs250.its`: + +```sh +# Extract GZIP compressed linux kernel to wolfboot root +gzip -cdvk ../yocto-dev-polarfire/build/tmp-glibc/work/mpfs_video_kit-oe-linux/linux-mchp/6.12.22+git/build/linux.bin > kernel.bin + +# Create custom FIT image +mkimage -f hal/mpfs250.its fitImage +FIT description: PolarFire SoC MPFS250T +Created: Tue Dec 23 11:29:02 2025 + Image 0 (kernel-1) + Description: Linux Kernel + Created: Tue Dec 23 11:29:02 2025 + Type: Kernel Image + Compression: uncompressed + Data Size: 19745280 Bytes = 19282.50 KiB = 18.83 MiB + Architecture: RISC-V + OS: Linux + Load Address: 0x80200000 + Entry Point: 0x80200000 + Hash algo: sha256 + Hash value: 800ce147fa91f367ec620936a59a1035c49971ed4b9080c96bdc547471e80487 + Image 1 (fdt-1) + Description: Flattened Device Tree blob + Created: Tue Dec 23 11:29:02 2025 + Type: Flat Device Tree + Compression: uncompressed + Data Size: 19897 Bytes = 19.43 KiB = 0.02 MiB + Architecture: RISC-V + Load Address: 0x8a000000 + Hash algo: sha256 + Hash value: 0b4efca8c0607c9a8f4f9a00ccb7691936e019f3181aab45e6d52dae91975039 + Default Configuration: 'conf1' + Configuration 0 (conf1) + Description: Linux kernel and FDT blob + Kernel: kernel-1 + FDT: fdt-1 + Hash algo: sha256 + Hash value: unavailable + +# Sign FIT image with version 1 +./tools/keytools/sign --ecc384 --sha384 fitImage wolfboot_signing_private_key.der 1 +``` + +Then write `fitImage_v1_signed.bin` to the image slots and the Yocto rootfs +(`tmp-glibc/deploy/images/mpfs-video-kit/*.rootfs.ext4`) to the rootfs partition - see +[Writing a card](#writing-a-card). + +## Debugging + +Start GDB server: + +```sh +$SC_INSTALL_DIR/openocd/bin/openocd --command "set DEVICE MPFS" --file board/microsemi-riscv.cfg +``` + +Start GDB Client: `riscv64-unknown-elf-gdb` + +``` +file wolfboot.elf +tar rem:3333 +add-symbol-file ../hart-software-services/build/hss-l2scratch.elf +set pagination off +foc c + +set $target_riscv=1 +set mem inaccessible-by-default off +set architecture riscv:rv64 +#load wolfboot.elf +#thread apply 2 set $pc=_reset +#thread apply all set $pc=_start +``` + +## Example boot output + +``` +wolfBoot Version: 2.10.0 +Running on E51 (hart 0) in M-mode +Boot RESET_SR: 1FF (bit0=PERIPH bit1=MSS bit2=CPU bit3=DBG bit4=FABRIC bit5=WDOG bit6=GPIO bit7=BUS bit8=SOFT) +======================================== +DDR: Training+MTC PASS +DDR: Initialization COMPLETE +SDHCI: platform init +Reading MBR... +Found GPT PTE at sector 1 +Valid GPT partition table +Max number of partitions: 128 + GPT part 0: 0_2000000h @ 0_100000 + GPT part 1: 0_2000000h @ 0_2100000 + GPT part 2: 7_6A300000h @ 0_4100000 +Total partitions on disk0: 3 +Checking primary OS image in 0,0... +Checking secondary OS image in 0,1... +Versions, A:1 B:0 +Load block size: 512KB +Load address 0x82000000 +Attempting boot from P:A +Boot partition: 0xA03FD90 (sz 19766364, ver 0x1, type 0x601) +Loading image from disk... +done +Checking image integrity... +done +Verifying image signature... +done +Firmware Valid. +Flattened uImage Tree: Version 17, Size 19766364 +Loading Image kernel-1: 0x820000C8 -> 0x80200000 (19745280 bytes) +Loading Image fdt-1: 0x832D4BCC -> 0x8A000000 (19897 bytes) +Loading DTS: 0x8A000000 -> 0x8A000000 (19897 bytes) +Booting at 80200000 +FDT: Set chosen, bootargs=earlycon=sbi root=/dev/mmcblk0p3 rootwait uio_pdrv_genirq.of_id=generic-uio +FDT: Device serial: 219A437C-6AE1F1C2-8EDC4324-685B2288 +FDT: MAC0 = 00:04:A3:5B:22:88 +FDT: Set watchdog@20001000 status=disabled (... all five MSS WDT nodes) +M->S handoff: entry=0x80200000 hart=0 dtb=0x8A000000 +Releasing hart 1 into S-mode at 0x80200000 (dtb=0x8A000000) +[ 0.000000] Linux version 6.12.22-linux4microchip+fpga-2025.07 ... #1 SMP +[ 0.000000] Machine model: Microchip PolarFire-SoC VIDEO Kit +[ 0.000000] SBI specification v0.3 detected +[ 0.000000] SBI implementation ID=0x776f6c66 Version=0x1 +[ 0.000000] SBI TIME extension detected +[ 0.000000] SBI IPI extension detected +[ 0.000000] SBI RFENCE extension detected +[ 0.000000] riscv: providing IPIs using SBI IPI extension +[ 0.016264] smp: Bringing up secondary CPUs ... +[ 0.040738] smp: Brought up 1 node, 4 CPUs +[ 3.764474] Run /sbin/init as init process +... +mpfs-video-kit login: +``` + +The Linux SBI lines confirm the kernel is talking to wolfBoot's own SBI runtime: the implementation ID `0x776f6c66` is ASCII `"wolf"`, distinguishing it from OpenSBI (ID `0x8`). + +## Performance comparison + +### Binary Size Comparison + +The following table compares wolfBoot binary sizes for different signature algorithms on PolarFire SoC (MPFS250): + +| Algorithm | Hash | Text | Data | BSS | Total | Binary Size | +|-----------|--------|---------|------|---------|---------|-------------| +| ECC384 | SHA384 | 67.1 KB | 8 B | 3.0 KB | 70.2 KB | 68 KB | +| ML-DSA 87 | SHA256 | 63.9 KB | 0 B | 14.5 KB | 78.4 KB | 64 KB | + +### Boot Time Comparison + +Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB encrypted FIT image: + +| Algorithm | Hash | Load Time | Decrypt Time | Integrity Check | Signature Verify | Total Boot Time | +|-------------|---------|-----------|--------------|-----------------|------------------|-----------------| +| ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | +| ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | + +Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit with host-side timestamps on the console, plaintext FIT, ECC384/SHA384, software crypto, the same 19.7 MB kernel either gzip-compressed in the FIT (5.9 MB) or stored uncompressed (19.8 MB): + +Elapsed time from power-on at each point, not per-phase duration: + +| Reached | gzip FIT | uncompressed FIT | +|-----------------------------------------------|----------|------------------| +| DDR trained, SD init, GPT read, FIT loaded, SHA384 integrity and ECC384 verify done | ~2 s | ~4 s | +| gzip inflate of the kernel finished | ~18 s | n/a | +| FIT copies and DTB fixups done | ~18 s | ~5 s | +| M-mode -> S-mode hand-off | ~18 s | ~5 s | +| Linux login prompt | ~41 s | ~28 s | + +Both columns are the rootfs-on-a-card image; an initramfs image reaches its login sooner because +it mounts no root device. + +The inflate runs on the E51 at about 1.2 MB/s of output, so a compressed kernel costs far more than the extra card read it saves; for a short boot, build the FIT with `compression = "none"` for the kernel (`FIT_KERNEL_COMP_ALG = "none"` in Yocto). The software SHA384 runs at about 11 MB/s on the E51 and the ECC384 verify is well under a second; neither is a lever. `BOOT_BENCHMARK` phase prints on this target are in real time: the timer reads the live CPU clock. + +The SD load, the FIT copies, the DTB fixups and the image hash all use DDR directly (see the read-first requirement under [M-mode + DDR](#m-mode--ddr-booting-linux-minimal-sbi)). Measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. + +## Benchmarks + +RISC-V 64-bit U54 (RV64GC1) 625 MHz + +wolfCrypt on Linux, for the algorithms wolfBoot uses on this part (hash, ECDSA, ML-DSA). + +``` +./configure --enable-riscv-asm --enable-mldsa --enable-mlkem --enable-sp=yes +make +./wolfcrypt/benchmark/benchmark +------------------------------------------------------------------------------ + wolfSSL version 5.8.4 +------------------------------------------------------------------------------ +Math: Multi-Precision: Wolf(SP) word-size=64 bits=3072 sp_int.c + Single Precision: ecc 256 rsa/dh 2048 3072 sp_c64.c + Assembly Speedups: RISCVASM ALIGN +wolfCrypt Benchmark (block bytes 1048576, min 1.0 sec each) +SHA 20 MiB took 1.141 seconds, 17.535 MiB/s +SHA-256 10 MiB took 1.071 seconds, 9.336 MiB/s +SHA-384 15 MiB took 1.066 seconds, 14.068 MiB/s +SHA-512 15 MiB took 1.066 seconds, 14.070 MiB/s +SHAKE128 15 MiB took 1.142 seconds, 13.135 MiB/s +SHAKE256 15 MiB took 1.402 seconds, 10.699 MiB/s +HMAC-SHA 20 MiB took 1.145 seconds, 17.470 MiB/s +HMAC-SHA256 10 MiB took 1.074 seconds, 9.310 MiB/s +HMAC-SHA384 15 MiB took 1.076 seconds, 13.944 MiB/s +HMAC-SHA512 15 MiB took 1.069 seconds, 14.036 MiB/s +PBKDF2 1 KiB took 1.023 seconds, 1.130 KiB/s +ECC [ SECP256R1] 256 key gen 800 ops took 1.100 sec, avg 1.375 ms, 727.248 ops/sec +ECDHE [ SECP256R1] 256 agree 300 ops took 1.041 sec, avg 3.470 ms, 288.152 ops/sec +ECDSA [ SECP256R1] 256 sign 600 ops took 1.144 sec, avg 1.907 ms, 524.370 ops/sec +ECDSA [ SECP256R1] 256 verify 300 ops took 1.173 sec, avg 3.909 ms, 255.844 ops/sec +ECC [ SECP384R1] 384 key gen 100 ops took 3.887 sec, avg 38.867 ms, 25.729 ops/sec +ECDHE [ SECP384R1] 384 agree 100 ops took 3.883 sec, avg 38.827 ms, 25.755 ops/sec +ECDSA [ SECP384R1] 384 sign 100 ops took 3.948 sec, avg 39.485 ms, 25.326 ops/sec +ECDSA [ SECP384R1] 384 verify 100 ops took 2.619 sec, avg 26.190 ms, 38.183 ops/sec +ML-DSA 44 key gen 600 ops took 1.107 sec, avg 1.844 ms, 542.217 ops/sec +ML-DSA 44 sign 200 ops took 1.144 sec, avg 5.719 ms, 174.842 ops/sec +ML-DSA 44 verify 600 ops took 1.146 sec, avg 1.910 ms, 523.569 ops/sec +ML-DSA 65 key gen 400 ops took 1.267 sec, avg 3.167 ms, 315.744 ops/sec +ML-DSA 65 sign 200 ops took 1.687 sec, avg 8.436 ms, 118.543 ops/sec +ML-DSA 65 verify 400 ops took 1.272 sec, avg 3.180 ms, 314.428 ops/sec +ML-DSA 87 key gen 200 ops took 1.066 sec, avg 5.331 ms, 187.588 ops/sec +ML-DSA 87 sign 100 ops took 1.162 sec, avg 11.617 ms, 86.084 ops/sec +ML-DSA 87 verify 200 ops took 1.077 sec, avg 5.385 ms, 185.704 ops/sec +Benchmark complete +``` + +## Source files + +`hal/mpfs250.c` - Hardware abstraction layer (UART, QSPI, SD/eMMC, multi-hart) +`hal/mpfs250.h` - Register definitions and hardware interfaces +`hal/mpfs250_ddr.c` - LPDDR4 PHY/PLL/training and DDR bring-up (M-mode, no HSS) +`src/ddr_cadence.c` - Generic Cadence DDR controller driver (controller CSR +programming, Memory Test Controller, LPDDR4 mode-register protocol) +`include/ddr_cadence.h` - Generic Cadence DDR controller register map + interface +`hal/mpfs250.ld` - Linker script for S-mode (HSS-based boot) +`hal/mpfs250-m.ld` - Linker script for M-mode (eNVM + L2 SRAM) +`hal/mpfs250-hss.ld` - Linker script for S-mode (HSS with L2-LIM) +`hal/mpfs250_ubl.c` / `hal/mpfs250_ubl.ld` - Boot mode 2 User Boot Loader stub (sNVM-authenticated verifier of the eNVM wolfBoot) +`hal/mpfs.dts` - Device tree source +`hal/mpfs.yaml` - HSS payload generator configuration for use of DDR +`hal/mpfs-l2lim.yaml` - HSS payload generator for the use of L2-LIM +`hal/mpfs250.its` - Example FIT image creation template +`hal/mpfs250_snvm.c` / `hal/mpfs250_snvm.h` - System Controller mailbox, sNVM keystore, PUF KEK +and key-wrap +`src/riscv_sbi.c` / `include/riscv_sbi.h` - minimal SBI runtime and the vendor extension +definitions +`tools/linux/wolfboot-sbi/` - the Linux consumer of the vendor extension (`/dev/wolfboot`) +`tools/scripts/mpfs_bootmode2.sh` - sNVM and boot-mode programming for the secure dies +`tools/scripts/mpfs_qspi_prog.py` - UART QSPI flash programmer +`config/examples/polarfire_mpfs250*.config` - the seven configuration templates + +## SBI vendor extension for the OS + +The M-mode wolfBoot stays resident as the SBI for the OS. With `MPFS_SBI_VENDOR=1` it also serves the Microchip vendor extension, EID `0x09000029` (`SBI_EXT_VENDOR_START | mvendorid 0x029`), the same number the Hart Software Services register, so an OS probes one extension for either firmware and tells them apart with `GET_VERSION`. The HSS function IDs for inter-hart communication, remoteproc, crypto and IPC are answered with `SBI_ERR_NOT_SUPPORTED`; `HSS_REBOOT` (0x10) performs a cold MSS reset. + +The definitions live in `include/riscv_sbi.h`; the handler is `sbi_hal_vendor_ecall()` in `hal/mpfs250.c`. Arguments travel in `a0..a2`, the SBI error returns in `a0` and the value in `a1`. Buffers are addresses in the caller's address space: M-mode reads and writes them through the caller's translation, byte by byte, with a trap vector that turns a bad address into `SBI_ERR_INVALID_ADDRESS` instead of a stopped hart. A buffer is at most 4096 bytes and, while the caller runs with paging off, must lie in DDR. + +| FID | Call | Arguments | Result | +|-----|------|-----------|--------| +| 0x40 | `GET_VERSION` | - | `a1` = 1 | +| 0x41 | `GET_SERIAL` | `a0` buffer, `a1` length >= 16 | the 16-byte device serial number | +| 0x42 | `GET_KEY` | `a0` purpose, `a1` buffer, `a2` length == 32 | a PUF-derived key; each purpose is served once per boot, a second call returns `SBI_ERR_DENIED` | +| 0x43 | `DIGEST_CHECK` | `a0` options word | starts the System Controller digest check | +| 0x44 | `IAP_AUTHENTICATE` | `a0` SPI directory index | starts IAP image authentication | +| 0x45 | `IAP_VERIFY` | `a0` SPI directory index | starts IAP verify | +| 0x46 | `IAP_PROGRAM` | `a0` SPI directory index | the device restarts on success; a return is the failure status in `a1` | +| 0x47 | `AUTO_UPDATE` | - | the device restarts on success; a return is the failure status in `a1` | +| 0x48 | `SC_FLASH_ERASE` | `a0` flash offset, `a1` length == one erase block (64 KB), both block aligned | erases that block of the System Controller's SPI flash; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x49 | `SC_FLASH_WRITE` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | programs the bytes; `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4A | `SC_FLASH_READ` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | reads the bytes (any offset, the directory included); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4B | `SC_FLASH_SET_IMAGE` | `a0` directory index 1 (upgrade) or 2 (blank), `a1` image offset | rewrites the SPI directory block with that entry changed (entry 1 also zeroes entry 2); `MPFS_SBI_SC_FLASH=1` builds only | +| 0x4F | `SC_STATUS` | - | `SBI_ERR_ALREADY_STARTED` while the started service runs, else `a1` = 16-bit status with mailbox word 1 (the digest-check `DIGESTERR` bits) in the upper half | + +`GET_KEY` purposes name a fixed label; the caller never supplies a challenge, so the OS cannot ask the PUF for wolfBoot's own key-encryption key. Purpose 1 is the MRAM log key (`SHA-384("wolfBoot-mram-log-v1" || PUF(SHA-384("wolfBoot-mram-log-v1")[0..15]))[0..31]`); purpose 2 reproduces the earlier user-space `varlog` derivation. The key is device-bound: the same whether the golden or an operational image booted. + +The System Controller services (0x43-0x4F) are served only in a build with `MPFS_SCB_SMODE_DENY=1`, where the OS cannot reach the mailbox itself; in an unfenced build they return `SBI_ERR_DENIED` and the OS uses its own mailbox drivers, which the fence would otherwise race. The long-running ones are started inside the ecall and polled through `SC_STATUS`, because an ecall holds its hart in M-mode with interrupts off. The mailbox is one resource for all harts: a call that finds it taken returns `SBI_ERR_FAILED`. + +The fence also covers the System Controller's QSPI flash controller, so a fenced OS cannot stage a bitstream the way the mainline `mpfs-auto-update` driver does (through `mtd` on that controller). `MPFS_SBI_SC_FLASH=1` adds the flash calls (0x48-0x4B) for that build: the OS erases and writes the image region directly, reads back what it wants verified, and points the SPI directory's upgrade entry at the image with `SC_FLASH_SET_IMAGE` before `IAP_AUTHENTICATE` / `IAP_PROGRAM` or `AUTO_UPDATE`. The first erase block (the directory and the start of the design-info area) is never erased or written through `SC_FLASH_ERASE` / `SC_FLASH_WRITE`; `SET_IMAGE` reads it, changes the one entry (the golden entry 0 stays what it was) and writes it back, the same read-erase-write the OS driver performs. Each erase call takes one block and each transfer at most 4 KB, so no ecall holds its hart for more than the erase time of one block; the OS side loops. The flash layout the mainline driver uses (directory in the first 1 KB, design info at 1 KB, bitstream slots from 1 MB + 1 KB) is the OS's business; wolfBoot only protects the directory block. + +The Linux side is the `wolfboot-sbi` module in `tools/linux/wolfboot-sbi/`: `/dev/wolfboot` (root only) with one ioctl per call, see `wolfboot_sbi.h`. It probes the extension and `GET_VERSION`, so it unloads cleanly under the HSS, and it never touches the mailbox registers, so the mainline `mpfs-sys-controller`, `mpfs-auto-update` and `mpfs-rng` drivers keep working in an unfenced build. A caller of `IAP_PROGRAM` or `AUTO_UPDATE` syncs its filesystems first: the device restarts inside the call. + +Build: `MPFS_SBI_VENDOR=1` on the M-mode target (`RISCV_MMODE=1`); the key derivation is SHA-384 whatever the image hash. + +## Hardware root of trust: PUF KEK, sNVM keystore, wrapped image key + +The PolarFire SoC MPFS250 hardware-root-of-trust features belong to the wolfBoot E51 M-mode boot flow ([M-mode + DDR](#m-mode--ddr-booting-linux-minimal-sbi)). They let wolfBoot anchor key material in the chip's System Controller instead of compiling it into the bootloader image: + +1. A System Controller mailbox driver for secure NVM (sNVM), the SRAM-PUF, and the nonce/TRNG service. +2. An sNVM keystore backend that serves the verification public keys from sNVM at boot. +3. A device-unique Key Encryption Key (KEK) derived from the SRAM-PUF, used to wrap/unwrap individual keys. +4. A PUF-wrapped image-encryption key: the AES image key is stored in sNVM wrapped by the PUF KEK and unwrapped at boot, with the bulk image decrypted on the M-mode disk-boot path. + +All features are off by default and gated behind build flags, so a normal build is unaffected. + +### Trust model + +The intended chain, from the immutable hardware up: + +``` +PolarFire boot ROM (secure boot of wolfBoot: not configured on the Video Kit, bootmode 1) + -> wolfBoot (M-mode, eNVM) + -> System Controller SRAM-PUF (fixed challenge) -> SHA-384 -> 256-bit KEK + -> sNVM keystore: ECC384 verification public key(s) + -> sNVM: AES-256 image key, wrapped by the PUF KEK + -> verify + decrypt the signed/encrypted Yocto FIT + -> 4-CPU SMP Linux +``` + +The KEK is re-derived on every boot and never leaves the device. The public keys are not secret; storing them in sNVM (rather than the image) lets the trust anchor be provisioned and rotated per device, and binds verification to the System Controller. + +#### Production requirement: the keystore pages must not stay writable + +The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: + +- Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. +- Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them, so any unit that reads a key or the serial number through the mailbox must be dropped from the image or moved to the [SBI vendor extension](#sbi-vendor-extension-for-the-os). + +### 1. System Controller mailbox services + +`hal/mpfs250.c` drives the System Controller services mailbox (`SCBCTRL` at `0x37020000`, mailbox RAM at `0x37020800`). A small generic `mpfs_scb_request()` issues a service and polls the request/busy bits; `mpfs_scb_read()` reads the response. The following services are wrapped: + +| Service | Opcode | wolfBoot wrapper | +|---------|--------|------------------| +| Device serial number | `0x00` | `mpfs_read_serial_number()` | +| sNVM write (plaintext / authenticated / ciphertext) | `0x10` / `0x11` / `0x12` | `mpfs_snvm_write()` | +| sNVM read | `0x18` | `mpfs_snvm_read()` | +| PUF emulation | `0x20` | `mpfs_puf_emulation()` | +| Nonce (TRNG) | `0x21` | `mpfs_nonce()` | + +sNVM holds 221 modules of 252 bytes (plaintext) or 236 bytes plus a 12-byte User Secret Key (authenticated). The PUF emulation service returns a 32-byte device-unique response for a 16-byte challenge; for a fixed challenge the response is stable within a boot and across cold boots, which is what makes it usable as a KEK seed. The nonce service returns 32 random bytes. + +Note: the TRNG (nonce) and PUF services take far longer than serial/sNVM-read, so the busy-completion wait uses a larger bound (`MPFS_SCB_BUSY_TIMEOUT`) than the request-accept wait (`MPFS_SCB_TIMEOUT`). + +### 2. sNVM public-key keystore (`SNVM_KEYSTORE`) + +`hal/mpfs250_snvm.c` + `hal/mpfs250_snvm.h` implement the five-function keystore API (`keystore_num_pubkeys`, `keystore_get_buffer`, `keystore_get_size`, `keystore_get_mask`, `keystore_get_key_type`) by reading the trust anchor from sNVM. The on-sNVM layout is the same family as the OTP keystore (`src/flash_otp_keystore.c`): a `WOLFBOOT`-magic header followed by packed `struct keystore_slot` entries, read through a linear reader that spans consecutive sNVM modules from `SNVM_KEYSTORE_MODULE` (default 200, `SNVM_KEYSTORE_MAX_MODULES` default 1; an ML-DSA-87 key needs 11). For the on-device provisioning writer every module in the range must be left runtime-writable (not ROM) in the Libero sNVM configuration: on the stock Video Kit design only modules 200 and 201 are. A production bitstream delivers the same page contents as ROM instead (see the trust model above). + +Build wiring (mirrors `FLASH_OTP_KEYSTORE`): + +- `SNVM_KEYSTORE=1` links `hal/mpfs250_snvm.o` in place of the compiled-in `src/keystore.o`, so the running bootloader contains no public keys and serves them from sNVM. +- `SNVM_KEYSTORE_PROVISION=1` keeps the compiled keystore and adds `snvm_keystore_provision()`, a one-time on-device writer that copies the compiled keystore image into sNVM (called from `hal_init`). Run once on a board, then deploy the `SNVM_KEYSTORE=1` build. Both provisioning writers read the page first and skip it when it already holds the content (`snvm: module N already provisioned`), so the extra boot the programmer triggers after flashing, or a second boot of the provisioning image, does not spend another write cycle. + +A keystore page that reads back as all zeros is reported as `sNVM keystore zeroized` and halts the boot. That is the state a zeroize procedure leaves behind (the OS overwrites the keystore and encryption-key pages through the System Controller's plaintext sNVM write service), and it is distinguished from a corrupt header, which merely yields no keys, so a deliberately erased unit is identifiable on the console. Recovery is the provisioning build again. + +### 3. PUF KEK and AES key-wrap (`SNVM_KEK`) + +The KEK and key-wrap live in `hal/mpfs250_snvm.c` as well: + +- `mpfs_puf_kek()` derives the 256-bit KEK as `SHA-384(label || PUF(fixed-challenge))` truncated to 32 bytes. SHA-384 is used because it is the build's hash (SHA-256/HMAC/HKDF are compiled out); the PUF response is already a high-entropy device-unique secret, so a labelled hash provides domain separation without pulling in HKDF. +- `snvm_kek_wrap()` / `snvm_kek_unwrap()` wrap/unwrap a key with the KEK using RFC 3394 AES key-wrap (`wc_AesKeyWrap` / `wc_AesKeyUnWrap`). + +`SNVM_KEK=1` adds RFC 3394 key-wrap to the AES that an `ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1` build already compiles; the KEK has no other consumer, so the build refuses the flag without that combination. Determinism and the wrap/unwrap round trip are covered by the host unit test `tools/unit-tests/unit-snvm-kek.c`. + +The AES key-wrap uses the wolfCrypt software implementation. The Athena F5200 offload (`MPFS_ATHENA=1`) serves SHA-384 and the bulk AES-256-CTR image decrypt through wolfCrypt crypto callbacks; the key-wrap is a few blocks per boot and stays in software. + +### 4. PUF-wrapped image-encryption key + +`hal/mpfs250_snvm.c` provides `wolfBoot_get_encrypt_key()` (under `CUSTOM_ENCRYPT_KEY`): it reads `[wrapped AES key (40 B)][nonce (16 B)]` from sNVM module `SNVM_ENCKEY_MODULE` (default 201, outside the keystore range), unwraps the key with the PUF KEK, and returns the plaintext key + nonce to the decrypt path. `snvm_enckey_provision()` (under `SNVM_ENCKEY_PROVISION`) wraps a key with the device KEK and writes the blob to sNVM. + +#### Disk-image decryption enablement + +Image encryption (`EXT_ENCRYPTED`) needs `EXT_FLASH`, `MMU` or `CUSTOM_ENCRYPT_KEY`. The M-mode disk-FIT path is `WOLFBOOT_NO_PARTITIONS` and has neither external flash nor an MMU, so it qualifies through `CUSTOM_ENCRYPT_KEY`: the platform supplies the key and `hal_set_key()`, the partition-resident key store, is compiled out (`src/libwolfboot.c`). The `DISK_ENCRYPT` path in `src/update_disk.c` zeroes its key material with `ForceZero` on every exit. + +### Field-recoverable zeroize + +`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. + +### Build flags summary + +| Flag | Effect | +|------|--------| +| `SNVM_KEYSTORE` | Serve the public-key trust anchor from sNVM (replaces compiled keystore) | +| `SNVM_KEYSTORE_PROVISION` | Add the one-time on-device keystore writer | +| `SNVM_KEK` | Enable the PUF KEK + RFC 3394 AES key-wrap (needs the AES-256 `ENCRYPT` + `CUSTOM_ENCRYPT_KEY` build) | +| `SNVM_ENCKEY_PROVISION` | Add the one-time PUF-wrapped encryption-key writer | +| `SNVM_KEYSTORE_MODULE` / `SNVM_KEYSTORE_MAX_MODULES` / `SNVM_ENCKEY_MODULE` | sNVM module numbers (defaults 200 / 1 / 201; the key module must lie outside the keystore range and be runtime-writable) | +| `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | +| `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | +| `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | +| `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | +| `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | +| `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | + +Example runtime encrypted build (compiled keystore for verification, PUF-wrapped key for decryption): + +```sh +make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR= \ + ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 wolfboot.elf +``` + +### Provisioning and validation recipe + +1. Build with `SNVM_ENCKEY_PROVISION=1` (and/or `SNVM_KEYSTORE_PROVISION=1`) plus `WOLFBOOT_SNVM_WRITE_APPROVED=1` added to the runtime build above, flash, and boot once: `hal_init` writes the PUF-wrapped AES key into sNVM[201] (and/or the keystore into sNVM[200]). +2. Sign + encrypt the FIT with a key file matching the provisioned key (`prov_aes_key` || `prov_aes_nonce` in `hal/mpfs250_snvm.c`, i.e. a 32-byte key followed by a 16-byte IV): + + ```sh + IMAGE_HEADER_SIZE=512 ./tools/keytools/sign --ecc384 --sha384 \ + --encrypt enc_key.bin --aes256 fitImage wolfboot_signing_private_key.der 1 + ``` + Note: `make keysclean` regenerates the signing key, so re-sign the image after any keysclean or the compiled keystore will not match. +3. Write `fitImage_v1_signed_and_encrypted.bin` to the SD boot partition. With the stock HSS, boot to the `>>` CLI, run `usbdmsc`, and `dd` to the first partition of the exposed disk. Do not move an SD-Wire mux while `usbdmsc` is running: HSS's MMC layer then serves one stale block for every LBA and does not recover until `usbdmsc` is restarted, which makes a populated card read as blank. +4. Flash the runtime build and cold-boot. Expected UART0: `Disk encryption enabled` -> `Decrypting image... done` -> `Firmware Valid.` -> `M->S handoff`, and UART1 reaches the Linux login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. + +### Sample validation logs (MPFS250TS Video Kit) + +The PUF response is identical for a fixed challenge across cold power cycles while the nonce service is random, and the PUF-derived KEK wraps and unwraps a test key deterministically (the same wrapped bytes on every boot); both were verified on the kit and the KEK logic is covered by `tools/unit-tests/unit-snvm-kek.c`. + +Full encrypted boot captured on the Video Kit (UART1, S-mode wolfBoot under HSS; the standalone M-mode target decrypts the same image in place in DDR): the AES key is unwrapped from sNVM with the PUF KEK, the image is loaded, decrypted and verified. + +``` +wolfBoot Version: 2.10.0 +Disk encryption enabled +... +Load address 0x8E000000 +Attempting boot from P:A +Loading image from disk...done +Decrypting image...done +Checking image integrity...done +Verifying image signature...done +Firmware Valid. +Booting at 8E000000 +PolarFire SoC MPFS250 wolfBoot demo Application +``` + +Linux (UART1): + +``` +[ 0.040726] smp: Brought up 1 node, 4 CPUs +OpenEmbedded nodistro.0 mpfs-video-kit ttyS1 +mpfs-video-kit login: +``` + +No `failed to come online`; the only error lines are the known-cosmetic eth-SGMII PHY, i2c clock-divider, and mmc-tuning messages unrelated to this feature. diff --git a/docs/README.md b/docs/README.md index cbe7a1f33a..67be49e2c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ See also: [wolfBoot Product Overview](https://www.wolfssl.com/products/wolfboot/ - [**Loader.md**](./Loader.md) - Loader/secondary stage behavior and handoff to application. - [**measured_boot.md**](./measured_boot.md) - Measured boot concepts and recording measurements (e.g., PCRs). - [**png/**](./png) - Folder of images/diagrams referenced by the documentation. +- [**PolarFire.md**](./PolarFire.md) - Microchip PolarFire SoC: quick start, boot configurations, sNVM/PUF key storage, SBI vendor extension, boot mode 2. - [**PQ.md**](./PQ.md) - Post-quantum algorithms and PQC support in wolfBoot. - [**README.md**](./README.md) - Overview and index of the documentation set. - [**remote_flash.md**](./remote_flash.md) - Working with external/remote flash (SPI/QSPI, mapping, access). diff --git a/docs/Targets.md b/docs/Targets.md index 711a58fec7..eb1c8c2645 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -880,22 +880,11 @@ add-symbol-file test-app/image.elf 0x20020100 ## Microchip PolarFire SoC -> **Filesystem boot:** this target can also read its signed image from a **file** on a read-only FAT32 or ext4 partition instead of from raw offset 0. Build with `DISK_FS=fat32|ext4|both` and set `BOOT_FILE_A` / `BOOT_FILE_B`. See [Disk boot from a read-only filesystem (FAT32 / ext4)](compile.md#disk-boot-from-a-read-only-filesystem-fat32--ext4). - -The PolarFire SoC is a 64-bit RISC-V SoC featuring a five-core CPU cluster (1× E51 monitor core and 4× U54 application cores) and FPGA fabric. Tested with MPFS250. - -### Features -* RISC-V 64-bit architecture (rv64imac) -* Five-core CPU: 1× E51 monitor + 4× U54 application cores -* Integrated DDR3/4, LPDDR3/4 controller and PHY -* PCIe Gen2, USB 2.0, and Gigabit Ethernet interfaces -* Secure boot capabilities -* Low power consumption -* External flash support +The PolarFire SoC is a 64-bit RISC-V SoC with a five-core CPU cluster (1x E51 monitor core and 4x U54 application cores) and FPGA fabric, tested here on MPFS250. wolfBoot runs on it either as a payload of Microchip's Hart Software Services (HSS) or as a complete replacement for it: in M-mode from eNVM it brings up LPDDR4, loads and verifies a signed image from SD card or QSPI, hands a U54 into S-mode and stays resident as the SBI the OS runs on. -### Supported Boot Configurations +**[PolarFire.md](PolarFire.md) is the documentation for this target** - a quick start that takes one build, one card and one eNVM program end to end, then every configuration in detail, QSPI and DDR notes, boot mode 2, sNVM/PUF key storage, the SBI vendor extension for the OS, Yocto, debugging and benchmarks. The two tables below are the summary. -Six ready-to-use config templates cover all supported boot mode / storage / memory combinations: +### PolarFire SoC boot configurations | Configuration | Config File | Boot Mode | Storage | Memory | HSS | |---------------|-------------|-----------|---------|--------|-----| @@ -905,1044 +894,34 @@ Six ready-to-use config templates cover all supported boot mode / storage / memo | **QSPI + L2-LIM** | `polarfire_mpfs250_hss_l2lim.config` | S-mode (U54 via HSS) | SC QSPI | L2-LIM (no DDR) | Yes | | **M-Mode (no HSS)** | `polarfire_mpfs250_m_qspi.config` | M-mode (E51, no HSS) | SC QSPI | L2 Scratchpad | No | | **M-Mode + DDR** | `polarfire_mpfs250_m.config` | M-mode (E51, no HSS) | SD Card | LPDDR4 (DDR) | No | +| **Boot mode 2 UBL** | `polarfire_mpfs250_ubl.config` | System Controller -> sNVM stub | eNVM (verifies it) | L2-LIM | No | + +### PolarFire SoC build options + +Make variables, on the command line or in the `.config`. See [PolarFire.md](PolarFire.md) for what each one does and what it requires. + +| Option | Purpose | +|--------|---------| +| `LIBERO_FPGA_CONFIG_DIR` | Path to the board's Libero/HSS `fpga_design_config` headers. Enables LPDDR4 init (`MPFS_DDR_INIT`); required for the M-Mode + DDR build. | +| `MPFS_ATHENA` (+ `MPFS_ATHENA_CAL_DIR`) | SHA-384 and AES-256-CTR offload to the Athena F5200 (TeraFire), with a mandatory self-check. | +| `SNVM_KEYSTORE`, `SNVM_KEYSTORE_PROVISION`, `SNVM_KEYSTORE_REQUIRE_ROM` | Serve the verification keystore from secure NVM; the one-time writer; refuse to boot unless every keystore page is ROM-flagged. | +| `SNVM_KEK`, `SNVM_ENCKEY_PROVISION` | PUF-derived KEK with RFC 3394 key-wrap for the sNVM-stored image-encryption key, and its one-time writer. | +| `MPFS_SCB_SMODE_DENY` | Fence S-mode off the System Controller mailbox (PMP plus dtb node removal), so nothing after wolfBoot can reach the sNVM write service. | +| `MPFS_SBI_VENDOR` | SBI vendor extension for the OS: firmware version, device serial, PUF-derived keys by purpose, and the System Controller services behind the fence. | +| `MPFS_SBI_SC_FLASH` | Adds System Controller SPI flash erase, write, read and directory update over that extension, for staging a bitstream from a fenced OS. | +| `MPFS_ENVM_MANIFEST` | Reserve the last eNVM page for the signed manifest the boot mode 2 stub verifies. | +| `DISK_GOLDEN_SLOT`, `DISK_BOOT_TRIES`, `DISK_FAILOVER_ZEROIZE` | Golden slot tried after A/B, unconfirmed-boot budget, and zeroizing the image key and slot heads before the golden boot. | +| `MPFS_QSPI_CLK_DIV` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` (default `QSPI_CLK_DIV_8`). | +| `MPFS_WDT_NODES_KEEP` | Leave the MSS watchdog nodes enabled and stop the parked E51 refreshing them, so the OS owns the watchdogs. | +| `WATCHDOG`, `WATCHDOG_TIMEOUT_MS` | Keep the E51 watchdog running during wolfBoot itself, with the given timeout (default 30 s). | + +> **Note:** the MPFS250 U54/E51 cores have no RISC-V crypto extensions (Zknh), so none of the +> configurations use them. The SD-card, QSPI and M-Mode + DDR configs build the portable RISC-V +> SHA assembly (`NO_ASM=0`); the size-constrained L2-LIM and M-mode QSPI configs set `NO_ASM=1` +> and use the C implementations. -The **M-Mode + DDR** configuration brings up the LPDDR4 controller from -the E51 in M-mode (no HSS), then loads a signed FIT image from SD card, -verifies it (SHA384 + ECC384) and hands off to a U54 hart in S-mode. -wolfBoot includes a minimal SBI runtime (`src/riscv_sbi.c`) so the -hand-off target can be a Linux kernel: tested booting 4-CPU SMP Yocto -Linux to a login prompt in ~40 s from power-on on the MPFS250T Video -Kit. Because all -LIBERO_SETTING_\* values are board-specific, this build pulls them from -a Libero/HSS-generated `fpga_design_config.h` pointed at by the -`LIBERO_FPGA_CONFIG_DIR` makefile variable - typical sources are an -HSS Video Kit build at -`/build/boards/mpfs-video-kit/fpga_design_config` or a Libero MSS -Configurator export. Setting `LIBERO_FPGA_CONFIG_DIR` automatically -defines `MPFS_DDR_INIT` and adds the directory to the include path -(see `arch.mk`); when unset, the DDR HAL is excluded and the build -still produces a working M-mode wolfBoot without DDR. Add -`-DDEBUG_DDR` to `CFLAGS_EXTRA` for verbose register-level traces -during bring-up. - -For GitHub Actions, `tools/ci/gen_mpfs_libero_stub.sh` generates a -compile-only stub (every `LIBERO_SETTING_*` symbol referenced by the -HAL, defined to `0`; loop-bound settings to `1U`) so the M-Mode + DDR -build path stays under continuous integration. The generated header is -**not** committed and **not** runnable - real boards must point -`LIBERO_FPGA_CONFIG_DIR` at the actual Libero / HSS output. - -Key build settings that differ between configurations: - -| Setting | SDCard | eMMC | QSPI | L2-LIM | M-Mode | M-Mode + DDR | -|---------|--------|------|------|--------|--------|--------------| -| `WOLFBOOT_ORIGIN` | `0x80000000` | `0x80000000` | `0x80000000` | `0x08040000` | `0x0A000000` | `0x0A000000` | -| `WOLFBOOT_LOAD_ADDRESS` | `0x8E000000` | `0x8E000000` | `0x8E000000` | `0x08060000` | `0x0A010200` | `0x82000000` | -| `WOLFBOOT_LOAD_DTS_ADDRESS` | `0x8A000000` | `0x8A000000` | `0x8A000000` | – | – | `0x8A000000` | -| `EXT_FLASH` | 0 | 0 | 1 | 1 | 1 | 0 | -| `DISK_SDCARD` | 1 | 0 | 0 | 0 | 0 | 1 | -| `DISK_EMMC` | 0 | 1 | 0 | 0 | 0 | 0 | -| `MPFS_L2LIM` | – | – | – | 1 | – | – | -| `RISCV_MMODE` | – | – | – | – | 1 | 1 | -| `LIBERO_FPGA_CONFIG_DIR` | – | – | – | – | – | required | -| `WOLFBOOT_MMODE_SMODE_BOOT` | – | – | – | – | – | 1 | -| Linker script | `mpfs250.ld` | `mpfs250.ld` | `mpfs250.ld` | `mpfs250-hss.ld` | `mpfs250-m.ld` | `mpfs250-m.ld` | -| HSS YAML | `mpfs.yaml` | `mpfs.yaml` | `mpfs.yaml` | `mpfs-l2lim.yaml` | N/A | N/A | -| `ELF` output | 1 | 1 | 1 | 0 (raw .bin) | 1 | 1 | - -> **Note:** All configurations require `NO_ASM=1` because the MPFS250 U54/E51 cores lack RISC-V -> crypto extensions (Zknh); wolfBoot uses portable C implementations for all cryptographic operations. - -### M-Mode Optional Build Flags - -These flags apply to `polarfire_mpfs250_m_qspi.config` and are added via `CFLAGS_EXTRA+=-D...`. - -| Flag | Default | Description | -|------|---------|-------------| -| `WATCHDOG` | undefined (disabled) | When defined, the E51 watchdog timer is **kept enabled** during wolfBoot operation with a generous timeout. When undefined, the WDT is **disabled** in `hal_init()` and re-enabled with the boot ROM default in `hal_prepare_boot()` before jumping to the application. Either way, the application receives a normal WDT. | -| `WATCHDOG_TIMEOUT_MS` | `30000` (30 s) | Watchdog timeout in milliseconds when `WATCHDOG` is defined. ECDSA P-384 verification on E51 with portable C math is bounded at ~5 s; the default 30 s avoids any need to refresh the WDT during the long verify call. | -| `MPFS_WDT_NODES_KEEP` (make variable, `=1`) | undefined | Linux boot only: keep the five MSS watchdog nodes enabled in the dtb and stop the parked E51 from refreshing them, so the OS owns the watchdogs. See "Watchdog policy" below. | -| `MPFS_QSPI_CLK_DIV` (make variable) | `QSPI_CLK_DIV_8` | QSPI SCK divider, a `QSPI_CLK_DIV_*` name from `hal/mpfs250.h`. See "PolarFire SoC QSPI". | - -#### Stack overflow detection - -The trap handler in `src/boot_riscv.c` automatically detects stack overflow on synchronous exceptions (requires `DEBUG_BOOT`). When a trap fires with `SP < _main_hart_stack_bottom`, it prints: - -``` -TRAP: cause=2 epc=A000740 tval=0 - sp=A02FFE8 -STACK OVERFLOW: under by 24 -``` - -This is helpful for diagnosing illegal-instruction TRAPs at random valid `.text` addresses, which are the classic signature of stack overflow corrupting the return address. - -The current `STACK_SIZE` in `hal/mpfs250-m.ld` is **32 KB**. Measured peak for ECC384 + SHA384 + SPMATHALL + NO_ASM is ~6 KB (5x headroom). - -### PolarFire SoC Files - -`hal/mpfs250.c` - Hardware abstraction layer (UART, QSPI, SD/eMMC, multi-hart) -`hal/mpfs250.h` - Register definitions and hardware interfaces -`hal/mpfs250_ddr.c` - LPDDR4 PHY/PLL/training and DDR bring-up (M-mode, no HSS) -`src/ddr_cadence.c` - Generic Cadence DDR controller driver (controller CSR -programming, Memory Test Controller, LPDDR4 mode-register protocol) -`include/ddr_cadence.h` - Generic Cadence DDR controller register map + interface -`hal/mpfs250.ld` - Linker script for S-mode (HSS-based boot) -`hal/mpfs250-m.ld` - Linker script for M-mode (eNVM + L2 SRAM) -`hal/mpfs250-hss.ld` - Linker script for S-mode (HSS with L2-LIM) -`hal/mpfs250_ubl.c` / `hal/mpfs250_ubl.ld` - Boot mode 2 User Boot Loader stub (sNVM-authenticated verifier of the eNVM wolfBoot) -`hal/mpfs.dts` - Device tree source -`hal/mpfs.yaml` - HSS payload generator configuration for use of DDR -`hal/mpfs-l2lim.yaml` - HSS payload generator for the use of L2-LIM -`hal/mpfs250.its` - Example FIT image creation template - -### PolarFire SoC Building wolfBoot - -All build settings come from .config file. For this platform use `TARGET=mpfs250` and `ARCH=RISCV64`. - -See example configuration at `config/examples/polarfire_mpfs250.config`. - -```sh -# Setup .config (build settings) -cp config/examples/polarfire_mpfs250.config .config - -# build boot loader -make wolfboot.elf -``` - -To assemble this as a flashable image you need the 0x100 byte HART header added: - -```sh -git clone https://github.com/polarfire-soc/hart-software-services.git -cd hart-software-services -cd tools/hss-payload-generator -make -# install tool -sudo cp hss-payload-generator /usr/local/bin/ -``` - -The HSS MMC boot source looks for GPT with GUID "21686148-6449-6E6F-744E-656564454649" or sector "0" if no GPT found. That GUID is the default "BIOS" boot partition. - -The resulting image from `hss-payload-generator` can be directly placed into GPT BIOS partition. - -Use this command to assemble a bootable wolfboot image: - -```sh -hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot.bin -``` - -You must generated the Device Tree Binary using: - -```sh -dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb` -``` - -Example one-shot command: - -```sh -cp ./config/examples/polarfire_mpfs250.config .config && make clean && make wolfboot.elf && size wolfboot.elf && dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb && hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot.bin -``` - -The HSS tinyCLI supports the `USBDMSC` command to mount the eMMC or SD card as a USB device. You can then use "dd" to copy the boot image to the BOOT partition. Use `lsblk` to locate the boot partition and replace /dev/sdc1 in the example: - -```sh -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 -``` - -### PolarFire SoC QSPI - -PolarFire SoC has two CoreQSPI v2 controllers with identical register layouts. The selection -is made at build time via `MPFS_SC_SPI` and affects which QSPI base address wolfBoot uses: - -```text - +-------------------+ +----------------------+ - | U54 cores | | U54 cores | - | (wolfBoot) | | (wolfBoot) | - +---------+---------+ +----------+-----------+ - | | - | direct register access | direct register access - | (MSS QSPI @ 0x2100_0000) | (SC QSPI @ 0x3702_0100) - v v - +-------------------+ +----------------------+ - | MSS QSPI IP | | SC QSPI IP | - | (CoreQSPI v2) | | (CoreQSPI v2) | - +---------+---------+ +----------+-----------+ - | | - v v - External QSPI flash Fabric-connected flash -``` - -Build options: - -- MSS QSPI controller (direct register access at 0x21000000, read/write/erase) - - `EXT_FLASH=1` - - Do not set `MPFS_SC_SPI` - - Example config: `config/examples/polarfire_mpfs250_qspi.config` with `CFLAGS_EXTRA` line removed. - -- SC QSPI controller (direct register access at 0x37020100, read/write/erase) - - `EXT_FLASH=1` - - `CFLAGS_EXTRA+=-DMPFS_SC_SPI` - - Example config: `config/examples/polarfire_mpfs250_qspi.config` as-is. - - Both controllers share the same CoreQSPI v2 register interface. - The only difference is that SC QSPI does not need MSS clock/reset setup. - -Example single-shot build: `cp config/examples/polarfire_mpfs250_qspi.config .config && make clean && make wolfboot.bin && hss-payload-generator -vvv -c ./hal/mpfs.yaml wolfboot.bin && make test-app/image.elf && ./tools/keytools/sign --ecc384 --sha384 test-app/image.elf wolfboot_signing_private_key.der 1` - -Notes: -- Both modes support full read, write, and erase operations. -- SCK is the controller clock divided by the CoreQSPI `CLKRATE` field, SCK = PCLK / (2 * CLKRATE). - `MPFS_QSPI_CLK_DIV` selects the divider by its `QSPI_CLK_DIV_*` name from `hal/mpfs250.h` - (`QSPI_CLK_DIV_2` through `QSPI_CLK_DIV_30`, CLKRATE 1..15). The default `QSPI_CLK_DIV_8` is - about 18.75 MHz from the 150 MHz APB clock, where read throughput stops improving: from there on - the polled FIFO transfer rather than SCK sets the rate, so a faster SCK buys nothing. The MSS QSPI - PCLK is the MSS APB clock, which is 40 MHz at the M-mode reset clock until wolfBoot's PLL - bring-up, so an M-mode build that starts QSPI before `mss_pll_init()` runs SCK that much slower; - the SC QSPI is clocked by the System Controller and is unaffected by the MSS PLL. -- For QSPI-based boot flows, disable SD/eMMC in the config (`DISK_SDCARD=0`, `DISK_EMMC=0`) unless you - explicitly want wolfBoot to load from disk and the application from QSPI. -- The MSS QSPI path expects external flash on the MSS QSPI pins; the SC QSPI path is for - fabric-connected flash (design flash) accessed via the System Controller's QSPI instance. - -### PolarFire SoC HSS S-Mode with L2-LIM (no DDR) - -wolfBoot can run in S-mode via HSS without DDR by targeting the on-chip **L2 Loosely Integrated -Memory (L2-LIM)**. HSS loads wolfBoot into L2-LIM on a U54 application core, and wolfBoot loads -the signed application from SC QSPI into L2-LIM as well. This is the configuration for systems -that keep HSS and run without DDR. - -**Features:** -* S-mode on U54 application core (hart 1), loaded by HSS -* wolfBoot and application both reside in L2-LIM (`0x08000000`, up to 1.5 MB) -* No DDR required -* SC QSPI flash for both wolfBoot payload and signed application image -* Raw binary output (`ELF=0`) required — ELF with debug symbols is too large for L2-LIM - -**Relevant files:** - -| File | Description | -|------|-------------| -| `config/examples/polarfire_mpfs250_hss_l2lim.config` | HSS S-mode + SC QSPI + L2-LIM | -| `hal/mpfs250-hss.ld` | Linker script for S-mode with L2-LIM | -| `hal/mpfs-l2lim.yaml` | HSS payload generator YAML for L2-LIM load target | - -**Build:** -```sh -cp config/examples/polarfire_mpfs250_hss_l2lim.config .config -make clean && make wolfboot.bin -dtc -I dts -O dtb hal/mpfs.dts -o hal/mpfs.dtb -hss-payload-generator -vvv -c ./hal/mpfs-l2lim.yaml wolfboot.bin -``` - -Flash the HSS payload to the eMMC/SD BIOS partition using HSS `USBDMSC`: -```sh -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 -``` - -**Build and sign the test application:** -```sh -make test-app/image_v1_signed.bin -``` - -**Flash the signed application to QSPI** using the UART programmer (enabled by default via -`UART_QSPI_PROGRAM=1`; requires `pyserial` installed): -```sh -python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB1 \ - test-app/image_v1_signed.bin 0x20000 -``` - -**Notes:** -- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash contents are stable. -- `ELF=0` is required: the test-app linker script (`test-app/RISCV64-mpfs250.ld`) places `.init` - (containing `_reset()`) first so the raw binary entry point is at offset 0. The full ELF with - debug symbols exceeds L2-LIM capacity. -- wolfBoot is placed at `0x08040000` (above the HSS L2-LIM resident region) and the application - is loaded at `0x08060000`. The stack resides at the top of the 1.5 MB L2-LIM region. -- HSS must be built and programmed to eNVM separately (see [PolarFire Building Hart Software Services](#polarfire-building-hart-software-services-hss)). -- **LIM instruction fetch caveat:** Ensure `L2_WAY_ENABLE` leaves enough cache ways unallocated - to back the LIM SRAM region. See the M-mode section for a detailed explanation. -- UART output appears on MMUART1 (`/dev/ttyUSB1`), same as other S-mode configurations. - -### PolarFire SoC M-Mode (bare-metal eNVM boot) - -wolfBoot supports running directly in Machine Mode (M-mode) on PolarFire SoC, replacing the Hart -Software Services (HSS) as the first-stage bootloader. wolfBoot runs on the E51 monitor core from -eNVM and loads a signed application from SC QSPI flash into L2 Scratchpad (on-chip RAM) -- no HSS -or DDR required. This is the minimal on-chip-only configuration. - -**Features:** -* Runs on E51 monitor core (hart 0) directly from eNVM -* Executes from L2 Scratchpad SRAM (256 KB at `0x0A000000`) -* Loads signed application from SC QSPI flash to L2 Scratchpad (`0x0A010200`) -* No HSS or DDR required — boots entirely from on-chip memory -* Parks and releases secondary U54 harts via CLINT IPI -* ECC384 + SHA384 signature verification - -**Relevant files:** - -| File | Description | -|------|-------------| -| `config/examples/polarfire_mpfs250_m_qspi.config` | M-mode + SC QSPI configuration | -| `hal/mpfs250-m.ld` | M-mode linker script (eNVM + L2 SRAM) | -| `hal/mpfs250.c` | HAL with QSPI driver, UART, L2 cache init | -| `src/boot_riscv_start.S` | M-mode assembly startup | - -**Boot flow:** -1. **eNVM reset vector** (`0x20220100`): CPU starts, startup code copies wolfBoot to L2 Scratchpad -2. **L2 Scratchpad execution** (`0x0A000000`): wolfBoot runs from scratchpad -3. **Hardware init**: L2 cache configuration, UART setup -4. **QSPI init**: SC QSPI controller (`0x37020100`), JEDEC ID read, 4-byte address mode -5. **Image load**: Read signed image from QSPI flash (`0x20000`) to L2 Scratchpad (`0x0A010200`) -6. **Verify & boot**: SHA384 integrity check, ECC384 signature verification, jump to app - -**Build:** -```sh -cp config/examples/polarfire_mpfs250_m_qspi.config .config -make clean && make wolfboot.elf -``` - -**Flash wolfBoot to eNVM** (requires SoftConsole / Libero SoC install): -```sh -export SC_INSTALL_DIR=/opt/Microchip/SoftConsole-v2022.2-RISC-V-747 - -$SC_INSTALL_DIR/eclipse/jre/bin/java -jar \ - $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ - --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD wolfboot.elf -``` - -**Build and sign the test application:** -```sh -make test-app/image_v1_signed.bin -``` - -**Flash the signed application to QSPI** using the UART programmer (requires `EXT_FLASH=1` and -`UART_QSPI_PROGRAM=1` in `.config`, and `pyserial` installed): -```sh -python3 tools/scripts/mpfs_qspi_prog.py /dev/ttyUSB0 \ - test-app/image_v1_signed.bin 0x20000 -``` - -The script: -1. Waits for wolfBoot to print the `QSPI-PROG: Press 'P'` prompt (power-cycle the board) -2. Sends `P` to enter programming mode -3. Transfers the binary in 256-byte ACK-driven chunks -4. wolfBoot erases, writes, and then continues booting the new image - -Use `0x20000` for the boot partition and `0x02000000` for the update partition. - -**QSPI partition layout** (Micron MT25QL01G, 128 MB): - -| Region | Address | Size | -|--------|---------|------| -| Boot partition | `0x00020000` | ~32 MB | -| Update partition | `0x02000000` | ~32 MB | -| Swap partition | `0x04000000` | 64 KB | - -**UART mapping:** - -| Hart | Core | MMUART | USB device | -|------|------|--------|------------| -| 0 | E51 | MMUART0 | /dev/ttyUSB0 | -| 1 | U54_1 | MMUART1 | /dev/ttyUSB1 | -| 2 | U54_2 | MMUART2 | N/A | -| 3 | U54_3 | MMUART3 | N/A | -| 4 | U54_4 | MMUART4 | N/A | - -**Expected serial output on successful boot:** -``` -wolfBoot Version: X.Y.Z (...) -Running on E51 (hart 0) in M-mode -QSPI: Using SC QSPI Controller (0x37020100) -QSPI: Flash ID = 0x20 0xBA 0x21 -QSPI-PROG: Press 'P' within 3s to program flash -QSPI-PROG: No trigger (got 0x00 ...), booting -Versions: Boot 1, Update 0 -... -Firmware Valid -Booting at 0x... -``` - -**Notes:** -- The E51 is `rv64imac` (no FPU or crypto extensions). wolfBoot is compiled with `NO_ASM=1` to - use portable C crypto implementations and `-march=rv64imac -mabi=lp64` for correct code - generation. The `rdtime` CSR instruction is not available in bare-metal M-mode; wolfBoot uses a - calibrated busy-loop for all delays (`udelay()` in `hal/mpfs250.c`). -- `UART_QSPI_PROGRAM=1` adds a 3-second boot pause every time. Set to `0` once the flash - contents are stable. -- The config uses `WOLFBOOT_LOAD_ADDRESS=0x0A010200` to place the application in L2 Scratchpad - above wolfBoot code (~64 KB at `0x0A000000`), with the stack at the top of the 256 KB region. -- **LIM instruction fetch limitation:** The on-chip LIM (`0x08000000`, 2 MB) is backed by L2 - cache ways. When `L2_WAY_ENABLE` is set to `0x0B` (all cache ways 0–7 active for caching), - no ways remain for LIM backing SRAM. Data reads from LIM work through the L2 cache, but - instruction fetch silently hangs — the CPU stalls with no trap generated. For this reason the - application is loaded into L2 Scratchpad (`0x0A000000`), which is always accessible regardless - of `L2_WAY_ENABLE`. To use LIM, reduce `L2_WAY_ENABLE` to free cache ways for LIM backing. -- **Strip debug symbols** before signing the test-app ELF. The debug build is ~150 KB but the - stripped ELF is ~5 KB. L2 Scratchpad has ~150 KB available between wolfBoot code and the stack: - `riscv64-unknown-elf-strip --strip-debug test-app/image.elf` -- **DDR support:** software LPDDR4 initialization is included via the **M-Mode + DDR** - configuration (`polarfire_mpfs250_m.config`, requires `LIBERO_FPGA_CONFIG_DIR`) for use cases - that require loading larger images (e.g. a Linux FIT) to DDR memory. See the next section. - -### PolarFire SoC M-Mode + DDR: booting Linux (minimal SBI) - -The **M-Mode + DDR** configuration (`config/examples/polarfire_mpfs250_m.config`) replaces both -HSS and OpenSBI: wolfBoot performs the LPDDR4 init/training on the E51, loads and verifies a -signed Yocto FIT image (kernel + dtb) from SD card into DDR, applies device-tree fixups, releases -U54 hart 1 into S-mode at the kernel entry, and then remains resident as a minimal M-mode SBI -runtime. Validated on the MPFS250T Video Kit: 4-CPU SMP Yocto Linux to login in ~40 s from -power-on. - -**Minimal SBI runtime** (`src/riscv_sbi.c`, a clean-room implementation of the OpenSBI/SBI spec; generic RISC-V with HAL hooks; enabled by -`WOLFBOOT_MMODE_SMODE_BOOT`): -* SBI v0.2 extensions: BASE, TIME (per-hart `mtimecmp`, MTIP-to-STIP injection), IPI (SSIP - injection via CLINT MSIP), RFENCE (remote `fence.i` / `sfence.vma` with completion wait), - HSM (`hart_start`/`hart_stop`/`hart_status` backed by per-hart start mailboxes), DBCN and the - legacy console putchar (shared with the wolfBoot UART), SRST. -* `rdtime` emulation: the U54/E51 have no `time` CSR, so `rdtime` is emulated from CLINT MTIME. - The M-mode HAL starts the MTIME time base before hand-off via SYSREG `RTC_CLOCK_CR` at 1 MHz - (`mpfs_enable_mtime()`), matching the device-tree `timebase-frequency`. -* Misaligned load/store emulation (not delegatable on these harts), including compressed forms, - for the kernel's unaligned copy tails. -* Per-hart M-mode trap stacks live in the `hss-buffer` reserved (nomap) DDR region; cross-hart - state (HSM mailboxes, IPI flags, the hart-release gate flag) lives in the E51 DTIM at - `0x01000000`, which is uncached and coherent for all harts. Cacheable L2-scratchpad memory - must not be used for cross-hart signalling (stores can be lost on dirty-line eviction). - -**Device-tree fixups** applied to the loaded dtb (`hal/mpfs250.c`): bootargs/root device -(the DTB's own `/chosen/bootargs` wins unless `LINUX_BOOTARGS`/`LINUX_BOOTARGS_ROOT` is set; -see "Linux kernel command line (bootargs)" in `docs/compile.md`), -MAC addresses from the device serial number, and all five MSS watchdog nodes are disabled. - -**Watchdog policy:** the MSS watchdogs always count and reset the chip on timeout (they cannot -be disabled in hardware, and `CONTROL=0` does not prevent the reset). After hand-off the parked -E51 acts as a monitor and refreshes all five watchdogs; the OS watchdog driver is disabled via -the dtb fixup so the two never conflict. `MPFS_WDT_NODES_KEEP=1` hands the watchdogs to the OS -instead: the dtb nodes stay enabled and the parked E51 stops refreshing, so the OS driver arms -them at probe and something on the OS side (a watchdog daemon, or the design's own fabric -scheme) has to service them from then on. A design with its own fabric watchdog needs neither -setting; wolfBoot never touches fabric logic. - -**Hand-off / SMP flow:** secondary harts park in eNVM until the E51 signals image-copy -completion (DTIM gate flag), then park in a WFI loop. The boot hart is released with a staged -mailbox {entry, dtb} plus MSIP; Linux brings up the remaining harts through SBI HSM -`hart_start`, which uses the same mailbox + MSIP path. The release path must stay fast -(no UART access): the kernel allows roughly one second for a started hart to come online. - -**Driver structure:** the licensed Cadence DDR controller logic (controller CSR programming, the -Memory Test Controller engine, and the LPDDR4 mode-register protocol) lives in the -target-independent `src/ddr_cadence.c` / `include/ddr_cadence.h` (controller base overridable via -`DDR_CADENCE_CTRL_BASE`). The Microchip-specific PHY, PLL, clock mux and training, plus the -board's `LIBERO_SETTING_*` values, stay in `hal/mpfs250_ddr.c`, which builds the controller -register table and composes the generic calls. Both compile only when `MPFS_DDR_INIT` is set. - -### PolarFire SoC hardware root of trust (PUF KEK, sNVM keystore, wrapped encryption key) - -The System Controller SRAM-PUF, secure NVM (sNVM), and TeraFire crypto can anchor key material in hardware: serve the verification public keys from sNVM, derive a device-unique KEK from the PUF, and store the AES image-encryption key in sNVM wrapped by that KEK. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). - -### PolarFire SoC boot mode 2: User Boot Loader stub - -Boot mode 2 ("user secure boot from sNVM") lets the System Controller authenticate the first code the MSS runs: it copies a User Boot Loader (UBL) out of authenticated sNVM pages into the LIM and releases all five harts at its entry. wolfBoot provides that UBL as a small verifier target, `TARGET=mpfs250_ubl`: it checks an ECC384/SHA384 wolfBoot manifest over the eNVM wolfBoot image against the public key compiled into the stub, then enters the eNVM wolfBoot at its reset vector on every hart exactly as a boot mode 1 boot would. The eNVM wolfBoot itself is unchanged and stays field-updatable; what the sNVM pages pin is the key that must have signed it. - -**eNVM layout** (`hal/mpfs250-m.ld`, `hal/mpfs250.h`) - -| Address | Content | -|---------|---------| -| `0x20220000` | boot ROM meta information (written by `mpfsBootmodeProgrammer`) | -| `0x20220100` | wolfBoot image, reset vector first | -| `0x2023FC00` | wolfBoot manifest header (`MPFS_ENVM_MANIFEST=1`), signed by the stub's key | - -The manifest is detached from the image it signs (the `WOLFBOOT_SELF_HEADER` form), so the image does not move and a boot mode 1 device boots the same eNVM contents. The last two eNVM pages stay free for the programmer. - -**Relevant files:** - -| File | Description | -|------|-------------| -| `config/examples/polarfire_mpfs250_ubl.config` | UBL stub configuration | -| `hal/mpfs250_ubl.c`, `hal/mpfs250_ubl.h` | stub HAL: watchdog hold, MMUART0 at the reset clock, manifest verify, hart hand-off | -| `hal/mpfs250_ubl.ld` | LIM linker script with the sNVM size limit | -| `src/boot_riscv_start.S` | `TARGET_mpfs250_ubl` startup: no copy, U54s park on a DTIM mailbox | -| `tools/scripts/mpfs_bootmode2.sh` | sNVM + boot mode programming on the secure dies, eNVM-only update | - -**Build the stub** (its own signing key; keep it out of the tree that signs application images): -```sh -cp config/examples/polarfire_mpfs250_ubl.config .config -make clean && make -# wolfboot.elf is the UBL; wolfboot_signing_private_key.der is the UBL key -``` -The image must fit the 221 authenticated sNVM pages (52,096 bytes after the 60-byte UBL header); the linker script enforces it. The sNVM pages above the stub stay available for the sNVM keystore and wrapped keys of [polarfire_snvm_puf.md](polarfire_snvm_puf.md). - -**Build and sign the eNVM wolfBoot** for the stub (any M-mode configuration): -```sh -cp config/examples/polarfire_mpfs250_m.config .config -make clean && make LIBERO_FPGA_CONFIG_DIR=... MPFS_ENVM_MANIFEST=1 \ - MPFS_UBL_KEY=/path/to/ubl/wolfboot_signing_private_key.der \ - wolfboot_envm_signed.bin -``` -`MPFS_ENVM_MANIFEST=1` links a manifest placeholder into the last used eNVM page; the rule signs the image without it (`wolfboot_envm.bin`, the ELF's loadable bytes with alignment gaps zero-filled, which is what the programmer writes), writes the resulting header into the placeholder and produces `wolfboot_envm_signed.elf`, which programs image and manifest together. `MPFS_UBL_KEY` defaults to the tree's own key, `MPFS_ENVM_VERSION` (default 1) is the manifest version, and `-DMPFS_UBL_MIN_VERSION=N` in the stub build refuses older manifests. - -**Program** (requires `SC_INSTALL_DIR` and `FPGENPROG`, see the M-mode section): -```sh -# 1. eNVM: image + manifest. This also selects boot mode 1, so the device -# boots the new eNVM contents before the stub is in place. -java -jar $SC_INSTALL_DIR/extras/mpfs/mpfsBootmodeProgrammer.jar \ - --bootmode 1 --die MPFS250T --package FCG1152 --workdir $PWD \ - wolfboot_envm_signed.elf - -# 2. sNVM: the stub, and boot mode 2. Non-secure dies: the programmer -# itself (--bootmode 2 --snvm_page 0 --usk 0 wolfboot.elf). Secure dies -# (MPFS250TS...), which the programmer does not list but which an -# authenticated sNVM client requires: -tools/scripts/mpfs_bootmode2.sh --die MPFS250TS --workdir $PWD /path/to/ubl/wolfboot.elf -``` -The script runs the programmer in `--dryrun` for the UBL image and the User sNVM Key (USK), replays its `fpgenprog` steps for the requested die and programs. Keep the USK file it names: the sNVM pages can only be rewritten with it. - -The UBL lives in an authenticated sNVM client, which the System Controller keys from the device's sNVM Master Key (SMK) -and the client's 12-byte USK. A device whose SMK has never been initialized cannot hold such a client, and -`mpfsBootmodeProgrammer` never asks for one, so `--regenerate-smk` passes `--regenerate_smk` to the `snvm_client` step -and the generated bitstream carries a new SMK. A new SMK invalidates every authenticated or encrypted sNVM client -already on the device; wolfBoot's own keystore and wrapped image key are plaintext clients and are not SMK-keyed. - -Updating the eNVM wolfBoot under boot mode 2 goes through the same script with `--envm`, which programs the eNVM client only and leaves the boot mode and sNVM alone: -```sh -tools/scripts/mpfs_bootmode2.sh --envm --die MPFS250TS --workdir $PWD wolfboot_envm_signed.elf -``` -`mpfsBootmodeProgrammer --bootmode 1` restores boot mode 1. - -**Expected serial output:** -``` -wolfBoot UBL 2.10.0: eNVM image 0x20220100, manifest 0x2023FC00 -UBL: eNVM wolfBoot signature OK (1051 ms), entering 0x20220100 -UBL: 5 of 5 harts released -wolfBoot Version: ... -``` -A manifest that does not verify prints which check failed (`no manifest`, `digest mismatch`, `signature invalid`) and `UBL: eNVM wolfBoot verify FAILED`; the stub panics and nothing in eNVM runs. - -**Notes:** -- The stub runs at the reset clock and changes no clock, L2 way or peripheral the eNVM wolfBoot expects to find in its reset state. It holds the five MSS watchdogs in the no-reset, maximum-window state for the verify and puts the E51's back before the jump, so the eNVM wolfBoot's own watchdog snapshot is what the boot ROM left. -- The U54s wait in the stub on a DTIM mailbox (`hal/mpfs250_ubl.h`) for the verified entry, acknowledge and jump; the E51 follows last. All five harts then run the eNVM startup as in boot mode 1. -- Immutability of the stub is a Libero decision: the ROM flag on its sNVM pages and a frozen boot configuration. wolfBoot does not program either. - -### PolarFire testing - -This section describes how to build the test-application, create a custom uSD with required partitions and copying signed test-application to uSD partitions. - -1) Partition uSD card (replace /dev/sdc with your actual media, find using `lsblk`): - -Note: adjust +64M for larger OFP A/B - -```sh -sudo fdisk /dev/sdc <; - entry = <0x80200000>; - hash-1 { algo = "sha256"; }; - ... - }; -}; -``` - -Then build the FIT directly - no manual `gzip -cdvk` step: - -```sh -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 -mkimage -f hal/mpfs250.its fitImage -``` - -At boot, wolfBoot decompresses the kernel into `0x80200000` directly out of -the FIT `data` blob. The outer wolfBoot signature covers the whole FIT -(including the compressed `data`), and gzip's CRC32 + ISIZE trailer covers -the decompressed output; the per-image `hash-1` subnodes are not re-checked -at runtime as they would duplicate the outer signature. - -##### Option B - Uncompressed FIT (`GZIP=0`) - -Build wolfBoot with `GZIP=0` and pre-decompress the kernel on the host. -Keep `compression = "none"` in `hal/mpfs250.its`: - -```sh -# Copy wolfBoot to "BIOS" partition -sudo dd if=wolfboot.bin of=/dev/sdc1 bs=512 && sudo cmp wolfboot.bin /dev/sdc1 - -# Extract GZIP compressed linux kernel to wolfboot root -gzip -cdvk ../yocto-dev-polarfire/build/tmp-glibc/work/mpfs_video_kit-oe-linux/linux-mchp/6.12.22+git/build/linux.bin > kernel.bin - -# Create custom FIT image -mkimage -f hal/mpfs250.its fitImage -FIT description: PolarFire SoC MPFS250T -Created: Tue Dec 23 11:29:02 2025 - Image 0 (kernel-1) - Description: Linux Kernel - Created: Tue Dec 23 11:29:02 2025 - Type: Kernel Image - Compression: uncompressed - Data Size: 19745280 Bytes = 19282.50 KiB = 18.83 MiB - Architecture: RISC-V - OS: Linux - Load Address: 0x80200000 - Entry Point: 0x80200000 - Hash algo: sha256 - Hash value: 800ce147fa91f367ec620936a59a1035c49971ed4b9080c96bdc547471e80487 - Image 1 (fdt-1) - Description: Flattened Device Tree blob - Created: Tue Dec 23 11:29:02 2025 - Type: Flat Device Tree - Compression: uncompressed - Data Size: 19897 Bytes = 19.43 KiB = 0.02 MiB - Architecture: RISC-V - Load Address: 0x8a000000 - Hash algo: sha256 - Hash value: 0b4efca8c0607c9a8f4f9a00ccb7691936e019f3181aab45e6d52dae91975039 - Default Configuration: 'conf1' - Configuration 0 (conf1) - Description: Linux kernel and FDT blob - Kernel: kernel-1 - FDT: fdt-1 - Hash algo: sha256 - Hash value: unavailable - -# Sign FIT image with version 1 -./tools/keytools/sign --ecc384 --sha384 fitImage wolfboot_signing_private_key.der 1 - -# Copy signed FIT image to both OFP A/B partitions -sudo dd if=fitImage_v1_signed.bin of=/dev/sdc2 bs=512 status=progress && sudo cmp fitImage_v1_signed.bin /dev/sdc2 -sudo dd if=fitImage_v1_signed.bin of=/dev/sdc3 bs=512 status=progress && sudo cmp fitImage_v1_signed.bin /dev/sdc3 - -# Copy root file system -sudo dd if=../yocto-dev-polarfire/build/tmp-glibc/deploy/images/mpfs-video-kit/mchp-base-image-sdk-mpfs-video-kit.rootfs.ext4 of=/dev/sdc4 bs=4M status=progress -``` - -### PolarFire SoC Encryption - -PolarFire SoC uses MMU mode with disk-based updates. The encryption key is stored in RAM rather than flash. - -Enable encryption in your configuration with `ENCRYPT=1` and one of: `ENCRYPT_WITH_AES256=1`, `ENCRYPT_WITH_AES128=1`, or `ENCRYPT_WITH_CHACHA=1`. - -| Algorithm | Key Size | Nonce/IV Size | -|-----------|----------|---------------| -| ChaCha20 | 32 bytes | 12 bytes | -| AES-128 | 16 bytes | 16 bytes | -| AES-256 | 32 bytes | 16 bytes | - -The `libwolfboot` API provides the following functions for managing the encryption key: - -```c -int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce); -int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce); -int wolfBoot_erase_encrypt_key(void); /* called automatically by wolfBoot_success() */ -``` - -To use your own implementation for getting the encryption key use `CUSTOM_ENCRYPT_KEY` and `OBJS_EXTRA=src/my_custom_encrypt_key.o`. -Then provide your own implementation of `int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce);` - -Example: - -```c -int RAMFUNCTION wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) -{ - int i; - /* Test key: "0123456789abcdef0123456789abcdef" (32 bytes for AES-256) */ - const char test_key[] = "0123456789abcdef0123456789abcdef"; - /* Test nonce: "0123456789abcdef" (16 bytes) */ - const char test_nonce[] = "0123456789abcdef"; - - for (i = 0; i < ENCRYPT_KEY_SIZE && i < (int)sizeof(test_key); i++) { - key[i] = (uint8_t)test_key[i]; - } - for (i = 0; i < ENCRYPT_NONCE_SIZE && i < (int)sizeof(test_nonce); i++) { - nonce[i] = (uint8_t)test_nonce[i]; - } - return 0; -} -``` - -To sign and encrypt an image, create a key file with the concatenated key and nonce, then use the sign tool: - -```sh -# Create key file (32-byte key + 16-byte nonce for AES-256) -printf "0123456789abcdef0123456789abcdef0123456789abcdef" > /tmp/enc_key.der - -# Sign and encrypt -./tools/keytools/sign --ecc384 --sha384 --aes256 --encrypt /tmp/enc_key.der \ - fitImage wolfboot_signing_private_key.der 1 -``` - -The result is `fitImage_v1_signed_and_encrypted.bin`, which gets placed into your OFP_A or OFP_B partitions. - -```sh -sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc2 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc2 -sudo dd if=fitImage_v1_signed_and_encrypted.bin of=/dev/sdc3 bs=512 status=progress && sudo cmp fitImage_v1_signed_and_encrypted.bin /dev/sdc3 -``` - -During boot, wolfBoot decrypts the image headers from disk to select the best candidate, loads and decrypts the full image to RAM, then verifies integrity and authenticity before booting. On successful boot, `wolfBoot_success()` clears the key from RAM. - -See the [Encrypted Partitions](encrypted_partitions.md) documentation for additional details. - -### PolarFire SoC with PQC (ML-DSA) - -#### Configuration - -Update your `.config` file with the following ML-DSA settings, or pass them as `make` arguments on top of `config/examples/polarfire_mpfs250_m.config` (the standalone M-mode E51 target verifies ML-DSA-87 this way, with SHA-384 as the image hash): - -```makefile -# ML-DSA 87 (Category 5) -SIGN=ML_DSA -HASH=SHA256 -ML_DSA_LEVEL=5 -IMAGE_SIGNATURE_SIZE=4627 -IMAGE_HEADER_SIZE=12288 -WOLFBOOT_SECTOR_SIZE?=0x4000 -``` - -**Important:** -- The `sign` tool requires `IMAGE_HEADER_SIZE` to be set as an environment variable, even if it's already configured in `.config`. This is because the sign tool reads the environment variable separately to determine the header size for padding. Without this, the sign tool may use a smaller default header size, causing a mismatch with wolfBoot's expected header size. -- The `WOLFBOOT_SECTOR_SIZE` must be larger than the `IMAGE_HEADER_SIZE`/ - -#### Signing and Encryption - -```sh -# Sign and Encrypt with PQ ML-DSA 5 (87) -# NOTE: IMAGE_HEADER_SIZE must match the value in .config -IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 --aes256 --encrypt /tmp/enc_key.der \ - fitImage wolfboot_signing_private_key.der 1 -``` - -**ML-DSA Parameter Reference:** - -| ML_DSA_LEVEL | Security Category | Signature Size | Private Key | Public Key | Recommended IMAGE_HEADER_SIZE | -|--------------|-------------------|----------------|-------------|------------|------------------------------ | -| 2 | Category 2 | 2420 | 2560 | 1312 | 8192 | -| 3 | Category 3 | 3309 | 4032 | 1952 | 8192 | -| 5 | Category 5 | 4627 | 4896 | 2592 | 12288 | - -For other ML-DSA levels, adjust `ML_DSA_LEVEL`, `IMAGE_SIGNATURE_SIZE`, and `IMAGE_HEADER_SIZE` accordingly in both `.config` and the signing command. - -### PolarFire Performance Comparison - -#### Binary Size Comparison - -The following table compares wolfBoot binary sizes for different signature algorithms on PolarFire SoC (MPFS250): - -| Algorithm | Hash | Text | Data | BSS | Total | Binary Size | -|-----------|--------|---------|------|---------|---------|-------------| -| ECC384 | SHA384 | 67.1 KB | 8 B | 3.0 KB | 70.2 KB | 68 KB | -| ML-DSA 87 | SHA256 | 63.9 KB | 0 B | 14.5 KB | 78.4 KB | 64 KB | - -#### Boot Time Comparison - -Boot time measurements on PolarFire SoC (RISC-V 64-bit U54 @ 625 MHz) for a 19MB encrypted FIT image: - -| Algorithm | Hash | Load Time | Decrypt Time | Integrity Check | Signature Verify | Total Boot Time | -|-------------|---------|-----------|--------------|-----------------|------------------|-----------------| -| ECC384 | SHA384 | ~800 ms | ~2900 ms | ~1500 ms | ~70 ms | ~5.3 seconds | -| ML-DSA 87 | SHA256 | ~835 ms | ~2900 ms | ~2100 ms | ~22 ms | ~5.9 seconds | - -Standalone M-mode (`polarfire_mpfs250_m.config`, E51 @ 600 MHz, no HSS) measured from power-on on the Video Kit with host-side timestamps on the console, plaintext FIT, ECC384/SHA384, software crypto, the same 19.7 MB kernel either gzip-compressed in the FIT (5.9 MB) or stored uncompressed (19.8 MB): - -| Phase | gzip FIT | uncompressed FIT | -|-----------------------------------------------|----------|------------------| -| DDR training, SD init, GPT read, FIT load, SHA384 integrity, ECC384 verify | ~2 s | ~4 s | -| gzip inflate of the kernel | ~16 s | - | -| FIT copies, DTB fixups | <1 s | <1 s | -| M-mode -> S-mode handoff | ~18 s | ~5 s | -| Linux login prompt | ~41 s | ~28 s | - -The inflate runs on the E51 at about 1.2 MB/s of output, so a compressed kernel costs far more than the extra card read it saves; for a short boot, build the FIT with `compression = "none"` for the kernel (`FIT_KERNEL_COMP_ALG = "none"` in Yocto). The software SHA384 runs at about 11 MB/s on the E51 and the ECC384 verify is well under a second; neither is a lever. `BOOT_BENCHMARK` phase prints on this target are in real time now that the timer uses the live CPU clock. - -The LPDDR4 controller on this board needs the first CPU-side transaction after DDR initialization to be a read: when a write comes first on a cold boot, it lands in DDR displaced by several bus beats and every later cached-port write in that boot follows it. `mpfs_ddr_init()` therefore reads one block through the non-cached alias before anything is written, after which CPU, L2 write-back and SDMA writes land exactly and the SD load, the FIT copies, the DTB fixups and the image hash use DDR directly. Measured on a buffer in L2 scratch the Athena SHA384 is about 1.3x the software rate and AES-256-CTR about 4.4x. ML-DSA-87 on this target (`SIGN=ML_DSA` on `polarfire_mpfs250_m.config`) links to a smaller image than ECC384 because no big-number code is needed. - -### PolarFire Soc Debugging - -Start GDB server: - -```sh -$SC_INSTALL_DIR/openocd/bin/openocd --command "set DEVICE MPFS" --file board/microsemi-riscv.cfg -``` - -Start GDB Client: `riscv64-unknown-elf-gdb` - -``` -file wolfboot.elf -tar rem:3333 -add-symbol-file ../hart-software-services/build/hss-l2scratch.elf -set pagination off -foc c - -set $target_riscv=1 -set mem inaccessible-by-default off -set architecture riscv:rv64 -#load wolfboot.elf -#thread apply 2 set $pc=_reset -#thread apply all set $pc=_start -``` - -### PolarFire Example Boot Output - -``` -wolfBoot Version: 2.8.0 -Running on E51 (hart 0) in M-mode -Boot RESET_SR: 1FF (bit0=PERIPH bit1=MSS bit2=CPU bit3=DBG bit4=FABRIC bit5=WDOG bit6=GPIO bit7=BUS bit8=SOFT) -======================================== -DDR: Training+MTC PASS -DDR: Initialization COMPLETE -SDHCI: platform init -Reading MBR... -Found GPT PTE at sector 1 -Valid GPT partition table -Max number of partitions: 128 - GPT part 0: 0_2000000h @ 0_100000 - GPT part 1: 0_2000000h @ 0_2100000 - GPT part 2: 7_6A300000h @ 0_4100000 -Total partitions on disk0: 3 -Checking primary OS image in 0,0... -Checking secondary OS image in 0,1... -Versions, A:1 B:0 -Load block size: 512KB -Load address 0x82000000 -Attempting boot from P:A -Boot partition: 0xA03FD90 (sz 19766364, ver 0x1, type 0x601) -Loading image from disk... -done -Checking image integrity... -done -Verifying image signature... -done -Firmware Valid. -Flattened uImage Tree: Version 17, Size 19766364 -Loading Image kernel-1: 0x820000C8 -> 0x80200000 (19745280 bytes) -Loading Image fdt-1: 0x832D4BCC -> 0x8A000000 (19897 bytes) -Loading DTS: 0x8A000000 -> 0x8A000000 (19897 bytes) -Booting at 80200000 -FDT: Set chosen, bootargs=earlycon=sbi root=/dev/mmcblk0p3 rootwait uio_pdrv_genirq.of_id=generic-uio -FDT: Device serial: 219A437C-6AE1F1C2-8EDC4324-685B2288 -FDT: MAC0 = 00:04:A3:5B:22:88 -FDT: Set watchdog@20001000 status=disabled (... all five MSS WDT nodes) -M->S handoff: entry=0x80200000 hart=0 dtb=0x8A000000 -Releasing hart 1 into S-mode at 0x80200000 (dtb=0x8A000000) -[ 0.000000] Linux version 6.12.22-linux4microchip+fpga-2025.07 ... #1 SMP -[ 0.000000] Machine model: Microchip PolarFire-SoC VIDEO Kit -[ 0.000000] SBI specification v0.2 detected -[ 0.000000] SBI implementation ID=0x776f6c66 Version=0x1 -[ 0.000000] SBI TIME extension detected -[ 0.000000] SBI IPI extension detected -[ 0.000000] SBI RFENCE extension detected -[ 0.000000] riscv: providing IPIs using SBI IPI extension -[ 0.016264] smp: Bringing up secondary CPUs ... -[ 0.040738] smp: Brought up 1 node, 4 CPUs -[ 3.764474] Run /sbin/init as init process -... -mpfs-video-kit login: -``` - -The Linux SBI lines confirm the kernel is talking to wolfBoot's own SBI runtime: the implementation ID `0x776f6c66` is ASCII `"wolf"`, distinguishing it from OpenSBI (ID `0x8`). - -### PolarFire Benchmarks - -RISC-V 64-bit U54 (RV64GC1) 625 MHz +> **Filesystem boot:** this target can also read its signed image from a **file** on a read-only FAT32 or ext4 partition instead of from raw offset 0. Build with `DISK_FS=fat32|ext4|both` and set `BOOT_FILE_A` / `BOOT_FILE_B`. See [Disk boot from a read-only filesystem (FAT32 / ext4)](compile.md#disk-boot-from-a-read-only-filesystem-fat32--ext4). -``` -./configure --enable-riscv-asm --enable-mldsa --enable-mlkem --enable-sp=yes -make -./wolfcrypt/benchmark/benchmark ------------------------------------------------------------------------------- - wolfSSL version 5.8.4 ------------------------------------------------------------------------------- -Math: Multi-Precision: Wolf(SP) word-size=64 bits=3072 sp_int.c - Single Precision: ecc 256 rsa/dh 2048 3072 sp_c64.c - Assembly Speedups: RISCVASM ALIGN -wolfCrypt Benchmark (block bytes 1048576, min 1.0 sec each) -RNG 5 MiB took 1.225 seconds, 4.081 MiB/s -AES-128-CBC-enc 10 MiB took 1.179 seconds, 8.478 MiB/s -AES-128-CBC-dec 10 MiB took 1.164 seconds, 8.589 MiB/s -AES-192-CBC-enc 10 MiB took 1.373 seconds, 7.281 MiB/s -AES-192-CBC-dec 10 MiB took 1.360 seconds, 7.354 MiB/s -AES-256-CBC-enc 10 MiB took 1.565 seconds, 6.389 MiB/s -AES-256-CBC-dec 10 MiB took 1.550 seconds, 6.451 MiB/s -AES-128-GCM-enc 10 MiB took 1.940 seconds, 5.156 MiB/s -AES-128-GCM-dec 10 MiB took 1.938 seconds, 5.159 MiB/s -AES-192-GCM-enc 5 MiB took 1.068 seconds, 4.680 MiB/s -AES-192-GCM-dec 5 MiB took 1.066 seconds, 4.689 MiB/s -AES-256-GCM-enc 5 MiB took 1.163 seconds, 4.298 MiB/s -AES-256-GCM-dec 5 MiB took 1.163 seconds, 4.301 MiB/s -GMAC Table 4-bit 15 MiB took 1.106 seconds, 13.566 MiB/s -CHACHA 20 MiB took 1.107 seconds, 18.068 MiB/s -CHA-POLY 15 MiB took 1.058 seconds, 14.178 MiB/s -POLY1305 75 MiB took 1.036 seconds, 72.387 MiB/s -SHA 20 MiB took 1.141 seconds, 17.535 MiB/s -SHA-256 10 MiB took 1.071 seconds, 9.336 MiB/s -SHA-384 15 MiB took 1.066 seconds, 14.068 MiB/s -SHA-512 15 MiB took 1.066 seconds, 14.070 MiB/s -SHA-512/224 15 MiB took 1.067 seconds, 14.060 MiB/s -SHA-512/256 15 MiB took 1.070 seconds, 14.023 MiB/s -SHA3-224 15 MiB took 1.328 seconds, 11.292 MiB/s -SHA3-256 15 MiB took 1.398 seconds, 10.731 MiB/s -SHA3-384 10 MiB took 1.206 seconds, 8.291 MiB/s -SHA3-512 10 MiB took 1.729 seconds, 5.785 MiB/s -SHAKE128 15 MiB took 1.142 seconds, 13.135 MiB/s -SHAKE256 15 MiB took 1.402 seconds, 10.699 MiB/s -HMAC-SHA 20 MiB took 1.145 seconds, 17.470 MiB/s -HMAC-SHA256 10 MiB took 1.074 seconds, 9.310 MiB/s -HMAC-SHA384 15 MiB took 1.076 seconds, 13.944 MiB/s -HMAC-SHA512 15 MiB took 1.069 seconds, 14.036 MiB/s -PBKDF2 1 KiB took 1.023 seconds, 1.130 KiB/s -RSA 2048 public 1000 ops took 1.087 sec, avg 1.087 ms, 920.244 ops/sec -RSA 2048 private 100 ops took 5.410 sec, avg 54.100 ms, 18.484 ops/sec -DH 2048 key gen 48 ops took 1.004 sec, avg 20.920 ms, 47.801 ops/sec -DH 2048 agree 100 ops took 2.087 sec, avg 20.873 ms, 47.909 ops/sec -ECC [ SECP256R1] 256 key gen 800 ops took 1.100 sec, avg 1.375 ms, 727.248 ops/sec -ECDHE [ SECP256R1] 256 agree 300 ops took 1.041 sec, avg 3.470 ms, 288.152 ops/sec -ECDSA [ SECP256R1] 256 sign 600 ops took 1.144 sec, avg 1.907 ms, 524.370 ops/sec -ECDSA [ SECP256R1] 256 verify 300 ops took 1.173 sec, avg 3.909 ms, 255.844 ops/sec -ECC [ SECP384R1] 384 key gen 100 ops took 3.887 sec, avg 38.867 ms, 25.729 ops/sec -ECDHE [ SECP384R1] 384 agree 100 ops took 3.883 sec, avg 38.827 ms, 25.755 ops/sec -ECDSA [ SECP384R1] 384 sign 100 ops took 3.948 sec, avg 39.485 ms, 25.326 ops/sec -ECDSA [ SECP384R1] 384 verify 100 ops took 2.619 sec, avg 26.190 ms, 38.183 ops/sec -ML-KEM 512 128 key gen 2000 ops took 1.021 sec, avg 0.511 ms, 1958.111 ops/sec -ML-KEM 512 128 encap 1700 ops took 1.006 sec, avg 0.592 ms, 1690.275 ops/sec -ML-KEM 512 128 decap 1300 ops took 1.075 sec, avg 0.827 ms, 1209.214 ops/sec -ML-KEM 768 192 key gen 1200 ops took 1.035 sec, avg 0.863 ms, 1158.970 ops/sec -ML-KEM 768 192 encap 1100 ops took 1.092 sec, avg 0.993 ms, 1006.925 ops/sec -ML-KEM 768 192 decap 800 ops took 1.055 sec, avg 1.319 ms, 758.026 ops/sec -ML-KEM 1024 256 key gen 800 ops took 1.124 sec, avg 1.405 ms, 711.862 ops/sec -ML-KEM 1024 256 encap 700 ops took 1.090 sec, avg 1.557 ms, 642.343 ops/sec -ML-KEM 1024 256 decap 600 ops took 1.181 sec, avg 1.968 ms, 508.073 ops/sec -ML-DSA 44 key gen 600 ops took 1.107 sec, avg 1.844 ms, 542.217 ops/sec -ML-DSA 44 sign 200 ops took 1.144 sec, avg 5.719 ms, 174.842 ops/sec -ML-DSA 44 verify 600 ops took 1.146 sec, avg 1.910 ms, 523.569 ops/sec -ML-DSA 65 key gen 400 ops took 1.267 sec, avg 3.167 ms, 315.744 ops/sec -ML-DSA 65 sign 200 ops took 1.687 sec, avg 8.436 ms, 118.543 ops/sec -ML-DSA 65 verify 400 ops took 1.272 sec, avg 3.180 ms, 314.428 ops/sec -ML-DSA 87 key gen 200 ops took 1.066 sec, avg 5.331 ms, 187.588 ops/sec -ML-DSA 87 sign 100 ops took 1.162 sec, avg 11.617 ms, 86.084 ops/sec -ML-DSA 87 verify 200 ops took 1.077 sec, avg 5.385 ms, 185.704 ops/sec -Benchmark complete -``` ## STM32F7 diff --git a/docs/encrypted_partitions.md b/docs/encrypted_partitions.md index 3c36883f5d..59b6c85e9f 100644 --- a/docs/encrypted_partitions.md +++ b/docs/encrypted_partitions.md @@ -42,7 +42,7 @@ You can use the `CUSTOM_ENCRYPT_KEY` option to implement your own functions for: `wolfBoot_get_encrypt_key`, `wolfBoot_set_encrypt_key` and `wolfBoot_erase_encrypt_key`. -For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [polarfire_snvm_puf.md](polarfire_snvm_puf.md). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. +For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [PolarFire.md](PolarFire.md#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. To enable: diff --git a/docs/keystore.md b/docs/keystore.md index d010562060..858b3be8e4 100644 --- a/docs/keystore.md +++ b/docs/keystore.md @@ -224,7 +224,7 @@ To support this mode of operation, the `keygen` tool supports the `--nolocalkeys ### Using KeyStore in secure NVM (PolarFire SoC sNVM) -On Microchip PolarFire SoC the trust anchor can be served from the System Controller secure NVM (sNVM) instead of being compiled into the bootloader, using the `SNVM_KEYSTORE` backend. See [polarfire_snvm_puf.md](polarfire_snvm_puf.md). +On Microchip PolarFire SoC the trust anchor can be served from the System Controller secure NVM (sNVM) instead of being compiled into the bootloader, using the `SNVM_KEYSTORE` backend. See [PolarFire.md](PolarFire.md#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). ## Build System Integration diff --git a/docs/polarfire_sbi_vendor.md b/docs/polarfire_sbi_vendor.md deleted file mode 100644 index fa7c86629b..0000000000 --- a/docs/polarfire_sbi_vendor.md +++ /dev/null @@ -1,31 +0,0 @@ -# PolarFire SoC: wolfBoot SBI vendor extension - -The M-mode wolfBoot stays resident as the SBI for the OS. With `MPFS_SBI_VENDOR=1` it also serves the Microchip vendor extension, EID `0x09000029` (`SBI_EXT_VENDOR_START | mvendorid 0x029`), the same number the Hart Software Services register, so an OS probes one extension for either firmware and tells them apart with `GET_VERSION`. The HSS function IDs for inter-hart communication, remoteproc, crypto and IPC are answered with `SBI_ERR_NOT_SUPPORTED`; `HSS_REBOOT` (0x10) performs a cold MSS reset. - -The definitions live in `include/riscv_sbi.h`; the handler is `sbi_hal_vendor_ecall()` in `hal/mpfs250.c`. Arguments travel in `a0..a2`, the SBI error returns in `a0` and the value in `a1`. Buffers are addresses in the caller's address space: M-mode reads and writes them through the caller's translation, byte by byte, with a trap vector that turns a bad address into `SBI_ERR_INVALID_ADDRESS` instead of a stopped hart. A buffer is at most 4096 bytes and, while the caller runs with paging off, must lie in DDR. - -| FID | Call | Arguments | Result | -|-----|------|-----------|--------| -| 0x40 | `GET_VERSION` | - | `a1` = 1 | -| 0x41 | `GET_SERIAL` | `a0` buffer, `a1` length >= 16 | the 16-byte device serial number | -| 0x42 | `GET_KEY` | `a0` purpose, `a1` buffer, `a2` length == 32 | a PUF-derived key; each purpose is served once per boot, a second call returns `SBI_ERR_DENIED` | -| 0x43 | `DIGEST_CHECK` | `a0` options word | starts the System Controller digest check | -| 0x44 | `IAP_AUTHENTICATE` | `a0` SPI directory index | starts IAP image authentication | -| 0x45 | `IAP_VERIFY` | `a0` SPI directory index | starts IAP verify | -| 0x46 | `IAP_PROGRAM` | `a0` SPI directory index | the device restarts on success; a return is the failure status in `a1` | -| 0x47 | `AUTO_UPDATE` | - | the device restarts on success; a return is the failure status in `a1` | -| 0x48 | `SC_FLASH_ERASE` | `a0` flash offset, `a1` length == one erase block (64 KB), both block aligned | erases that block of the System Controller's SPI flash; `MPFS_SBI_SC_FLASH=1` builds only | -| 0x49 | `SC_FLASH_WRITE` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | programs the bytes; `MPFS_SBI_SC_FLASH=1` builds only | -| 0x4A | `SC_FLASH_READ` | `a0` flash offset, `a1` buffer, `a2` length <= 4096 | reads the bytes (any offset, the directory included); `MPFS_SBI_SC_FLASH=1` builds only | -| 0x4B | `SC_FLASH_SET_IMAGE` | `a0` directory index 1 (upgrade) or 2 (blank), `a1` image offset | rewrites the SPI directory block with that entry changed (entry 1 also zeroes entry 2); `MPFS_SBI_SC_FLASH=1` builds only | -| 0x4F | `SC_STATUS` | - | `SBI_ERR_ALREADY_STARTED` while the started service runs, else `a1` = 16-bit status with mailbox word 1 (the digest-check `DIGESTERR` bits) in the upper half | - -`GET_KEY` purposes name a fixed label; the caller never supplies a challenge, so the OS cannot ask the PUF for wolfBoot's own key-encryption key. Purpose 1 is the MRAM log key (`SHA-384("wolfBoot-mram-log-v1" || PUF(SHA-384("wolfBoot-mram-log-v1")[0..15]))[0..31]`); purpose 2 reproduces the earlier user-space `varlog` derivation. The key is device-bound: the same whether the golden or an operational image booted. - -The System Controller services (0x43-0x4F) are served only in a build with `MPFS_SCB_SMODE_DENY=1`, where the OS cannot reach the mailbox itself; in an unfenced build they return `SBI_ERR_DENIED` and the OS uses its own mailbox drivers, which the fence would otherwise race. The long-running ones are started inside the ecall and polled through `SC_STATUS`, because an ecall holds its hart in M-mode with interrupts off. The mailbox is one resource for all harts: a call that finds it taken returns `SBI_ERR_FAILED`. - -The fence also covers the System Controller's QSPI flash controller, so a fenced OS cannot stage a bitstream the way the mainline `mpfs-auto-update` driver does (through `mtd` on that controller). `MPFS_SBI_SC_FLASH=1` adds the flash calls (0x48-0x4B) for that build: the OS erases and writes the image region directly, reads back what it wants verified, and points the SPI directory's upgrade entry at the image with `SC_FLASH_SET_IMAGE` before `IAP_AUTHENTICATE` / `IAP_PROGRAM` or `AUTO_UPDATE`. The first erase block (the directory and the start of the design-info area) is never erased or written through `SC_FLASH_ERASE` / `SC_FLASH_WRITE`; `SET_IMAGE` reads it, changes the one entry (the golden entry 0 stays what it was) and writes it back, the same read-erase-write the OS driver performs. Each erase call takes one block and each transfer at most 4 KB, so no ecall holds its hart for more than the erase time of one block; the OS side loops. The flash layout the mainline driver uses (directory in the first 1 KB, design info at 1 KB, bitstream slots from 1 MB + 1 KB) is the OS's business; wolfBoot only protects the directory block. - -The Linux side is the `wolfboot-sbi` module in `tools/linux/wolfboot-sbi/`: `/dev/wolfboot` (root only) with one ioctl per call, see `wolfboot_sbi.h`. It probes the extension and `GET_VERSION`, so it unloads cleanly under the HSS, and it never touches the mailbox registers, so the mainline `mpfs-sys-controller`, `mpfs-auto-update` and `mpfs-rng` drivers keep working in an unfenced build. A caller of `IAP_PROGRAM` or `AUTO_UPDATE` syncs its filesystems first: the device restarts inside the call. - -Build: `MPFS_SBI_VENDOR=1` on the M-mode target (`RISCV_MMODE=1`); the key derivation is SHA-384 whatever the image hash. diff --git a/docs/polarfire_snvm_puf.md b/docs/polarfire_snvm_puf.md deleted file mode 100644 index 7e99f52ea4..0000000000 --- a/docs/polarfire_snvm_puf.md +++ /dev/null @@ -1,153 +0,0 @@ -# PolarFire SoC: PUF Root of Trust, sNVM Keystore, and PUF-Wrapped Encryption Key (M-Mode) - -This document describes the PolarFire SoC MPFS250 hardware-root-of-trust features added for the wolfBoot E51 M-mode boot flow (the same flow that brings up LPDDR4 and boots a signed Yocto FIT from SD; see `Targets.md`). They let wolfBoot anchor key material in the chip's System Controller instead of compiling it into the bootloader image: - -1. A System Controller mailbox driver for secure NVM (sNVM), the SRAM-PUF, and the nonce/TRNG service. -2. An sNVM keystore backend that serves the verification public keys from sNVM at boot. -3. A device-unique Key Encryption Key (KEK) derived from the SRAM-PUF, used to wrap/unwrap individual keys. -4. A PUF-wrapped image-encryption key: the AES image key is stored in sNVM wrapped by the PUF KEK and unwrapped at boot, with the bulk image decrypted on the M-mode disk-boot path. - -All features are off by default and gated behind build flags, so a normal build is unaffected. - -## Trust model - -The intended chain, from the immutable hardware up: - -``` -PolarFire boot ROM (secure boot of wolfBoot: not configured on the Video Kit, bootmode 1) - -> wolfBoot (M-mode, eNVM) - -> System Controller SRAM-PUF (fixed challenge) -> SHA-384 -> 256-bit KEK - -> sNVM keystore: ECC384 verification public key(s) - -> sNVM: AES-256 image key, wrapped by the PUF KEK - -> verify + decrypt the signed/encrypted Yocto FIT - -> 4-CPU SMP Linux -``` - -The KEK is re-derived on every boot and never leaves the device. The public keys are not secret; storing them in sNVM (rather than the image) lets the trust anchor be provisioned and rotated per device, and binds verification to the System Controller. - -### Production requirement: the keystore pages must not stay writable - -The sNVM write service is reachable from anything that can drive the System Controller mailbox, including the Linux `mpfs-sys-controller` driver, so on the development flow above the trust anchor is only as protected as the OS. A production unit closes this in one of two ways, both opt-in at build time: - -- Deliver the keystore pages as ROM in the bitstream (Libero sNVM client with the keystore image as content and the ROM attribute set) and build with `SNVM_KEYSTORE=1 SNVM_KEYSTORE_REQUIRE_ROM=1`: wolfBoot reads the page admin word of every keystore module and refuses to boot unless its ROM flag is set. The ROM flag is a bitstream property; the sNVM system services can neither set nor clear it, so there is no on-device step that can lock a page by accident. The on-device provisioning writer is for development only and cannot write a ROM page. -- Build with `MPFS_SCB_SMODE_DENY=1`: every hart's PMP fences S-mode off the System Controller control and mailbox window (`0x37020000`-`0x37020FFF`) before the hand-off, and the DTB fixup disables the `mailbox@37020800`, `spi@37020100` (System Controller QSPI) and `syscontroller` nodes so the OS does not probe hardware it cannot reach. A supervisor access into the window is otherwise redirected to the kernel's trap vector as an access fault (this core cannot delegate access faults, so wolfBoot's SBI forwards them). The sNVM pages stay writable by wolfBoot (M-mode) only. This removes the OS-side System Controller services (device serial number, bitstream services, sNVM access, the zeroize procedure) and anything the OS image builds on them: on the Video Kit Yocto image the `varlog-luks-key.service` unit, which fetches its LUKS key through the System Controller, fails and systemd waits out its device timeout before the login prompt. - -## 1. System Controller mailbox services - -`hal/mpfs250.c` drives the System Controller services mailbox (`SCBCTRL` at `0x37020000`, mailbox RAM at `0x37020800`). A small generic `mpfs_scb_request()` issues a service and polls the request/busy bits; `mpfs_scb_read()` reads the response. The following services are wrapped: - -| Service | Opcode | wolfBoot wrapper | -|---------|--------|------------------| -| Device serial number | `0x00` | `mpfs_read_serial_number()` | -| sNVM write (plaintext / authenticated / ciphertext) | `0x10` / `0x11` / `0x12` | `mpfs_snvm_write()` | -| sNVM read | `0x18` | `mpfs_snvm_read()` | -| PUF emulation | `0x20` | `mpfs_puf_emulation()` | -| Nonce (TRNG) | `0x21` | `mpfs_nonce()` | - -sNVM holds 221 modules of 252 bytes (plaintext) or 236 bytes plus a 12-byte User Secret Key (authenticated). The PUF emulation service returns a 32-byte device-unique response for a 16-byte challenge; for a fixed challenge the response is stable within a boot and across cold boots, which is what makes it usable as a KEK seed. The nonce service returns 32 random bytes. - -Note: the TRNG (nonce) and PUF services take far longer than serial/sNVM-read, so the busy-completion wait uses a larger bound (`MPFS_SCB_BUSY_TIMEOUT`) than the request-accept wait (`MPFS_SCB_TIMEOUT`). - -## 2. sNVM public-key keystore (`SNVM_KEYSTORE`) - -`hal/mpfs250_snvm.c` + `hal/mpfs250_snvm.h` implement the five-function keystore API (`keystore_num_pubkeys`, `keystore_get_buffer`, `keystore_get_size`, `keystore_get_mask`, `keystore_get_key_type`) by reading the trust anchor from sNVM. The on-sNVM layout is the same family as the OTP keystore (`src/flash_otp_keystore.c`): a `WOLFBOOT`-magic header followed by packed `struct keystore_slot` entries, read through a linear reader that spans consecutive sNVM modules from `SNVM_KEYSTORE_MODULE` (default 200, `SNVM_KEYSTORE_MAX_MODULES` default 1; an ML-DSA-87 key needs 11). For the on-device provisioning writer every module in the range must be left runtime-writable (not ROM) in the Libero sNVM configuration: on the stock Video Kit design only modules 200 and 201 are. A production bitstream delivers the same page contents as ROM instead (see the trust model above). - -Build wiring (mirrors `FLASH_OTP_KEYSTORE`): - -- `SNVM_KEYSTORE=1` links `hal/mpfs250_snvm.o` in place of the compiled-in `src/keystore.o`, so the running bootloader contains no public keys and serves them from sNVM. -- `SNVM_KEYSTORE_PROVISION=1` keeps the compiled keystore and adds `snvm_keystore_provision()`, a one-time on-device writer that copies the compiled keystore image into sNVM (called from `hal_init`). Run once on a board, then deploy the `SNVM_KEYSTORE=1` build. Both provisioning writers read the page first and skip it when it already holds the content (`snvm: module N already provisioned`), so the extra boot the programmer triggers after flashing, or a second boot of the provisioning image, does not spend another write cycle. - -A keystore page that reads back as all zeros is reported as `sNVM keystore zeroized` and halts the boot. That is the state a zeroize procedure leaves behind (the OS overwrites the keystore and encryption-key pages through the System Controller's plaintext sNVM write service), and it is distinguished from a corrupt header, which merely yields no keys, so a deliberately erased unit is identifiable on the console. Recovery is the provisioning build again. - -## 3. PUF KEK and AES key-wrap (`SNVM_KEK`) - -The KEK and key-wrap live in `hal/mpfs250_snvm.c` as well: - -- `mpfs_puf_kek()` derives the 256-bit KEK as `SHA-384(label || PUF(fixed-challenge))` truncated to 32 bytes. SHA-384 is used because it is the build's hash (SHA-256/HMAC/HKDF are compiled out); the PUF response is already a high-entropy device-unique secret, so a labelled hash provides domain separation without pulling in HKDF. -- `snvm_kek_wrap()` / `snvm_kek_unwrap()` wrap/unwrap a key with the KEK using RFC 3394 AES key-wrap (`wc_AesKeyWrap` / `wc_AesKeyUnWrap`). - -`SNVM_KEK=1` adds RFC 3394 key-wrap to the AES that an `ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1` build already compiles; the KEK has no other consumer, so the build refuses the flag without that combination. Determinism and the wrap/unwrap round trip are covered by the host unit test `tools/unit-tests/unit-snvm-kek.c`. - -The AES key-wrap uses the wolfCrypt software implementation. The Athena F5200 offload (`MPFS_ATHENA=1`, see `Targets.md`) serves SHA-384 and the bulk AES-256-CTR image decrypt through wolfCrypt crypto callbacks; the key-wrap is a few blocks per boot and stays in software. - -## 4. PUF-wrapped image-encryption key - -`hal/mpfs250_snvm.c` provides `wolfBoot_get_encrypt_key()` (under `CUSTOM_ENCRYPT_KEY`): it reads `[wrapped AES key (40 B)][nonce (16 B)]` from sNVM module `SNVM_ENCKEY_MODULE` (default 201, outside the keystore range), unwraps the key with the PUF KEK, and returns the plaintext key + nonce to the decrypt path. `snvm_enckey_provision()` (under `SNVM_ENCKEY_PROVISION`) wraps a key with the device KEK and writes the blob to sNVM. - -### Disk-image decryption enablement - -wolfBoot's image encryption (`EXT_ENCRYPTED`) was previously tied to the partition model and required `EXT_FLASH` or `MMU`. The M-mode disk-FIT path uses `WOLFBOOT_NO_PARTITIONS` and is neither. Two small, target-independent changes enable encrypted disk boot generally (regression-safe; host unit tests pass): - -- `src/libwolfboot.c`: the `EXT_ENCRYPTED requires EXT_FLASH or MMU` guard now also accepts `CUSTOM_ENCRYPT_KEY` (the platform supplies the key), and `hal_set_key()` (partition-resident key storage) is folded under `#ifndef CUSTOM_ENCRYPT_KEY`. -- `src/update_disk.c`: the `DISK_ENCRYPT` path gains `ForceZero` for secret cleanup. - -## Field-recoverable zeroize - -`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. - -## Build flags summary - -| Flag | Effect | -|------|--------| -| `SNVM_KEYSTORE` | Serve the public-key trust anchor from sNVM (replaces compiled keystore) | -| `SNVM_KEYSTORE_PROVISION` | Add the one-time on-device keystore writer | -| `SNVM_KEK` | Enable the PUF KEK + RFC 3394 AES key-wrap (needs the AES-256 `ENCRYPT` + `CUSTOM_ENCRYPT_KEY` build) | -| `SNVM_ENCKEY_PROVISION` | Add the one-time PUF-wrapped encryption-key writer | -| `SNVM_KEYSTORE_MODULE` / `SNVM_KEYSTORE_MAX_MODULES` / `SNVM_ENCKEY_MODULE` | sNVM module numbers (defaults 200 / 1 / 201; the key module must lie outside the keystore range and be runtime-writable) | -| `WOLFBOOT_SNVM_WRITE_APPROVED` | Required by every knob that writes sNVM; the build fails without it | -| `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | -| `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | -| `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | -| `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | -| `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | - -Example runtime encrypted build (compiled keystore for verification, PUF-wrapped key for decryption): - -```sh -make CROSS_COMPILE=riscv64-unknown-elf- LIBERO_FPGA_CONFIG_DIR= \ - ENCRYPT=1 ENCRYPT_WITH_AES256=1 CUSTOM_ENCRYPT_KEY=1 SNVM_KEK=1 wolfboot.elf -``` - -## Provisioning and validation recipe - -1. Build with `SNVM_ENCKEY_PROVISION=1` (and/or `SNVM_KEYSTORE_PROVISION=1`) plus `WOLFBOOT_SNVM_WRITE_APPROVED=1` added to the runtime build above, flash, and boot once: `hal_init` writes the PUF-wrapped AES key into sNVM[201] (and/or the keystore into sNVM[200]). -2. Sign + encrypt the FIT with a key file matching the provisioned key (`prov_aes_key` || `prov_aes_nonce` in `hal/mpfs250_snvm.c`, i.e. a 32-byte key followed by a 16-byte IV): - - ```sh - IMAGE_HEADER_SIZE=512 ./tools/keytools/sign --ecc384 --sha384 \ - --encrypt enc_key.bin --aes256 fitImage wolfboot_signing_private_key.der 1 - ``` - Note: `make keysclean` regenerates the signing key, so re-sign the image after any keysclean or the compiled keystore will not match. -3. Write `fitImage_v1_signed_and_encrypted.bin` to the SD boot partition. With the stock HSS, boot to the `>>` CLI, run `usbdmsc`, and `dd` to the first partition of the exposed disk. Do not move an SD-Wire mux while `usbdmsc` is running: HSS's MMC layer then serves one stale block for every LBA and does not recover until `usbdmsc` is restarted, which makes a populated card read as blank. -4. Flash the runtime build and cold-boot. Expected UART0: `Disk encryption enabled` -> `Decrypting image... done` -> `Firmware Valid.` -> `M->S handoff`, and UART1 reaches the Linux login with `Brought up 1 node, 4 CPUs` and no `failed to come online`. - -## Sample validation logs (MPFS250TS Video Kit) - -The PUF response is identical for a fixed challenge across cold power cycles while the nonce service is random, and the PUF-derived KEK wraps and unwraps a test key deterministically (the same wrapped bytes on every boot); both were verified on the kit and the KEK logic is covered by `tools/unit-tests/unit-snvm-kek.c`. - -Full encrypted boot captured on the Video Kit (UART1, S-mode wolfBoot under HSS; the standalone M-mode target decrypts the same image in place in DDR): the AES key is unwrapped from sNVM with the PUF KEK, the image is loaded, decrypted and verified. - -``` -wolfBoot Version: 2.9.0 -Disk encryption enabled -... -Load address 0x8E000000 -Attempting boot from P:A -Loading image from disk...done -Decrypting image...done -Checking image integrity...done -Verifying image signature...done -Firmware Valid. -Booting at 8E000000 -PolarFire SoC MPFS250 wolfBoot demo Application -``` - -Linux (UART1): - -``` -[ 0.040726] smp: Brought up 1 node, 4 CPUs -OpenEmbedded nodistro.0 mpfs-video-kit ttyS1 -mpfs-video-kit login: -``` - -No `failed to come online`; the only error lines are the known-cosmetic eth-SGMII PHY, i2c clock-divider, and mmc-tuning messages unrelated to this feature. diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 3589b8545c..f439285b55 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -1783,7 +1783,7 @@ int mpfs_nonce(uint8_t *nonce) * there keys off build-supplied macros only, not this fallback. */ #ifndef LINUX_BOOTARGS #ifndef LINUX_BOOTARGS_ROOT -/* wolfBoot SD layout (tools/scripts/program-sdcard.sh): p1=boot FIT, +/* wolfBoot SD layout (docs/PolarFire.md): p1=boot FIT, * p2=update, p3=rootfs. */ #define LINUX_BOOTARGS_ROOT "/dev/mmcblk0p3" #endif