diff --git a/config/examples/polarfire_mpfs250_m.config b/config/examples/polarfire_mpfs250_m.config index c566100cd0..23b5f1ab54 100644 --- a/config/examples/polarfire_mpfs250_m.config +++ b/config/examples/polarfire_mpfs250_m.config @@ -139,6 +139,12 @@ CFLAGS_EXTRA+=-DSDHCI_FORCE_CARD_DETECT #ENCRYPT_WITH_AES256=1 #OBJS_EXTRA=src/my_custom_encrypt_key.o +# Keep the golden image encrypted across a failover zeroize: its key lives in +# a second sNVM module (default 202) that the zeroize never erases. Needs the +# encrypted build above plus SNVM_KEK=1 and DISK_GOLDEN_SLOT=1. Sign the +# golden image with the module-202 key and the A/B images with module 201. +#DISK_GOLDEN_ENCRYPT_KEY=1 + # Used by test-application/ELF wrapper WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80200000 WOLFBOOT_PARTITION_SIZE=0x4000000 diff --git a/docs/PolarFire.md b/docs/PolarFire.md index a8ca7c3c6f..2426bf2a9a 100644 --- a/docs/PolarFire.md +++ b/docs/PolarFire.md @@ -1366,7 +1366,13 @@ Image encryption (`EXT_ENCRYPTED`) needs `EXT_FLASH`, `MMU` or `CUSTOM_ENCRYPT_K ### Field-recoverable zeroize -`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; it must be a plaintext signed image, since the key that decrypted the operational images is gone. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. +`DISK_FAILOVER_ZEROIZE=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and the `WOLFBOOT_SNVM_WRITE_APPROVED=1` gate every sNVM writer carries) turns a failover to the golden slot into a zeroize: when both operational slots are given up on, wolfBoot zeroes sNVM page `SNVM_ENCKEY_MODULE` (the PUF-wrapped image key, read back and checked; a page that already reads as zeros is not written again) and the head of both operational slots, then boots the golden image. The keystore page is never touched, so the golden image still verifies; by default it must be a plaintext signed image, since the key that decrypted the operational images is gone. `DISK_GOLDEN_ENCRYPT_KEY=1` lifts that restriction - see below. Nothing derived from the PUF is lost: the MRAM log key is re-derived on every boot. Re-provisioning the image key afterwards is the `SNVM_ENCKEY_PROVISION` service flow; a brownout during the zeroize write can leave the page read-only, in which case the device cannot be re-keyed. + +#### Keeping the golden image encrypted + +`DISK_GOLDEN_ENCRYPT_KEY=1` (with `DISK_GOLDEN_SLOT=1`, `SNVM_KEK=1` and `DISK_FAILOVER_ZEROIZE=1`) gives the golden image its own key in `SNVM_GOLDEN_ENCKEY_MODULE` (default 202), which the failover never erases. The zeroize still destroys the operational key in `SNVM_ENCKEY_MODULE`, so the operational images become undecryptable while the golden image stays encrypted at rest and bootable. Both keys are wrapped by the same device PUF KEK; the separation is which module gets erased. The platform picks the key with `wolfBoot_select_golden_encrypt_key()`, which has no default implementation on purpose: a target that cannot select a key fails to link rather than reading the golden image under the operational one. + +`SNVM_ENCKEY_PROVISION=1` writes both modules. Sign and encrypt the golden image with the module-202 key and the A/B images with the module-201 key; swapping them leaves the golden slot unbootable. With `SNVM_ENCKEY_PROVISION_EXTERN` the integrator supplies `snvm_golden_enckey_prov_key[32]` and `snvm_golden_enckey_prov_nonce[16]` alongside the operational pair. A golden bitstream that carries sNVM clients should carry module 202 only: carrying module 201 as well restores the operational key on reapply and so partially undoes the zeroize. ### Build flags summary @@ -1381,6 +1387,8 @@ Image encryption (`EXT_ENCRYPTED`) needs `EXT_FLASH`, `MMU` or `CUSTOM_ENCRYPT_K | `SNVM_KEYSTORE_REQUIRE_ROM` | Refuse to boot unless every keystore page is ROM-flagged (bitstream-provisioned keystore) | | `MPFS_SCB_SMODE_DENY` | PMP-fence S-mode off the System Controller mailbox and disable its DTB nodes | | `DISK_FAILOVER_ZEROIZE` | Zero the image-key page and the operational slots when falling back to the golden image | +| `DISK_GOLDEN_ENCRYPT_KEY` | Give the golden image its own key in `SNVM_GOLDEN_ENCKEY_MODULE`, so it stays encrypted across a zeroize | +| `SNVM_GOLDEN_ENCKEY_MODULE` | sNVM module holding the golden image key (default 202; must differ from `SNVM_ENCKEY_MODULE` and lie outside the keystore range) | | `SNVM_ENCKEY_INSECURE_TEST_KEY` | Acknowledges provisioning the built-in public test key | | `SNVM_ENCKEY_PROVISION_EXTERN` | Provision a production key instead: the integrator defines `snvm_enckey_prov_key[32]` and `snvm_enckey_prov_nonce[16]` in another object | diff --git a/docs/encrypted_partitions.md b/docs/encrypted_partitions.md index 59b6c85e9f..64ccfbe414 100644 --- a/docs/encrypted_partitions.md +++ b/docs/encrypted_partitions.md @@ -42,7 +42,7 @@ You can use the `CUSTOM_ENCRYPT_KEY` option to implement your own functions for: `wolfBoot_get_encrypt_key`, `wolfBoot_set_encrypt_key` and `wolfBoot_erase_encrypt_key`. -For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [PolarFire.md](PolarFire.md#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. +For an example that stores the AES key in non-volatile memory wrapped by a device-unique hardware PUF key (Microchip PolarFire SoC), see [PolarFire.md](PolarFire.md#hardware-root-of-trust-puf-kek-snvm-keystore-wrapped-image-key). Note that disk/`NO_PARTITIONS` boot can use `CUSTOM_ENCRYPT_KEY` for image encryption without `EXT_FLASH` or `MMU`. A disk-boot target with a golden slot can give the recovery image a key of its own, so it stays encrypted after the operational key is destroyed: see [Keeping the golden image encrypted](PolarFire.md#keeping-the-golden-image-encrypted). To enable: diff --git a/hal/mpfs250_snvm.c b/hal/mpfs250_snvm.c index d758df1f00..1b76e6fee9 100644 --- a/hal/mpfs250_snvm.c +++ b/hal/mpfs250_snvm.c @@ -420,6 +420,22 @@ int snvm_kek_unwrap(const uint8_t *in, uint32_t insz, uint8_t *key, #error "SNVM PUF encrypt-key provider expects a 256-bit (AES-256) image key" #endif +#ifdef DISK_GOLDEN_ENCRYPT_KEY +/* Which module wolfBoot_get_encrypt_key() reads. Defaults to the + * operational one, so the A/B version probe that runs before any slot is + * elected needs no selection. */ +static uint8_t snvm_enckey_module = SNVM_ENCKEY_MODULE; + +int wolfBoot_select_golden_encrypt_key(int golden) +{ + snvm_enckey_module = golden ? SNVM_GOLDEN_ENCKEY_MODULE : + SNVM_ENCKEY_MODULE; + return 0; +} +#else +#define snvm_enckey_module SNVM_ENCKEY_MODULE +#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) @@ -434,10 +450,11 @@ int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) * or candidate key material in either output. */ wc_ForceZero(key, ENCRYPT_KEY_SIZE); wc_ForceZero(nonce, ENCRYPT_NONCE_SIZE); - ret = mpfs_snvm_read(SNVM_ENCKEY_MODULE, NULL, NULL, page, + 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); + wolfBoot_printf("encrypt-key: sNVM read[%u] failed (%d)\n", + (unsigned)snvm_enckey_module, ret); return -1; } ret = snvm_kek_unwrap(page, SNVM_KEK_WRAPPED_LEN, key, ENCRYPT_KEY_SIZE); @@ -537,7 +554,30 @@ static const uint8_t prov_aes_nonce[ENCRYPT_NONCE_SIZE] = { }; #endif -int snvm_enckey_provision(void) +#ifdef DISK_GOLDEN_ENCRYPT_KEY +#if defined(SNVM_ENCKEY_PROVISION_EXTERN) +extern const uint8_t snvm_golden_enckey_prov_key[SNVM_KEK_AESKEY_LEN]; +extern const uint8_t snvm_golden_enckey_prov_nonce[ENCRYPT_NONCE_SIZE]; +#define prov_golden_aes_key snvm_golden_enckey_prov_key +#define prov_golden_aes_nonce snvm_golden_enckey_prov_nonce +#else +/* Second publicly known test vector, distinct from the operational one so a + * test cannot pass by reading the wrong module. */ +static const uint8_t prov_golden_aes_key[SNVM_KEK_AESKEY_LEN] = { + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f +}; +static const uint8_t prov_golden_aes_nonce[ENCRYPT_NONCE_SIZE] = { + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f +}; +#endif +#endif + +static int snvm_enckey_provision_module(uint8_t module, const uint8_t *key, + const uint8_t *nonce) { uint8_t page[MPFS_SNVM_PLAIN_DATA_LEN]; int ret, i; @@ -545,20 +585,35 @@ int snvm_enckey_provision(void) 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, + ret = snvm_kek_wrap(key, (uint32_t)SNVM_KEK_AESKEY_LEN, 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); + memcpy(page + SNVM_KEK_WRAPPED_LEN, nonce, ENCRYPT_NONCE_SIZE); + ret = snvm_page_write_if_changed(module, page); wolfBoot_printf("enckey provision: sNVM write[%d] ret=%d\n", - SNVM_ENCKEY_MODULE, ret); + (int)module, ret); wc_ForceZero(page, sizeof(page)); return ret; } + +int snvm_enckey_provision(void) +{ + int ret; + + ret = snvm_enckey_provision_module(SNVM_ENCKEY_MODULE, prov_aes_key, + prov_aes_nonce); +#ifdef DISK_GOLDEN_ENCRYPT_KEY + if (ret == 0) { + ret = snvm_enckey_provision_module(SNVM_GOLDEN_ENCKEY_MODULE, + prov_golden_aes_key, prov_golden_aes_nonce); + } +#endif + return ret; +} #endif /* SNVM_ENCKEY_PROVISION */ #endif /* CUSTOM_ENCRYPT_KEY && SNVM_KEK */ diff --git a/hal/mpfs250_snvm.h b/hal/mpfs250_snvm.h index 32c335c24b..65e44e6b92 100644 --- a/hal/mpfs250_snvm.h +++ b/hal/mpfs250_snvm.h @@ -115,6 +115,29 @@ int snvm_keystore_provision(void); #endif #endif +#ifdef DISK_GOLDEN_ENCRYPT_KEY +/* sNVM module holding the golden image's own wrapped key. The failover + * never erases it, which is what keeps the golden image encrypted after a + * zeroize of SNVM_ENCKEY_MODULE. */ +#ifndef SNVM_GOLDEN_ENCKEY_MODULE +#define SNVM_GOLDEN_ENCKEY_MODULE 202 +#endif +#if (SNVM_GOLDEN_ENCKEY_MODULE < 0) || \ + (SNVM_GOLDEN_ENCKEY_MODULE >= MPFS_SNVM_MODULE_MAX) +#error "SNVM_GOLDEN_ENCKEY_MODULE is outside the sNVM module range 0..220" +#endif +#if (SNVM_GOLDEN_ENCKEY_MODULE == SNVM_ENCKEY_MODULE) +#error "SNVM_GOLDEN_ENCKEY_MODULE must differ from SNVM_ENCKEY_MODULE" +#endif +#if defined(SNVM_KEYSTORE) || defined(SNVM_KEYSTORE_PROVISION) +#if (SNVM_GOLDEN_ENCKEY_MODULE >= SNVM_KEYSTORE_MODULE) && \ + (SNVM_GOLDEN_ENCKEY_MODULE < \ + (SNVM_KEYSTORE_MODULE + SNVM_KEYSTORE_MAX_MODULES)) +#error "SNVM_GOLDEN_ENCKEY_MODULE overlaps the sNVM keystore module range" +#endif +#endif +#endif /* DISK_GOLDEN_ENCRYPT_KEY */ + /* 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); diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index d8313809e8..1a568aa78d 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -830,6 +830,12 @@ int wolfBoot_initialize_encryption(void); int wolfBoot_set_encrypt_key(const uint8_t *key, const uint8_t *nonce); int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce); int wolfBoot_erase_encrypt_key(void); +#ifdef DISK_GOLDEN_ENCRYPT_KEY +/* Select the golden image's key (golden != 0) or the operational one for the + * next wolfBoot_get_encrypt_key(). Implemented by the platform that holds + * both; wolfBoot_erase_encrypt_key() always erases the operational key. */ +int wolfBoot_select_golden_encrypt_key(int golden); +#endif #if !defined(__WOLFBOOT) && defined(TZEN) diff --git a/options.mk b/options.mk index 0592944472..d0f33d3e96 100644 --- a/options.mk +++ b/options.mk @@ -180,7 +180,8 @@ ifeq ($(MPFS_SBI_SC_FLASH),1) endif # Field recovery: before the golden slot boots, zero the sNVM image-key page and the head of both -# operational slots (hal_disk_failover). The golden image must be a plaintext signed image. +# operational slots (hal_disk_failover). The golden image must be a plaintext signed image, unless +# DISK_GOLDEN_ENCRYPT_KEY gives it a key of its own. ifeq ($(DISK_FAILOVER_ZEROIZE),1) ifneq ($(DISK_GOLDEN_SLOT)$(SNVM_KEK),11) $(error DISK_FAILOVER_ZEROIZE=1 requires DISK_GOLDEN_SLOT=1 and SNVM_KEK=1) @@ -194,6 +195,19 @@ ifeq ($(DISK_FAILOVER_ZEROIZE),1) endif CFLAGS+=-D"DISK_FAILOVER_ZEROIZE" endif + +# Keep the golden image encrypted across a zeroize by storing its image key in a second sNVM +# module (SNVM_GOLDEN_ENCKEY_MODULE), which the failover never erases. The platform must +# implement wolfBoot_select_golden_encrypt_key(); there is deliberately no default, so a build +# that cannot select a key fails to link rather than decrypting the golden image with the wrong one. +ifeq ($(DISK_GOLDEN_ENCRYPT_KEY),1) + ifneq ($(DISK_GOLDEN_SLOT)$(SNVM_KEK)$(DISK_FAILOVER_ZEROIZE),111) + $(error DISK_GOLDEN_ENCRYPT_KEY=1 requires DISK_GOLDEN_SLOT=1, SNVM_KEK=1 \ + and DISK_FAILOVER_ZEROIZE=1: a second key exists so the zeroize can \ + destroy one and leave the other) + endif + CFLAGS+=-D"DISK_GOLDEN_ENCRYPT_KEY" +endif # QSPI SCK divider: a QSPI_CLK_DIV_* name from hal/mpfs250.h, default QSPI_CLK_DIV_8. ifneq ($(MPFS_QSPI_CLK_DIV),) CFLAGS+=-DMPFS_QSPI_CLK_DIV=$(MPFS_QSPI_CLK_DIV) diff --git a/src/update_disk.c b/src/update_disk.c index 815969ad67..e7e3c67c30 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -166,6 +166,10 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; /* Under the failover zeroize the image key may already be gone and the golden * image is a plaintext one, so only the operational slots are encrypted. * Without it every slot is encrypted and a missing key is fatal. */ +#if defined(DISK_GOLDEN_ENCRYPT_KEY) && !defined(DISK_FAILOVER_ZEROIZE) +#error "DISK_GOLDEN_ENCRYPT_KEY needs DISK_FAILOVER_ZEROIZE" +#endif + #ifdef DISK_FAILOVER_ZEROIZE #ifndef DISK_GOLDEN_SLOT #error "DISK_FAILOVER_ZEROIZE needs DISK_GOLDEN_SLOT" @@ -179,7 +183,13 @@ static int failover_cause; failover_cause = (c); } } while (0) static int disk_key_ok = 1; #define SLOT_ENCRYPTED_KEY_OK() (disk_key_ok != 0) +#ifdef DISK_GOLDEN_ENCRYPT_KEY +/* The golden image has a key of its own, so it stays encrypted after the + * operational one is zeroized. */ +#define SLOT_ENCRYPTED(s) 1 +#else #define SLOT_ENCRYPTED(s) (!SLOT_IS_GOLDEN(s)) +#endif /* Bytes zeroed at the start of each operational slot: the manifest header * and the signature go, so the image can never be elected again. */ #ifndef DISK_FAILOVER_ERASE_SIZE @@ -341,6 +351,21 @@ static void disk_crypto_clear(void) wc_ForceZero(disk_encrypt_nonce, sizeof(disk_encrypt_nonce)); } +#ifdef DISK_GOLDEN_ENCRYPT_KEY +/* Re-key the cipher and the IV nonce for the slot about to be read. Both are + * keyed once up front, which is enough only while every slot shares a key. */ +static int disk_crypto_rekey(int slot) +{ + if (wolfBoot_select_golden_encrypt_key(SLOT_IS_GOLDEN(slot)) != 0) { + return -1; + } + if (crypto_init() != 0) { + return -1; + } + return wolfBoot_get_encrypt_key(disk_encrypt_key, disk_encrypt_nonce); +} +#endif + static void disk_decrypted_header_clear(uint8_t *hdr) { wc_ForceZero(hdr, IMAGE_HEADER_SIZE); @@ -1068,8 +1093,18 @@ void RAMFUNCTION wolfBoot_start(void) hdr_ptr = p_hdr; #ifdef DISK_ENCRYPT - /* Decrypt header to parse image size. A golden image is signed - * but not encrypted: it must boot after the image key is gone. */ +#ifdef DISK_GOLDEN_ENCRYPT_KEY + /* Each slot carries its own key, so re-key before reading one. */ + if (disk_crypto_rekey(selected) != 0) { + wolfBoot_printf("Error selecting the key for %s\r\n", part_name); + SET_FAILOVER_IF_UNSET(DISK_FAILOVER_NO_KEY); + selected ^= 1; + continue; + } +#endif + /* Decrypt header to parse image size. Without + * DISK_GOLDEN_ENCRYPT_KEY a golden image is signed but not + * encrypted: it must boot after the image key is gone. */ if (!SLOT_ENCRYPTED(selected)) { hdr_ptr = p_hdr; } diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 12a69a11e7..7d392b9e5d 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -70,7 +70,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update-monolithic \ unit-nsc-update \ unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-update-flash-custom-trailer unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ - unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-failover unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-update-disk-fit-ramdisk unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ + unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-failover unit-update-disk-golden-enckey unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-update-disk-fit-ramdisk unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ @@ -528,6 +528,8 @@ unit-update-disk-golden:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RA -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-disk-failover:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-disk-golden-enckey:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Non-FSP disk-boot OOB regression (CRIT-03). WOLFBOOT_RAMBOOT_MAX_SIZE is the # cap the loader applies to the unauthenticated header fw_size before loading to # RAM, the same bound update_disk.c and update_ram.c enforce. @@ -1333,6 +1335,9 @@ unit-update-disk-golden: ../../include/target.h unit-update-disk-golden.c unit-update-disk-failover: ../../include/target.h unit-update-disk-failover.c gcc -o $@ unit-update-disk-failover.c $(CFLAGS) $(LDFLAGS) +unit-update-disk-golden-enckey: ../../include/target.h unit-update-disk-golden-enckey.c + gcc -o $@ unit-update-disk-golden-enckey.c $(CFLAGS) $(LDFLAGS) + # WOLFBOOT_FSP (x86) boot path of update_disk.c: the low-memory (tolum) # size check must reject a slot and fall back to the other one, like every # other per-slot rejection in the retry loop. diff --git a/tools/unit-tests/unit-update-disk-golden-enckey.c b/tools/unit-tests/unit-update-disk-golden-enckey.c new file mode 100644 index 0000000000..3436b51182 --- /dev/null +++ b/tools/unit-tests/unit-update-disk-golden-enckey.c @@ -0,0 +1,440 @@ +/* unit-update-disk-golden-enckey.c + * + * Unit test for a golden slot encrypted under its own key + * (DISK_GOLDEN_ENCRYPT_KEY), so a zeroize of the operational key leaves the + * recovery image both encrypted and bootable. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#define WOLFBOOT_UPDATE_DISK +#define WOLFBOOT_SELF_UPDATE_MONOLITHIC +#define RAM_CODE +#define WOLFBOOT_SELF_HEADER +#define EXT_ENCRYPTED +#define ENCRYPT_WITH_CHACHA +#define HAVE_CHACHA +#define IMAGE_HEADER_SIZE 256 +#define BOOT_PART_A 0 +#define BOOT_PART_B 1 +#define BOOT_PART_GOLDEN 2 +#define DISK_GOLDEN_SLOT +#define DISK_FAILOVER_ZEROIZE +#define DISK_GOLDEN_ENCRYPT_KEY +#define MOCK_ADDRESS_BOOT 0xCD000000 + +#include +#include +#include +#include + +#include "hal.h" +#include "target.h" +#include "wolfboot/wolfboot.h" +#include "image.h" +#include "loader.h" +#include + +#define TEST_PAYLOAD_SIZE 64 +#define TEST_PART_SIZE (IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE + 1024) +#define FILL_A 0xA1 +#define FILL_B 0xB2 +#define FILL_G 0xC3 +#define N_PARTS 3 + +/* The two image keys. The cipher mock below is an XOR with the installed key + * byte, so a slot read under the wrong key yields a wrong magic and a wrong + * payload fill: key separation is actually exercised, not just recorded. */ +#define OP_KEY_BYTE 0x5A +#define GOLDEN_KEY_BYTE 0x77 + +/* The region of a partition that is encrypted; the trailer is not. */ +#define ENC_LEN (IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE) + +static uint8_t load_buffer[TEST_PART_SIZE]; +#define WOLFBOOT_LOAD_ADDRESS ((uintptr_t)load_buffer) + +static uint8_t part_image[N_PARTS][TEST_PART_SIZE]; +static int mock_reads[N_PARTS]; +static int mock_writes[N_PARTS]; +static int mock_bad_fill[N_PARTS]; +static int mock_do_boot_called; +static int mock_booted_fill; +static int mock_op_key_gone; +static int mock_golden_key_gone; +static int mock_op_key_erased; +static int mock_golden_key_erased; +static int mock_golden_selected; +static uint8_t mock_pending_key; +static uint8_t mock_cur_key; +ChaCha chacha; + +static void set_u16_le(uint8_t *dst, uint16_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)(value >> 8); +} + +static void set_u32_le(uint8_t *dst, uint32_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)((value >> 8) & 0xFF); + dst[2] = (uint8_t)((value >> 16) & 0xFF); + dst[3] = (uint8_t)(value >> 24); +} + +/* Build a plaintext image, then "encrypt" the image region under key_byte. */ +static void build_image(uint8_t *image, uint32_t version, uint8_t fill, + uint8_t key_byte) +{ + int i; + + memset(image, 0, TEST_PART_SIZE); + set_u32_le(image, WOLFBOOT_MAGIC); + set_u32_le(image + sizeof(uint32_t), TEST_PAYLOAD_SIZE); + set_u16_le(image + IMAGE_HEADER_OFFSET, HDR_VERSION); + set_u16_le(image + IMAGE_HEADER_OFFSET + sizeof(uint16_t), 4); + set_u32_le(image + IMAGE_HEADER_OFFSET + 2 * sizeof(uint16_t), version); + memset(image + IMAGE_HEADER_SIZE, fill, TEST_PAYLOAD_SIZE); + for (i = 0; i < ENC_LEN; i++) { + image[i] ^= key_byte; + } +} + +static void reset_mocks(void) +{ + memset(load_buffer, 0, sizeof(load_buffer)); + build_image(part_image[0], 3, FILL_A, OP_KEY_BYTE); + build_image(part_image[1], 2, FILL_B, OP_KEY_BYTE); + build_image(part_image[2], 1, FILL_G, GOLDEN_KEY_BYTE); + memset(mock_reads, 0, sizeof(mock_reads)); + memset(mock_writes, 0, sizeof(mock_writes)); + memset(mock_bad_fill, 0, sizeof(mock_bad_fill)); + mock_do_boot_called = 0; + mock_booted_fill = -1; + mock_op_key_gone = 0; + mock_golden_key_gone = 0; + mock_op_key_erased = 0; + mock_golden_key_erased = 0; + mock_golden_selected = -1; + mock_pending_key = OP_KEY_BYTE; + mock_cur_key = OP_KEY_BYTE; + wolfBoot_panicked = 0; +} + +static void mark_bad(uint8_t fill) +{ + int i; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] == 0) { + mock_bad_fill[i] = fill; + return; + } + } +} + +static int pending_key_gone(void) +{ + if (mock_pending_key == GOLDEN_KEY_BYTE) { + return mock_golden_key_gone; + } + return mock_op_key_gone; +} + +/* crypto_init() expands to this: it installs the selected key, the way the + * real one re-reads it from storage. */ +int chacha_init(void) +{ + if (pending_key_gone()) { + return -1; + } + mock_cur_key = mock_pending_key; + return 0; +} + +int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter) +{ + (void)ctx; (void)inIv; (void)counter; + return 0; +} + +int wc_Chacha_Process(ChaCha* ctx, byte* output, const byte* input, + word32 msglen) +{ + word32 i; + (void)ctx; + for (i = 0; i < msglen; i++) { + output[i] = (byte)(input[i] ^ mock_cur_key); + } + return 0; +} + +void wc_ForceZero(void* mem, size_t len) +{ + volatile uint8_t *p = (volatile uint8_t *)mem; + while (len-- > 0) + *p++ = 0; +} + +int wolfBoot_initialize_encryption(void) +{ + return chacha_init(); +} + +int wolfBoot_select_golden_encrypt_key(int golden) +{ + mock_golden_selected = golden; + mock_pending_key = golden ? GOLDEN_KEY_BYTE : OP_KEY_BYTE; + return 0; +} + +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + if (pending_key_gone()) { + return -1; + } + mock_cur_key = mock_pending_key; + memset(key, mock_cur_key, ENCRYPT_KEY_SIZE); + memset(nonce, 0xC3, ENCRYPT_NONCE_SIZE); + return 0; +} + +/* The failover only ever erases the operational key. The golden counter is + * here so a regression that erased it would fail the suite. */ +int wolfBoot_erase_encrypt_key(void) +{ + mock_op_key_erased++; + mock_op_key_gone = 1; + return 0; +} + +int disk_init(int drv) +{ + (void)drv; + return 0; +} + +int disk_open(int drv) +{ + (void)drv; + return 0; +} + +void disk_close(int drv) +{ + (void)drv; +} + +int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + mock_reads[part]++; + memcpy(buf, part_image[part] + off, (size_t)sz); + return (int)sz; +} + +int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, + const uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + mock_writes[part]++; + memcpy(part_image[part] + off, buf, (size_t)sz); + return (int)sz; +} + +int disk_part_size(int drv, int part, uint64_t *size) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + *size = TEST_PART_SIZE; + return 0; +} + +int wolfBoot_open_image_address(struct wolfBoot_image* img, uint8_t* image) +{ + uint32_t magic; + uint32_t fw_size; + + memcpy(&magic, image, sizeof(magic)); + if (magic != WOLFBOOT_MAGIC) + return -1; + memset(img, 0, sizeof(*img)); + img->hdr = image; + memcpy(&fw_size, image + sizeof(uint32_t), sizeof(fw_size)); + img->fw_size = fw_size; + img->fw_base = image + IMAGE_HEADER_SIZE; + img->hdr_ok = 1; + return 0; +} + +int wolfBoot_verify_integrity(struct wolfBoot_image* img) +{ + int i; + uint8_t fill = load_buffer[0]; + (void)img; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] != 0 && mock_bad_fill[i] == fill) + return -1; + } + img->sha_ok = 1; + return 0; +} + +int wolfBoot_verify_authenticity(struct wolfBoot_image* img) +{ + img->signature_ok = 1; + return 0; +} + +int wolfBoot_image_part_matches(struct wolfBoot_image* img) +{ + (void)img; + return 0; +} + +int wolfBoot_get_dts_size(void *dts_addr, uint32_t capacity) +{ + (void)capacity; (void)dts_addr; + return -1; +} + +void hal_prepare_boot(void) +{ +} + +void do_boot(const uint32_t *address) +{ + mock_do_boot_called++; + mock_booted_fill = ((const uint8_t*)address)[0]; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; (void)len; + return 0; +} + +#include "update_disk.c" + +START_TEST(test_operational_boots_under_its_own_key) +{ + reset_mocks(); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(mock_op_key_erased, 0); + ck_assert_int_eq(mock_golden_key_erased, 0); +} +END_TEST + +START_TEST(test_encrypted_golden_boots_after_the_zeroize) +{ + /* The case that is impossible without this option: both operational + * images are given up on, the failover zeroizes their key, and the + * golden image still decrypts because its key is a different one. */ + reset_mocks(); + build_image(part_image[1], 3, FILL_B, OP_KEY_BYTE); + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_op_key_erased, 1); + ck_assert_int_eq(mock_golden_selected, 1); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_failover_leaves_the_golden_key_alone) +{ + reset_mocks(); + memset(part_image[0], 0, TEST_PART_SIZE); + memset(part_image[1], 0, TEST_PART_SIZE); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_op_key_erased, 1); + /* The whole point: the recovery key survives the zeroize. */ + ck_assert_int_eq(mock_golden_key_erased, 0); + ck_assert_int_eq(mock_golden_key_gone, 0); +} +END_TEST + +START_TEST(test_golden_fails_closed_without_its_own_key) +{ + /* No silent plaintext fallback: with the golden key gone there is + * nothing left to boot. */ + reset_mocks(); + mock_op_key_gone = 1; + mock_golden_key_gone = 1; + wolfBoot_start(); + ck_assert_int_eq(mock_do_boot_called, 0); + ck_assert_int_eq(wolfBoot_panicked, 1); +} +END_TEST + +START_TEST(test_golden_under_the_operational_key_does_not_boot) +{ + /* A golden image encrypted with the operational key must not boot once + * the golden slot is read under the golden key. */ + reset_mocks(); + build_image(part_image[2], 1, FILL_G, OP_KEY_BYTE); + memset(part_image[0], 0, TEST_PART_SIZE); + memset(part_image[1], 0, TEST_PART_SIZE); + wolfBoot_start(); + ck_assert_int_eq(mock_do_boot_called, 0); + ck_assert_int_eq(wolfBoot_panicked, 1); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot"); + TCase *tc = tcase_create("update-disk-golden-enckey"); + + tcase_add_test(tc, test_operational_boots_under_its_own_key); + tcase_add_test(tc, test_encrypted_golden_boots_after_the_zeroize); + tcase_add_test(tc, test_failover_leaves_the_golden_key_alone); + tcase_add_test(tc, test_golden_fails_closed_without_its_own_key); + tcase_add_test(tc, test_golden_under_the_operational_key_does_not_boot); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = wolfboot_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +}