diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 825ec1f000..b6736e3fe0 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -425,6 +425,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 eb7d095f51..ce090b5ef2 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:= @@ -728,7 +744,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) @@ -768,6 +784,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 f7c7d720ce..b482910a60 100644 --- a/arch.mk +++ b/arch.mk @@ -1167,9 +1167,16 @@ 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. + # 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))) + $(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 ifneq ($(LIBERO_FPGA_CONFIG_DIR),) CFLAGS+=-DMPFS_DDR_INIT -I$(LIBERO_FPGA_CONFIG_DIR) # Generic Cadence DDR controller driver + the MPFS PHY/PLL/training @@ -1222,9 +1229,11 @@ 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: soft-float lp64 to match Microchip's CAL archives, which an lp64d + # build cannot link against. rv64imac not rv64imafd because this toolchain + # ships no F/D-ISA + soft-float multilib; wolfBoot emits no FP anyway. + 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/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 bc22c5f677..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 @@ -77,8 +79,13 @@ 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 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) WOLFBOOT_SECTOR_SIZE?=0x1000 @@ -131,17 +138,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 7a7710f767..81e5876a80 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -1315,6 +1315,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. @@ -1596,7 +1600,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) @@ -1651,6 +1655,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: 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..2dc1ebd2f8 --- /dev/null +++ b/docs/polarfire_snvm_puf.md @@ -0,0 +1,143 @@ +# 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. + +## 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). 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. + +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, and `DISK_DECRYPT_STAGING=1` lets a HAL supply `hal_disk_decrypt_addr()` / `hal_disk_decrypt_copy()` so the image is decrypted through a staging buffer instead of in place (default: in place). + +### 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_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 d84aace4e1..2d93c5777d 100644 --- a/hal/mpfs250-m.ld +++ b/hal/mpfs250-m.ld @@ -26,19 +26,19 @@ 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. 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) - * ECC384 + SHA384 + SPMATHALL + NO_ASM measured peak: ~6KB. - * 32KB provides 5x headroom. */ -PROVIDE(STACK_SIZE = 32k); +/* 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 + * addresses the startup pin loop never made resident. */ +ASSERT(LENGTH(L2_SCRATCH) <= 4 * 0x20000, + "L2_SCRATCH exceeds the 4 scratchpad ways (512 KB)") SECTIONS { diff --git a/hal/mpfs250.c b/hal/mpfs250.c index 3bad3aae08..815532137e 100644 --- a/hal/mpfs250.c +++ b/hal/mpfs250.c @@ -45,6 +45,30 @@ #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 +/* 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) @@ -362,6 +386,435 @@ 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; +} + +static int mpfs_athena_sha384_blocklen(void) +{ + return (int)iGetBlockLen(SATHASHTYPE_SHA384); +} + +static int mpfs_athena_sha384_init(void *ctx) +{ + if (CALHashCtxIni((SATRESCONTEXTPTR)ctx, SATHASHTYPE_SHA384) + != SATR_SUCCESS) { + 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) +{ + if (CALHashCtx((SATRESHANDLE)0, (SATRESCONTEXTPTR)ctx, in, + (SATUINT32_t)len, NULL, SAT_FALSE) != SATR_SUCCESS) { + 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) +{ + if (CALHashCtx((SATRESHANDLE)0, (SATRESCONTEXTPTR)ctx, in, + (SATUINT32_t)len, digest, SAT_TRUE) != SATR_SUCCESS) { + 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 +/* 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 + +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; +}; + +static struct athena_hash_slot athena_hash_slots[ATHENA_HASH_SLOTS]; + +/* 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; + +/* 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; + + 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, 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; + word32 sz; + + 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 || (sz % WC_AES_BLOCK_SIZE) != 0) { + /* A trailing partial block would mean reproducing wolfCrypt's + * leftover-keystream bookkeeping here; leave those to software. */ + return CRYPTOCB_UNAVAILABLE; + } + + /* The engine advances the counter in place, so aes->reg stays correct for + * any subsequent call on the same context. */ + if (mpfs_athena_aes256_ctr(aes->devKey, aes->reg, info->cipher.aesctr.in, + info->cipher.aesctr.out, sz) != 0) { + return WC_HW_E; + } + 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) { + /* 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 CRYPTOCB_UNAVAILABLE; + } + if (mpfs_athena_sha384_init(slot->ctx) != 0) { + return CRYPTOCB_UNAVAILABLE; + } + slot->owner = info->hash.sha384; + slot->part_len = 0; + } + + if (info->hash.in != NULL && info->hash.inSz > 0) { + if (athena_hash_feed(slot, info->hash.in, info->hash.inSz) != 0) { + slot->owner = NULL; + 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; + if (ret != 0) { + return WC_HW_E; + } + } + + return 0; +} + +/* Returns 0 when the engine is up and the callback is registered. */ +/* 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. */ +static int mpfs_athena_selfcheck(void) +{ + wc_Sha384 sha; + uint8_t buf[256]; + uint8_t digest[48]; + int ret; + int i; + + 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) { + 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) { + ret = wc_Sha384Final(&sha, digest); + } + wc_Sha384Free(&sha); + if (ret != 0 || athena_cb_calls == 0 || + memcmp(digest, athena_kat_sha384_256b, sizeof(digest)) != 0) { + 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_cb_calls == 0 || + memcmp(out, athena_kat_aesctr, sizeof(out)) != 0 || + memcmp(out, out2, sizeof(out)) != 0) { + 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\n"); + wolfBoot_panic(); + } +#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 @@ -399,7 +852,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"); @@ -460,6 +913,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", @@ -472,12 +929,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) { @@ -506,6 +959,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"); @@ -527,63 +999,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. */ @@ -745,6 +1373,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 diff --git a/hal/mpfs250.h b/hal/mpfs250.h index a3de52b817..48ae21598b 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 @@ -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) @@ -237,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 */ @@ -257,17 +282,56 @@ #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, 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. 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) +#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 +369,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 +383,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 @@ -351,6 +413,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 */ @@ -374,7 +449,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 @@ -385,7 +459,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 * @@ -509,7 +582,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 @@ -561,7 +652,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 @@ -599,7 +689,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 * @@ -660,7 +749,6 @@ int mpfs_pdma_memcpy(void *dst, const void *src, uint32_t bytes); #endif #endif /* __ASSEMBLER__ */ - #ifdef EXT_FLASH /* QSPI Flash Controller * @@ -778,5 +866,4 @@ int qspi_init(void); #endif /* EXT_FLASH */ - #endif /* MPFS250_DEF_INCLUDED */ diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index 551c82f2b6..fcffd9f490 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,22 +518,9 @@ static void setup_segments(void) mb(); } -/* DDR Controller Configuration - * - * 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 - * 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 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 }, @@ -659,95 +629,11 @@ 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 }, + + /* 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 }, { MC_CFG_LOOKAHEAD_ACT, LIBERO_SETTING_CFG_LOOKAHEAD_ACT }, { MC_INIT_AUTOINIT_DISABLE, LIBERO_SETTING_INIT_AUTOINIT_DISABLE }, @@ -917,10 +803,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 +1108,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 @@ -2247,6 +2216,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 +2267,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 +2691,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 +2787,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 +3054,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 */ @@ -3244,6 +3234,49 @@ 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) { + wolfBoot_printf("DDR: WARNING DQ/DQS window %u taps after " + "%u resets, continuing\n", (unsigned)eye_min, + (unsigned)n); + } + rst_cnt[0] = 0; + rst_cnt[1] = 0; + } train_stat = DDRPHY_REG(PHY_TRAINING_STATUS); /* Fast path: if TIP completed full training (train_stat diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c new file mode 100644 index 0000000000..035ea4cdbb --- /dev/null +++ b/hal/mpfs250_snvm.c @@ -0,0 +1,433 @@ +/* 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) && + (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; +} + +int keystore_num_pubkeys(void) +{ + uint8_t hdr_buf[SNVM_KEYSTORE_HDR_SIZE]; + struct snvm_keystore_hdr *hdr = (struct snvm_keystore_hdr *)hdr_buf; + + if (snvm_keystore_read(0, hdr_buf, SNVM_KEYSTORE_HDR_SIZE) != 0) { + return 0; + } + if (memcmp(hdr->magic, SNVM_KEYSTORE_MAGIC, 8) != 0) { + /* An all-zero header is a zeroized keystore, not a provisioning + * mistake: say so and stop, there is no trust anchor to verify with. */ + if (hdr_buf[0] == 0 && + memcmp(hdr_buf, hdr_buf + 1, SNVM_KEYSTORE_HDR_SIZE - 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; + } + 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(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..f15c12cebf --- /dev/null +++ b/hal/mpfs250_snvm.h @@ -0,0 +1,139 @@ +/* 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. */ +#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 */ + +#endif /* MPFS250_SNVM_H */ diff --git a/include/hal.h b/include/hal.h index 55d93230ab..2bd956886e 100644 --- a/include/hal.h +++ b/include/hal.h @@ -43,6 +43,12 @@ extern int wolfBoot_fit_memcpy(void *dst, const void *src, uint32_t len); #else extern void do_boot(const uint32_t *app_offset); #endif +#ifdef DISK_DECRYPT_STAGING +/* Disk decrypt staging (src/update_disk.c): the address the ciphertext is read + * through, and the copy that lands plaintext in the load region. */ +uintptr_t hal_disk_decrypt_addr(uintptr_t addr); +int hal_disk_decrypt_copy(void *dst, const void *src, uint32_t len); +#endif extern void arch_reboot(void); /* Simulator-only calls */ diff --git a/options.mk b/options.mk index cd8719d03d..2ddba27ef9 100644 --- a/options.mk +++ b/options.mk @@ -77,6 +77,75 @@ 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: 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 @@ -835,6 +904,17 @@ ifeq ($(DISK_BOOT_CONFIRM),1) CFLAGS+=-D"DISK_BOOT_CONFIRM=1" endif +# Disk-boot decrypt staging (src/update_disk.c): the HAL supplies +# hal_disk_decrypt_addr() / hal_disk_decrypt_copy() for targets whose CPU +# stores into the load region are not coherent with the DMA that filled it. +DISK_DECRYPT_STAGING ?= 0 +ifeq ($(DISK_DECRYPT_STAGING),1) + CFLAGS+=-D"DISK_DECRYPT_STAGING" + ifneq ($(DISK_DECRYPT_STAGE_SZ),) + CFLAGS+=-DDISK_DECRYPT_STAGE_SZ=$(DISK_DECRYPT_STAGE_SZ) + endif +endif + # Optional read-only filesystem support for disk boot (src/update_disk.c), # so a boot slot can name a file instead of requiring the signed image at # raw offset 0 of a partition. DISK_FS = fat32 | ext4 | both. Leaving it @@ -1566,6 +1646,11 @@ ifneq (,$(filter RISCV RISCV64,$(ARCH))) CFLAGS+=-DSTACK_SIZE_PER_HART=$(STACK_SIZE_PER_HART) endif +# 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 + CFLAGS+=$(CFLAGS_EXTRA) OBJS+=$(OBJS_EXTRA) @@ -1621,6 +1706,35 @@ 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 + # 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. diff --git a/src/boot_riscv_start.S b/src/boot_riscv_start.S index 5177180389..650cbb7032 100644 --- a/src/boot_riscv_start.S +++ b/src/boot_riscv_start.S @@ -67,16 +67,34 @@ _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) + /* 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 */ + 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 +115,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/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/src/riscv_sbi.c b/src/riscv_sbi.c index 734f475937..f81f110fa9 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". */ @@ -560,8 +561,17 @@ static long sbi_wait_ipi_done(unsigned long mask, unsigned long base, sbi_hart_state[h] != SBI_HSM_STARTED) { continue; } - spin = 10000000U; + /* 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 (sbi_ipi_done[h] <= sbi_ipi_wait_gen[h] && spin > 0U) { + if (sbi_ipi_ops[self] != 0U) { + sbi_ipi_irq(self); + } spin--; } if (sbi_ipi_done[h] <= sbi_ipi_wait_gen[h]) { @@ -580,8 +590,8 @@ 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 */ #ifdef DEBUG_SBI + volatile uint32_t spin; /* UART-drain delay before SRST/SHUTDOWN reset */ static uint32_t sbi_dbg_calls = 0; #endif @@ -717,8 +727,11 @@ 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; } for (k = 0; k < n; k += c) { c = n - k; @@ -746,10 +759,14 @@ 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 #ifdef TARGET_mpfs250 SYSREG_MSS_RESET_CR = 0xDEAD; #endif @@ -798,8 +815,10 @@ unsigned long sbi_handle_ecall(unsigned long *regs, unsigned long epc) 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++) { } +#endif #ifdef TARGET_mpfs250 SYSREG_MSS_RESET_CR = 0xDEAD; #endif diff --git a/src/update_disk.c b/src/update_disk.c index 4c1264165b..63b59891ae 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -131,11 +131,34 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; # error "WOLFBOOT_RAMBOOT_MAX_SIZE required to bound the disk image RAM load" #endif +/* Address for the in-place decrypt; a platform that cannot store to the normal + * load view overrides it (see the crypto_decrypt call below). */ +/* DISK_DECRYPT_STAGING: on targets where CPU stores into the load region are + * not coherent with the DMA that filled it, the ciphertext is read through + * the view hal_disk_decrypt_addr() names, decrypted into a staging buffer and + * landed with hal_disk_decrypt_copy() (the same copy the load used). */ +#ifdef DISK_DECRYPT_STAGING +#define DISK_DECRYPT_ADDR(a) hal_disk_decrypt_addr((uintptr_t)(a)) +#define DISK_DECRYPT_COPY(d, s, n) hal_disk_decrypt_copy((d), (s), (n)) +#ifndef DISK_DECRYPT_STAGE_SZ +#define DISK_DECRYPT_STAGE_SZ 4096 +#endif +#else +#define DISK_DECRYPT_ADDR(a) (a) +#endif + #ifdef DISK_ENCRYPT /* Module-level storage for encryption key */ static uint8_t disk_encrypt_key[ENCRYPT_KEY_SIZE]; +#ifdef DISK_DECRYPT_STAGE_SZ +/* Every non-final chunk must be whole cipher blocks or the counter desyncs. */ +#if (DISK_DECRYPT_STAGE_SZ % ENCRYPT_BLOCK_SIZE) != 0 +#error "DISK_DECRYPT_STAGE_SZ must be a multiple of ENCRYPT_BLOCK_SIZE" +#endif +#endif + static uint16_t get_hdr_u16(const uint8_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); @@ -922,8 +945,40 @@ void RAMFUNCTION wolfBoot_start(void) 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); + /* DISK_DECRYPT_ADDR aliases the read; DISK_DECRYPT_COPY lands the + * plaintext when the CPU cannot write that view. The stage size is a + * multiple of both ENCRYPT_BLOCK_SIZE values, keeping the counter in step. */ +#ifdef DISK_DECRYPT_COPY + { + static uint8_t dec_stage[DISK_DECRYPT_STAGE_SZ]; + uint32_t dec_off = 0; + uint32_t dec_chunk; + + while (dec_off < os_image.fw_size) { + dec_chunk = os_image.fw_size - dec_off; + if (dec_chunk > (uint32_t)DISK_DECRYPT_STAGE_SZ) { + dec_chunk = (uint32_t)DISK_DECRYPT_STAGE_SZ; + } + crypto_decrypt(dec_stage, + (uint8_t*)DISK_DECRYPT_ADDR((uint8_t*)load_address + dec_off), + dec_chunk); + if (DISK_DECRYPT_COPY((uint8_t*)load_address + dec_off, + dec_stage, dec_chunk) != 0) { + wc_ForceZero(dec_stage, sizeof(dec_stage)); + disk_decrypted_header_clear(dec_hdr); + disk_crypto_clear(); + wolfBoot_printf("Decrypt copy to load address failed\r\n"); + wolfBoot_panic(); + } + dec_off += dec_chunk; + wolfBoot_watchdog_feed(); + } + wc_ForceZero(dec_stage, sizeof(dec_stage)); + } +#else + crypto_decrypt((uint8_t*)DISK_DECRYPT_ADDR(load_address), + (uint8_t*)DISK_DECRYPT_ADDR(load_address), os_image.fw_size); +#endif BENCHMARK_END("done"); #endif diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index d65816f77a..db4cb7f7ae 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -100,6 +100,8 @@ TESTS+=unit-flash-erase-mcxw 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 @@ -749,6 +751,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; +}