diff --git a/.github/workflows/build-and-test-refactor.yml b/.github/workflows/build-and-test-refactor.yml index ad869ab59..69c48eec2 100644 --- a/.github/workflows/build-and-test-refactor.yml +++ b/.github/workflows/build-and-test-refactor.yml @@ -66,6 +66,10 @@ jobs: - name: Build and test refactor DMA ASAN run: cd test-refactor/posix && make clean && make -j DMA=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + # Build and test with NVM flash CRC16 integrity checking enabled + - name: Build and test refactor NVM flash CRC ASAN + run: cd test-refactor/posix && make clean && make -j NVM_FLASH_CRC=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + # Build and test with LMS and XMSS both in verify-only mode - name: Build and test refactor DMA ASAN LMS/XMSS verify-only run: cd test-refactor/posix && make clean && make -j DMA=1 ASAN=1 LMS_VERIFY_ONLY=1 XMSS_VERIFY_ONLY=1 WOLFSSL_DIR=../../wolfssl && make run diff --git a/.github/workflows/build-and-test-whnvmtool.yml b/.github/workflows/build-and-test-whnvmtool.yml index d69b4e135..d11db4826 100644 --- a/.github/workflows/build-and-test-whnvmtool.yml +++ b/.github/workflows/build-and-test-whnvmtool.yml @@ -57,6 +57,9 @@ jobs: - name: Build and test NVM tool with ASAN run: cd tools/whnvmtool && make clean && make check WOLFSSL_DIR=../../wolfssl ASAN=1 + - name: Build and test NVM tool with NVM flash CRC ASAN + run: cd tools/whnvmtool && make clean && make check WOLFSSL_DIR=../../wolfssl NVM_FLASH_CRC=1 ASAN=1 + # Build and test with DEBUG=1 - name: Build and test NVM tool with DEBUG run: cd tools/whnvmtool && make clean && make check WOLFSSL_DIR=../../wolfssl DEBUG=1 diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index bbcbe0eae..05c7b32e7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -66,6 +66,10 @@ jobs: - name: Build and test DMA ASAN run: cd test && make clean && make -j DMA=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + # Build and test with NVM flash CRC16 integrity checking enabled + - name: Build and test NVM flash CRC ASAN + run: cd test && make clean && make -j NVM_FLASH_CRC=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + # Build and test with LMS and XMSS both in verify-only mode - name: Build and test DMA ASAN LMS/XMSS verify-only run: cd test && make clean && make -j DMA=1 ASAN=1 LMS_VERIFY_ONLY=1 XMSS_VERIFY_ONLY=1 WOLFSSL_DIR=../wolfssl && make run diff --git a/docs/src/5-Features.md b/docs/src/5-Features.md index 3495d3667..0407ea170 100644 --- a/docs/src/5-Features.md +++ b/docs/src/5-Features.md @@ -225,6 +225,8 @@ The access field is used to express coarser-grained permissions (owner / other / The `wh_Nvm_*` API is implemented against a backend callback table (`whNvmCb`) that abstracts the details of how objects are actually laid out on storage. The core library does not depend on any particular backend — selecting a backend is part of server configuration, and ports or applications can supply their own implementations against the same interface. wolfHSM ships with two reference backends, both built on top of the [flash abstraction](#flash-abstraction): - **`nvm_flash`** (`wh_nvm_flash.c`): the default backend, suitable for flash devices with small write granularity (8 bytes or less). It manages two equal-sized partitions in flash, with one designated as active at any time. New objects are added by programming directly into free space at the end of the active partition, which keeps write amplification low for read-heavy and append-dominated workloads. A directory of object state is cached in RAM and rebuilt from flash at initialization. Destruction of objects (and explicit compaction) is performed by regenerating the inactive partition with only the surviving objects, then atomically switching the active partition pointer and erasing the old one. An interruption before the switch leaves the previous partition intact; an interruption after the switch is recovered by completing the erase of the now-inactive partition on the next boot. + + With `WOLFHSM_CFG_NVM_FLASH_CRC16` defined, `nvm_flash` additionally stores a CRC16 (CRC-16/CCITT-FALSE) of each object's metadata and data in spare bits of the on-flash object state, providing integrity checking against flash corruption. Metadata is verified whenever the directory is rebuilt from flash: an object whose metadata fails its CRC is treated as absent and reclaimable, and the next compaction drops it and frees its slot and data. For an interrupted (uncommitted) write whose metadata fails its CRC, the extent of the partially written data is unknown, so the remainder of the data area is reserved and new writes return `WH_ERROR_NOSPACE` until a compaction reclaims the entry (`wh_Nvm_AddObjectWithReclaim` does this automatically). Object data is verified on full-object reads (offset 0 for the object's full length) and while objects are copied during compaction, returning `WH_ERROR_NOTVERIFIED` on mismatch — a failed compaction copy aborts the reclaim with the active partition intact. Reads of a partial byte range are *not* verified, which includes client reads issued at a nonzero offset or chunked through a communication buffer smaller than the object; server-local consumers (keystore, certificate manager, image manager) read whole objects and are always verified. Two caveats: if an object was overwritten and the newest copy's metadata is corrupted before the duplicate is compacted away, the previous version becomes visible again until the next compaction; and enabling the option changes the on-flash format, so images written with and without it are mutually incompatible (existing images must be re-provisioned, and `whnvmtool` must be built with the same setting). - **`nvm_flash_log`** (`wh_nvm_flash_log.c`): an alternative backend designed for flash devices with **large write granularity** (e.g. 64 bytes) where every program operation must be aligned and padded to that boundary. It also uses a two-partition layout, but caches the entire active partition in RAM and rewrites the whole inactive partition on every mutation. Each partition header carries a monotonic epoch counter, and the partition with the highest epoch is treated as authoritative on the next initialization. The implementation favors simplicity and a uniform write pattern at the cost of higher write amplification, which is acceptable on the read-heavy workloads it is intended for. Selected at build time via `WOLFHSM_CFG_SERVER_NVM_FLASH_LOG`. Both backends bind to a `whFlashCb` flash driver supplied by the port; the choice between them is a function of the underlying flash device's program granularity and the application's write profile, not of any user-facing feature. Ports targeting microcontrollers with conventional NOR flash typically use `nvm_flash`; ports targeting devices whose program operation is fundamentally a 32- or 64-byte page write are better served by `nvm_flash_log`. diff --git a/docs/src/6-Utilities.md b/docs/src/6-Utilities.md index 42eb2368f..21e6c8823 100644 --- a/docs/src/6-Utilities.md +++ b/docs/src/6-Utilities.md @@ -18,7 +18,7 @@ This chapter describes the auxiliary tools that ship alongside the wolfHSM clien The NVM provisioning tool (`tools/whnvmtool/`) is a host-side utility that builds a pre-populated wolfHSM NVM image from a configuration file. It is intended for device provisioning: rather than having the server populate its NVM at runtime, the integrator describes the desired initial contents — a set of NVM objects and keys, each with its metadata ID, access permissions, flags, label, and a path to the binary payload — and the tool produces a single image file that can be programmed into the device's flash at manufacture or used in place to back a `whNvmFlash` provider in simulation. Currently the tool targets the `whNvmFlash` provider; the generated image is binary, and can be converted to Intel HEX with the standard `objcopy` workflow for use with automated programmers. -Because the on-flash layout depends on build-time configuration, the tool must be compiled against the same wolfHSM version as the target server and with a matching `WOLFHSM_CFG_NVM_OBJECT_COUNT`, and the `--size` argument must match the server's `whNvmFlash` partition size. For the full configuration file schema, command-line options, hex conversion recipe, and test workflow, see [`tools/whnvmtool/README.md`](https://github.com/wolfSSL/wolfHSM/blob/main/tools/whnvmtool/README.md). +Because the on-flash layout depends on build-time configuration, the tool must be compiled against the same wolfHSM version as the target server and with a matching `WOLFHSM_CFG_NVM_OBJECT_COUNT` and `WOLFHSM_CFG_NVM_FLASH_CRC16` setting, and the `--size` argument must match the server's `whNvmFlash` partition size. For the full configuration file schema, command-line options, hex conversion recipe, and test workflow, see [`tools/whnvmtool/README.md`](https://github.com/wolfSSL/wolfHSM/blob/main/tools/whnvmtool/README.md). ## Benchmark Suite diff --git a/docs/src/9-Configuration.md b/docs/src/9-Configuration.md index 1952e2532..01760f2ac 100644 --- a/docs/src/9-Configuration.md +++ b/docs/src/9-Configuration.md @@ -98,6 +98,7 @@ These macros size the server-side key cache. The cache is split into "regular" s | Macro | Default | Description | |---|---|---| | `WOLFHSM_CFG_NVM_OBJECT_COUNT` | `32` | Maximum number of objects the NVM directory can hold simultaneously (RAM directory cache *and* the on-disk directory it mirrors). Determines the upper bound on the number of keys, certificates, counters, and user objects that can coexist in NVM at one time. | +| `WOLFHSM_CFG_NVM_FLASH_CRC16` | Undefined | If defined, the `nvm_flash` backend stores a CRC16 of each object's metadata and data in the on-flash object state and verifies them: metadata when the directory is loaded (failing objects become invisible and reclaimable), data on full-object reads and reclaim copies (returning `WH_ERROR_NOTVERIFIED` on mismatch). Partial reads are not verified. Changes the on-flash format: images written with and without this option are mutually incompatible, and `whnvmtool` must be built with the same setting as the server. | | `WOLFHSM_CFG_SERVER_NVM_FLASH_LOG` | Undefined | If defined, compile the log-structured NVM flash backend (`wh_nvm_flash_log`). When enabled it can be selected at runtime as an alternative to the regular flash backend; useful for flash parts that tolerate fewer erases or that prefer append-only update patterns. | ## Certificate Manager diff --git a/src/wh_nvm_flash.c b/src/wh_nvm_flash.c index 3447f3cb8..eb394c4aa 100644 --- a/src/wh_nvm_flash.c +++ b/src/wh_nvm_flash.c @@ -35,6 +35,7 @@ #include "wolfhsm/wh_flash.h" #include "wolfhsm/wh_flash_unit.h" #include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_utils.h" #include "wolfhsm/wh_nvm_flash.h" @@ -47,6 +48,19 @@ enum { * with erased flash */ static const whFlashUnit BASE_STATE = 0x1234567800000000ULL; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 +/* With CRC16 enabled, the object start and count state words carry a CRC in + * bits [47:32], replacing the low half of the magic: + * start word: [63:48]=0x1234 [47:32]=CRC16(metadata) [31:0]=start + * count word: [63:48]=0x1234 [47:32]=CRC16(data) [31:0]=count + * The epoch word and all partition state words keep the full BASE_STATE + * magic. The remaining 0x12/0x34 bytes still keep every state word distinct + * from erased flash. */ +static const whFlashUnit CRC_BASE_STATE = 0x1234000000000000ULL; +#define NF_STATE_CRC_PACK(_crc) (((whFlashUnit)(_crc)) << 32) +#define NF_STATE_CRC_EXTRACT(_unit) ((uint16_t)(((_unit) >> 32) & 0xFFFFULL)) +#endif + /* On-flash layout of the state of an Object or Directory*/ typedef struct { whFlashUnit epoch; /* Not Erased: counter */ @@ -124,23 +138,30 @@ static int nfPartition_CheckDataRange(whNvmFlashContext* context, static int nfObject_Offset(whNvmFlashContext* context, int partition, int object_index, uint32_t *out_object_offset); static int nfObject_ProgramBegin(whNvmFlashContext* context, int partition, - int object_index, uint32_t epoch, uint32_t start, whNvmMetadata* meta); + int object_index, uint32_t epoch, + uint32_t start, whNvmMetadata* meta, + uint16_t crc_meta); static int nfObject_ProgramDataBytes(whNvmFlashContext* context, int partition, uint32_t offset, uint32_t byte_count, const uint8_t* data); static int nfObject_ProgramFinish(whNvmFlashContext* context, int partition, - int object_index, uint32_t byte_count); + int object_index, uint32_t byte_count, + uint16_t crc_data); static int nfObject_Program(whNvmFlashContext* context, int partition, - int object_index, uint32_t epoch, whNvmMetadata* meta, uint32_t start, - const uint8_t* data); + int object_index, uint32_t epoch, + whNvmMetadata* meta, uint32_t start, + const uint8_t* data, uint16_t crc_meta, + uint16_t crc_data); static int nfObject_ReadDataBytes(whNvmFlashContext* context, int partition, int object_index, uint32_t byte_offset, uint32_t byte_count, uint8_t* out_data); static int nfObject_Copy(whNvmFlashContext* context, int object_index, int partition, uint32_t *inout_next_object, uint32_t *inout_next_data); -static int nfMemDirectory_Parse(nfMemDirectory* d); +static int nfMemDirectory_Parse(whNvmFlashContext* context, nfMemDirectory* d); static int nfMemDirectory_FindObjectIndexById(nfMemDirectory* d, whNvmId id, int *out_object_index); +static int nfIdList_Contains(whNvmId list_count, const whNvmId* id_list, + whNvmId id); static int nfMemState_Read(whNvmFlashContext* context, uint32_t offset, @@ -203,11 +224,29 @@ static int nfMemState_Read(whNvmFlashContext* context, uint32_t offset, state->epoch = buffer.epoch; state->start = buffer.start; state->count = buffer.count; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + state->crc_meta = NF_STATE_CRC_EXTRACT(buffer.start); + state->crc_data = NF_STATE_CRC_EXTRACT(buffer.count); +#endif /* Used */ state->status = NF_STATUS_USED; } else if ( (blank_epoch == WH_ERROR_NOTBLANK) && (blank_start == WH_ERROR_NOTBLANK)){ + /* Count is blank. Recover epoch and start so the directory can + * account for this entry's reserved data area */ + ret = wh_FlashUnit_Read(context->cb, context->flash, offset, 2, + (whFlashUnit*)&buffer); + if (ret != 0) { + /* Error reading state*/ + return ret; + } + + state->epoch = buffer.epoch; + state->start = buffer.start; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + state->crc_meta = NF_STATE_CRC_EXTRACT(buffer.start); +#endif state->status = NF_STATUS_DATA_BAD; } else if (blank_epoch == WH_ERROR_NOTBLANK) { state->status = NF_STATUS_META_BAD; @@ -247,6 +286,24 @@ static int nfMemObject_Read(whNvmFlashContext* context, /* Copy the metadata out of the buffer */ memcpy(&object->metadata, buffer, sizeof(object->metadata)); clear_metadata = 0; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + /* Verify the metadata against the CRC in the start state word */ + if (((object->state.status == NF_STATUS_USED) || + (object->state.status == NF_STATUS_DATA_BAD)) && + (wh_Utils_Crc16(WH_UTILS_CRC16_INIT, &object->metadata, + sizeof(object->metadata)) != + object->state.crc_meta)) { + if (object->state.status == NF_STATUS_USED) { + object->state.status = NF_STATUS_CRC_BAD; + } + else { + /* Interrupted entry: without trusted metadata the extent + * of its partially written data is unknown */ + object->state.status = NF_STATUS_LEN_BAD; + } + clear_metadata = 1; + } +#endif } } if (clear_metadata != 0){ @@ -377,7 +434,7 @@ static int nfPartition_ReadParseMemDirectory(whNvmFlashContext* context, int par if (ret != 0) { return ret; } - return nfMemDirectory_Parse(directory); + return nfMemDirectory_Parse(context, directory); } static int nfPartition_ProgramEpoch(whNvmFlashContext* context, @@ -524,8 +581,9 @@ static int nfObject_Offset(whNvmFlashContext* context, int partition, } static int nfObject_ProgramBegin(whNvmFlashContext* context, int partition, - int object_index, uint32_t epoch, uint32_t start, - whNvmMetadata* meta) + int object_index, uint32_t epoch, + uint32_t start, whNvmMetadata* meta, + uint16_t crc_meta) { int rc = 0; uint32_t object_offset = 0; @@ -538,6 +596,13 @@ static int nfObject_ProgramBegin(whNvmFlashContext* context, int partition, return WH_ERROR_BADARGS; } +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + /* Start word carries the metadata CRC in place of the low magic half */ + state_start = CRC_BASE_STATE | NF_STATE_CRC_PACK(crc_meta) | start; +#else + (void)crc_meta; +#endif + rc = nfObject_Offset(context, partition, object_index, &object_offset); if (rc != WH_ERROR_OK) { return rc; @@ -601,7 +666,8 @@ static int nfObject_ProgramDataBytes(whNvmFlashContext* context, int partition, } static int nfObject_ProgramFinish(whNvmFlashContext* context, int partition, - int object_index, uint32_t byte_count) + int object_index, uint32_t byte_count, + uint16_t crc_data) { int rc; uint32_t object_offset = 0; @@ -611,6 +677,14 @@ static int nfObject_ProgramFinish(whNvmFlashContext* context, int partition, return WH_ERROR_BADARGS; } +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + /* Count word carries the data CRC in place of the low magic half */ + state_count = CRC_BASE_STATE | NF_STATE_CRC_PACK(crc_data) | + WHFU_BYTES2UNITS(byte_count); +#else + (void)crc_data; +#endif + rc = nfObject_Offset(context, partition, object_index, &object_offset); if (rc != WH_ERROR_OK) { return rc; @@ -626,9 +700,10 @@ static int nfObject_ProgramFinish(whNvmFlashContext* context, int partition, } static int nfObject_Program(whNvmFlashContext* context, int partition, - int object_index, uint32_t epoch, - whNvmMetadata* meta, - uint32_t start, const uint8_t* data) + int object_index, uint32_t epoch, + whNvmMetadata* meta, uint32_t start, + const uint8_t* data, uint16_t crc_meta, + uint16_t crc_data) { int rc = 0; @@ -636,8 +711,8 @@ static int nfObject_Program(whNvmFlashContext* context, int partition, return WH_ERROR_BADARGS; } - rc = nfObject_ProgramBegin(context, partition, object_index, - epoch, start, meta); + rc = nfObject_ProgramBegin(context, partition, object_index, epoch, start, + meta, crc_meta); if (rc == 0) { /* allow metadata only entries for things like counters */ if (data != NULL) { @@ -646,7 +721,7 @@ static int nfObject_Program(whNvmFlashContext* context, int partition, } if (rc == 0) { rc = nfObject_ProgramFinish(context, partition, object_index, - meta->len); + meta->len, crc_data); } } return rc; @@ -656,6 +731,7 @@ static int nfObject_ReadDataBytes(whNvmFlashContext* context, int partition, int object_index, uint32_t byte_offset, uint32_t byte_count, uint8_t* out_data) { + int ret = 0; int start = 0; uint32_t startOffset = 0; @@ -685,9 +761,25 @@ static int nfObject_ReadDataBytes(whNvmFlashContext* context, int partition, return WH_ERROR_BADARGS; } - return wh_FlashUnit_ReadBytes( + ret = wh_FlashUnit_ReadBytes( context->cb, context->flash, startOffset * WHFU_BYTES_PER_UNIT + byte_offset, byte_count, out_data); + +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + /* Full-object reads are verified against the stored data CRC. Partial + * reads cannot be verified */ + if ((ret == 0) && + (context->directory.objects[object_index].state.status == + NF_STATUS_USED) && + (byte_offset == 0) && + (byte_count == context->directory.objects[object_index].metadata.len)) { + if (wh_Utils_Crc16(WH_UTILS_CRC16_INIT, out_data, byte_count) != + context->directory.objects[object_index].state.crc_data) { + ret = WH_ERROR_NOTVERIFIED; + } + } +#endif + return ret; } static int nfObject_Copy(whNvmFlashContext* context, int object_index, @@ -699,6 +791,11 @@ static int nfObject_Copy(whNvmFlashContext* context, int object_index, nfMemDirectory* d = NULL; uint32_t data_len = 0; uint32_t data_offset = 0; + uint16_t crc_meta = 0; + uint16_t crc_data = 0; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + uint16_t crc_calc = WH_UTILS_CRC16_INIT; +#endif if ( (context == NULL) || (inout_next_object == NULL) || @@ -712,10 +809,23 @@ static int nfObject_Copy(whNvmFlashContext* context, int object_index, data_len = d->objects[object_index].metadata.len; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + /* Carry the source CRCs forward rather than recomputing, so corruption + * of the cached metadata since load is not re-blessed with a fresh CRC */ + crc_meta = d->objects[object_index].state.crc_meta; + crc_data = d->objects[object_index].state.crc_data; + + /* Verify the cached metadata before programming it */ + if (wh_Utils_Crc16(WH_UTILS_CRC16_INIT, &d->objects[object_index].metadata, + sizeof(d->objects[object_index].metadata)) != crc_meta) { + return WH_ERROR_NOTVERIFIED; + } +#endif + /* Copy the object to the new partition */ ret = nfObject_ProgramBegin(context, partition, dest_object, - d->objects[object_index].state.epoch, - dest_data, &d->objects[object_index].metadata); + d->objects[object_index].state.epoch, dest_data, + &d->objects[object_index].metadata, crc_meta); if (ret != 0) return ret; /* Loop through reading the old data into buffer */ @@ -732,6 +842,10 @@ static int nfObject_Copy(whNvmFlashContext* context, int object_index, data_offset, this_len, buffer); if (ret != 0) return ret; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + crc_calc = wh_Utils_Crc16(crc_calc, buffer, this_len); +#endif + /* Write the data to the new object. */ ret = nfObject_ProgramDataBytes( context, @@ -744,7 +858,16 @@ static int nfObject_Copy(whNvmFlashContext* context, int object_index, data_offset += this_len; dest_data += WHFU_BYTES2UNITS(this_len); } - ret = nfObject_ProgramFinish(context, partition, dest_object, data_len); + +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + /* Verify the data read from the source object */ + if (crc_calc != crc_data) { + return WH_ERROR_NOTVERIFIED; + } +#endif + + ret = nfObject_ProgramFinish(context, partition, dest_object, data_len, + crc_data); if (ret != 0) return ret; dest_object++; @@ -755,13 +878,13 @@ static int nfObject_Copy(whNvmFlashContext* context, int object_index, } -static int nfMemDirectory_Parse(nfMemDirectory* d) +static int nfMemDirectory_Parse(whNvmFlashContext* context, nfMemDirectory* d) { int done = 0; int this_entry = 0; int that_entry = 0; - if (d == NULL) { + if ((context == NULL) || (d == NULL)) { return WH_ERROR_BADARGS; } @@ -797,6 +920,28 @@ static int nfMemDirectory_Parse(nfMemDirectory* d) d->objects[d->next_free_object].state.start + WHFU_BYTES2UNITS(d->objects[d->next_free_object].metadata.len); break; + case NF_STATUS_CRC_BAD: + /* Metadata failed CRC. Its data area is still reserved */ + d->reclaimable_entries++; + d->reclaimable_data += d->objects[d->next_free_object].state.count; + d->next_free_data = d->objects[d->next_free_object].state.start + + d->objects[d->next_free_object].state.count; + break; + case NF_STATUS_LEN_BAD: + /* Interrupted entry with untrusted metadata: the extent of its + * data is unknown, so reserve through the end of the data area. + * New writes get NOSPACE until compaction reclaims the entry. A + * later intact entry re-bounds the reservation with its own start + * and count. Reclaimable size is a best effort */ + d->reclaimable_entries++; + if ((context->partition_units - NF_PARTITION_DATA_OFFSET) > + d->objects[d->next_free_object].state.start) { + d->reclaimable_data += + (context->partition_units - NF_PARTITION_DATA_OFFSET) - + d->objects[d->next_free_object].state.start; + } + d->next_free_data = context->partition_units; + break; default: /* Unknown state. Better barf */ return WH_ERROR_ABORTED; @@ -844,6 +989,19 @@ static int nfMemDirectory_FindObjectIndexById(nfMemDirectory* d, whNvmId id, return ret; } +/* Returns nonzero if id is in the list */ +static int nfIdList_Contains(whNvmId list_count, const whNvmId* id_list, + whNvmId id) +{ + int i = 0; + + for (i = 0; i < (int)list_count; i++) { + if (id_list[i] == id) { + return 1; + } + } + return 0; +} /************* WolfHSM NVM Interfaces ***********/ @@ -921,7 +1079,7 @@ int wh_NvmFlash_Init(void* c, const void* cf) ret = nfPartition_ReadMemDirectory(context, context->active, &context->directory); if (ret == WH_ERROR_OK) { - ret = nfMemDirectory_Parse(&context->directory); + ret = nfMemDirectory_Parse(context, &context->directory); if (ret == WH_ERROR_OK) { context->initialized = 1; } @@ -970,6 +1128,9 @@ int wh_NvmFlash_List(void* c, if (context == NULL) { return WH_ERROR_BADARGS; } + if (context->directory_bad != 0) { + return WH_ERROR_ABORTED; + } d = &context->directory; @@ -1027,11 +1188,22 @@ int wh_NvmFlash_GetAvailable(void* c, if (context == NULL) { return WH_ERROR_BADARGS; } + if (context->directory_bad != 0) { + return WH_ERROR_ABORTED; + } nfMemDirectory *d = &context->directory; if (out_avail_size != NULL) { - *out_avail_size = (context->partition_units - - NF_PARTITION_DATA_OFFSET - d->next_free_data) * - WHFU_BYTES_PER_UNIT; + uint32_t data_units = + context->partition_units - NF_PARTITION_DATA_OFFSET; + /* next_free_data can exceed the data area when an entry with + * untrusted metadata reserves through the end of it */ + if (d->next_free_data < data_units) { + *out_avail_size = + (data_units - d->next_free_data) * WHFU_BYTES_PER_UNIT; + } + else { + *out_avail_size = 0; + } } if (out_avail_objects != NULL) { *out_avail_objects = WOLFHSM_CFG_NVM_OBJECT_COUNT - d->next_free_object; @@ -1054,6 +1226,9 @@ int wh_NvmFlash_GetMetadata(void* c, whNvmId id, whNvmMetadata* meta) if (context == NULL) { return WH_ERROR_BADARGS; } + if (context->directory_bad != 0) { + return WH_ERROR_ABORTED; + } ret = nfMemDirectory_FindObjectIndexById(&context->directory, id, &entry); if (ret == 0) { @@ -1072,18 +1247,23 @@ int wh_NvmFlash_GetMetadata(void* c, whNvmId id, whNvmMetadata* meta) int wh_NvmFlash_AddObject(void* c, whNvmMetadata *meta, whNvmSize data_len, const uint8_t* data) { - whNvmFlashContext* context = c; - nfMemDirectory* d = NULL; - int oldentry = -1; - int ret = 0; - uint32_t epoch = 0; - uint32_t count = 0; + whNvmFlashContext* context = c; + nfMemDirectory* d = NULL; + int oldentry = -1; + int ret = 0; + uint32_t epoch = 0; + uint32_t count = 0; + uint16_t crc_meta = 0; + uint16_t crc_data = 0; if ( (context == NULL) || (meta == NULL) || ((data_len > 0) && (data == NULL)) ) { return WH_ERROR_BADARGS; } + if (context->directory_bad != 0) { + return WH_ERROR_ABORTED; + } d = &context->directory; if ( (d->next_free_object == WOLFHSM_CFG_NVM_OBJECT_COUNT) || @@ -1102,13 +1282,13 @@ int wh_NvmFlash_AddObject(void* c, whNvmMetadata *meta, meta->len = data_len; count = WHFU_BYTES2UNITS(meta->len); - ret = nfObject_Program(context, - context->active, - d->next_free_object, - epoch, - meta, - d->next_free_data, - data); +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + crc_meta = wh_Utils_Crc16(WH_UTILS_CRC16_INIT, meta, sizeof(*meta)); + crc_data = wh_Utils_Crc16(WH_UTILS_CRC16_INIT, data, data_len); +#endif + + ret = nfObject_Program(context, context->active, d->next_free_object, epoch, + meta, d->next_free_data, data, crc_meta, crc_data); if (ret == 0) { /* Update directory with new object */ @@ -1116,6 +1296,10 @@ int wh_NvmFlash_AddObject(void* c, whNvmMetadata *meta, d->objects[d->next_free_object].state.epoch = epoch; d->objects[d->next_free_object].state.start = d->next_free_data; d->objects[d->next_free_object].state.count = count; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + d->objects[d->next_free_object].state.crc_meta = crc_meta; + d->objects[d->next_free_object].state.crc_data = crc_data; +#endif memcpy(&d->objects[d->next_free_object].metadata, meta, sizeof(*meta)); d->next_free_data += count; d->next_free_object++; @@ -1140,12 +1324,11 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count, int ret = 0; whNvmFlashContext* context = c; nfMemDirectory* d = NULL; - nfMemState new_state = {0}; - int list_entry = 0; + nfMemState new_state = {0}; int entry = 0; int src_part = 0; int dest_part = 0; - int any_marked = 0; + int any_matched = 0; uint32_t dest_object = 0; uint32_t dest_data = 0; @@ -1153,6 +1336,9 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count, ((list_count > 0) && (id_list == NULL)) ) { return WH_ERROR_BADARGS; } + if (context->directory_bad != 0) { + return WH_ERROR_ABORTED; + } /* Context is valid. Generate helper values */ d = &context->directory; @@ -1165,24 +1351,22 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count, .count = context->state.count, }; - /* Go through the current directory and mark the listed id's as bad */ - for (list_entry = 0; list_entry < list_count; list_entry++) { - /* Mark all matching entries as bad. Should only be 1. */ - do { - entry = -1; - ret = nfMemDirectory_FindObjectIndexById(d, id_list[list_entry], - &entry); - if ((ret == 0) && (entry >= 0)) { - d->objects[entry].state.status = NF_STATUS_DATA_BAD; - any_marked = 1; - } - } while (entry >= 0); + /* Check whether any current object is on the list. The directory is not + * modified until replication succeeds, so an aborted replication leaves + * it matching the still-active partition */ + for (entry = 0; entry < WOLFHSM_CFG_NVM_OBJECT_COUNT; entry++) { + if ((d->objects[entry].state.status == NF_STATUS_USED) && + (nfIdList_Contains(list_count, id_list, + d->objects[entry].metadata.id) != 0)) { + any_matched = 1; + break; + } } /* Nothing matched a non-empty list: replicating would just rewrite the * partition unchanged, so skip the flash wear. A zero list_count is a * compaction request and must still replicate. */ - if ((list_count > 0) && (any_marked == 0)) { + if ((list_count > 0) && (any_matched == 0)) { return WH_ERROR_OK; } @@ -1191,37 +1375,41 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count, if (ret == WH_ERROR_NOTBLANK) { ret = nfPartition_Erase(context, dest_part); } - if (ret != 0) { - return ret; - } - ret = nfPartition_ProgramEpoch(context, dest_part, new_state.epoch); - if (ret != 0) { - return ret; + if (ret == 0) { + ret = nfPartition_ProgramEpoch(context, dest_part, new_state.epoch); } /* Write partition start */ - ret = nfPartition_ProgramStart(context, dest_part, new_state.start); - if (ret != 0) { - return ret; + if (ret == 0) { + ret = nfPartition_ProgramStart(context, dest_part, new_state.start); } - /* Write each used object to new partition */ - for (entry = 0; entry < WOLFHSM_CFG_NVM_OBJECT_COUNT; entry++) { - if (d->objects[entry].state.status == NF_STATUS_USED) { - ret = nfObject_Copy(context, entry, - dest_part, &dest_object, &dest_data); - if (ret != WH_ERROR_OK) { - /* Abort reclaim to avoid activating a partially copied - * partition */ - return ret; + /* Write each used object that is not on the list to new partition */ + if (ret == 0) { + for (entry = 0; entry < WOLFHSM_CFG_NVM_OBJECT_COUNT; entry++) { + if ((d->objects[entry].state.status == NF_STATUS_USED) && + (nfIdList_Contains(list_count, id_list, + d->objects[entry].metadata.id) == 0)) { + ret = nfObject_Copy(context, entry, dest_part, &dest_object, + &dest_data); + if (ret != WH_ERROR_OK) { + /* Abort reclaim to avoid activating a partially copied + * partition */ + break; + } } } } /* Write partition count */ - ret = nfPartition_ProgramCount(context, dest_part, new_state.count); + if (ret == 0) { + ret = nfPartition_ProgramCount(context, dest_part, new_state.count); + } + if (ret != 0) { + /* Replication failed. The directory was not modified and still + * matches the active partition */ return ret; } @@ -1230,8 +1418,12 @@ int wh_NvmFlash_DestroyObjects(void* c, whNvmId list_count, dest_part, &context->directory); if (ret != 0) { /* Failed to reread the directory. Read the previous one instead */ - (void)nfPartition_ReadParseMemDirectory(context, - src_part, &context->directory); + if (nfPartition_ReadParseMemDirectory(context, src_part, + &context->directory) != 0) { + /* Directory no longer matches flash. Refuse further use until + * the context is reinitialized */ + context->directory_bad = 1; + } return ret; } @@ -1258,6 +1450,9 @@ int wh_NvmFlash_Read(void* c, whNvmId id, whNvmSize offset, whNvmSize data_len, ((data_len > 0) && (data == NULL)) ){ return WH_ERROR_BADARGS; } + if (context->directory_bad != 0) { + return WH_ERROR_ABORTED; + } ret = nfMemDirectory_FindObjectIndexById(&context->directory, id, &object_index); diff --git a/src/wh_utils.c b/src/wh_utils.c index 3272c8e72..634204118 100644 --- a/src/wh_utils.c +++ b/src/wh_utils.c @@ -79,6 +79,30 @@ uint32_t wh_Utils_ntohl(uint32_t networklong) { return wh_Utils_htonl(networklong); } +/** CRC16 functions */ +uint16_t wh_Utils_Crc16(uint16_t crc, const void* data, size_t len) +{ + const uint8_t* p = (const uint8_t*)data; + size_t i; + int bit; + + if (p == NULL) { + return crc; + } + + for (i = 0; i < len; i++) { + crc = (uint16_t)(crc ^ ((uint16_t)p[i] << 8)); + for (bit = 0; bit < 8; bit++) { + if ((crc & 0x8000U) != 0U) { + crc = (uint16_t)((uint16_t)(crc << 1) ^ 0x1021U); + } + else { + crc = (uint16_t)(crc << 1); + } + } + } + return crc; +} int wh_Utils_memeqzero(uint8_t* buffer, uint32_t size) diff --git a/test-refactor/README.md b/test-refactor/README.md index d7811ce39..08f87502e 100644 --- a/test-refactor/README.md +++ b/test-refactor/README.md @@ -94,7 +94,7 @@ Translated tests: | `wh_test_clientserver.c::_testClientCounter` | `client-server/wh_test_counter.c::whTest_Counter` | Client | exercises saturate-on-overflow and slot-leak detection | | `wh_test_wolfcrypt_test.c::whTest_WolfCryptTest` | `client-server/wh_test_wolfcrypt.c::whTest_WolfCryptTest` | Client | | | `wh_test_flash_ramsim.c::whTest_Flash_RamSim` | `posix/wh_test_flash_ramsim.c::{whTest_FlashWriteLock, whTest_FlashEraseProgramVerify, whTest_FlashUnitOps}` | POSIX port-specific (`whTestGroup_RunOne`) | remove ramsim coupling and migrate to server group | -| `wh_test_nvm_flash.c::{whTest_NvmFlash, whTest_NvmFlash_Recovery}` | `posix/wh_test_nvm_flash.c::{whTest_NvmAddOverwriteDestroy, whTest_NvmFlashLog, whTest_NvmRecovery}` | POSIX port-specific (`whTestGroup_RunOne`) | remove ramsim coupling and migrate to server group; flash-log backend exercised by `whTest_NvmFlashLog` (skipped unless `WOLFHSM_CFG_SERVER_NVM_FLASH_LOG`) | +| `wh_test_nvm_flash.c::{whTest_NvmFlash, whTest_NvmFlash_Recovery, whTest_NvmFlash_Crc16}` | `posix/wh_test_nvm_flash.c::{whTest_NvmAddOverwriteDestroy, whTest_NvmFlashLog, whTest_NvmRecovery, whTest_NvmCrc16}` | POSIX port-specific (`whTestGroup_RunOne`) | remove ramsim coupling and migrate to server group; flash-log backend exercised by `whTest_NvmFlashLog` (skipped unless `WOLFHSM_CFG_SERVER_NVM_FLASH_LOG`); CRC16 integrity checks exercised by `whTest_NvmCrc16` (skipped unless built with `NVM_FLASH_CRC=1`) | | `wh_test_flash_fault_inject.c` | `posix/wh_test_flash_fault_inject.c` | helper (no test) | fault-injection flash wrapper used by the recovery test | | `wh_test_posix_threadsafe_stress.c::whTest_ThreadSafeStress` | called directly from `posix/wh_test_posix_main.c` | POSIX port-specific (direct call) | | | `wh_test_check_struct_padding.c` | `misc/wh_test_check_struct_padding.c` | Build-time (compile-only) | Wire-format `-Wpadded` audit; the POSIX Makefile compiles it with `-Wpadded -DWH_PADDING_CHECK`. Not a runtime test, so not registered in `wh_test_list.c` | diff --git a/test-refactor/posix/Makefile b/test-refactor/posix/Makefile index 89673960d..b459c4cdf 100644 --- a/test-refactor/posix/Makefile +++ b/test-refactor/posix/Makefile @@ -112,6 +112,11 @@ ifeq ($(DMA),1) DEF += -DWOLFHSM_CFG_DMA endif +# Enable CRC16 integrity checking in the NVM flash backend +ifeq ($(NVM_FLASH_CRC),1) + DEF += -DWOLFHSM_CFG_NVM_FLASH_CRC16 +endif + # Build LMS/XMSS in verify-only mode (omits private-key, sign, and keygen # paths). May be combined to exercise the mixed (one verify-only) case. ifeq ($(LMS_VERIFY_ONLY),1) diff --git a/test-refactor/posix/wh_test_nvm_flash.c b/test-refactor/posix/wh_test_nvm_flash.c index 5415dad12..2ec414cb1 100644 --- a/test-refactor/posix/wh_test_nvm_flash.c +++ b/test-refactor/posix/wh_test_nvm_flash.c @@ -35,6 +35,7 @@ #include "wolfhsm/wh_flash_unit.h" #include "wolfhsm/wh_nvm.h" #include "wolfhsm/wh_nvm_flash.h" +#include "wolfhsm/wh_utils.h" #include "wh_test_common.h" #include "wh_test_list.h" @@ -330,9 +331,9 @@ int whTest_NvmFlashLog(void* ctx) /* ---- NVM recovery ---- */ /* - * Two flash images for the recovery test: one live backing store - * and one snapshot replayed as init data to model a reboot over a - * dirty flash. File-static to keep 2 MB off the stack. + * Two flash images shared by the recovery and CRC tests: one live + * backing store and one snapshot replayed as init data to model a + * reboot over a dirty flash. File-static to keep 2 MB off the stack. */ static uint8_t _recoveryMemory[NVM_FLASH_SIZE]; static uint8_t _recoveryBackup[NVM_FLASH_SIZE]; @@ -408,11 +409,118 @@ static int _simulateFailureAndRecover( } +/* + * Interrupt an add while a committed object is already in the partition, + * then verify the reloaded directory accounts for both data regions and + * places new objects after them. + */ +static int _simulateFailureWithPrecedingObject(void) +{ + const whFlashCb flashCb[1] = {WH_FLASH_RAMSIM_CB}; + whFlashRamsimCtx flashCtx[1] = {0}; + whFlashRamsimCfg flashCfg[1] = {{ + .size = NVM_FLASH_SIZE, + .sectorSize = NVM_FLASH_SECTOR_SZ, + .pageSize = NVM_FLASH_PAGE_SZ, + .erasedByte = (uint8_t)0, + .memory = _recoveryMemory, + }}; + const whFlashCb flashFaultInjCb[1] = {WH_FLASH_FAULTINJECT_CB}; + whFlashFaultInjectCtx faultInjCtx[1] = {0}; + whFlashFaultInjectCfg faultInjCfg[1] = {{ + .realCb = flashCb, + .realCtx = flashCtx, + .realCfg = flashCfg, + }}; + const whNvmCb cb[1] = {WH_NVM_FLASH_CB}; + whNvmFlashContext context[1] = {0}; + whNvmFlashConfig cfg = { + .cb = flashFaultInjCb, + .context = faultInjCtx, + .config = faultInjCfg, + }; + whNvmMetadata firstMeta = {.id = 50, .label = "RecoveryFirst"}; + whNvmMetadata intrMeta = {.id = 51, .label = "RecoveryIntr"}; + whNvmMetadata postMeta = {.id = 52, .label = "RecoveryPost"}; + whNvmMetadata checkMeta = {0}; + uint8_t firstData[64]; + uint8_t intrData[40]; + uint8_t postData[40]; + uint8_t readBuf[64]; + uint32_t availStart = 0; + uint32_t availAfter = 0; + uint32_t reclaimAfter = 0; + whNvmId objsStart = 0; + whNvmId objsAfter = 0; + whNvmId objsReclAfter = 0; + uint32_t i = 0; + + for (i = 0; i < sizeof(firstData); i++) { + firstData[i] = (uint8_t)(0x11 ^ (i * 7)); + } + for (i = 0; i < sizeof(intrData); i++) { + intrData[i] = (uint8_t)(0x22 ^ (i * 3)); + postData[i] = (uint8_t)(0x33 ^ (i * 5)); + } + + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL( + cb->GetAvailable(context, &availStart, &objsStart, NULL, NULL)); + + /* Commit one object so the interrupted entry's data starts at a nonzero + * offset */ + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &firstMeta, (whNvmSize)sizeof(firstData), firstData)); + + /* Interrupt the next add at the count-word program (5th program: epoch, + * metadata, start, data, count) */ + faultInjCtx->failAfterPrograms = 5; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == + cb->AddObject(context, &intrMeta, + (whNvmSize)sizeof(intrData), intrData)); + + /* Reboot onto the dirty flash */ + memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(_recoveryMemory, 0, NVM_FLASH_SIZE); + flashCfg->initData = _recoveryBackup; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + + /* Committed object is intact, interrupted one is hidden */ + WH_TEST_RETURN_ON_FAIL(cb->Read(context, firstMeta.id, 0, + (whNvmSize)sizeof(firstData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(firstData, readBuf, sizeof(firstData))); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, intrMeta.id, &checkMeta)); + + /* Both data regions are accounted: the committed object as used, the + * interrupted one as reserved and reclaimable */ + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availAfter, &objsAfter, + &reclaimAfter, &objsReclAfter)); + WH_TEST_ASSERT_RETURN(availAfter == + availStart - sizeof(firstData) - sizeof(intrData)); + WH_TEST_ASSERT_RETURN(objsAfter == objsStart - 2); + WH_TEST_ASSERT_RETURN(reclaimAfter == sizeof(intrData)); + WH_TEST_ASSERT_RETURN(objsReclAfter == 1); + + /* A new add lands after both regions instead of on top of them */ + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &postMeta, (whNvmSize)sizeof(postData), postData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, postMeta.id, 0, + (whNvmSize)sizeof(postData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(postData, readBuf, sizeof(postData))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + return 0; +} + + /* * Recover from a program failure at two points: writing the object * start (the metadata/start record only) and writing the object * count (after the data is on flash). Each scenario checks the * partial object is reclaimed and the live counts are consistent. + * Also repeats the count-word interruption with a committed object + * already in the partition, pinning the recovered start offset. */ int whTest_NvmRecovery(void* ctx) { @@ -452,5 +560,486 @@ int whTest_NvmRecovery(void* ctx) /* available object should be decremented */ WH_TEST_ASSERT_RETURN(objsAfter == objsBefore - 1); + WH_TEST_PRINT("--simulate failure after a committed object\n"); + WH_TEST_RETURN_ON_FAIL(_simulateFailureWithPrecedingObject()); + + return 0; +} + + +/* ---- NVM CRC16 integrity ---- */ + +#if defined(WOLFHSM_CFG_NVM_FLASH_CRC16) + +/* Find needle in haystack. Returns byte offset or -1 if not found */ +static int _findFlashPattern(const uint8_t* haystack, uint32_t hay_len, + const uint8_t* needle, uint32_t needle_len) +{ + uint32_t i; + + if ((needle_len == 0) || (needle_len > hay_len)) { + return -1; + } + for (i = 0; i <= hay_len - needle_len; i++) { + if (memcmp(&haystack[i], needle, needle_len) == 0) { + return (int)i; + } + } + return -1; +} + +static int _crc16Vectors(void) +{ + const char* check = "123456789"; + uint16_t crc; + uint16_t crc_split; + + /* Known CRC-16/CCITT-FALSE check value */ + crc = wh_Utils_Crc16(WH_UTILS_CRC16_INIT, check, 9); + WH_TEST_ASSERT_RETURN(crc == 0x29B1); + + /* Incremental computation matches one-shot */ + crc_split = wh_Utils_Crc16(WH_UTILS_CRC16_INIT, check, 4); + crc_split = wh_Utils_Crc16(crc_split, check + 4, 5); + WH_TEST_ASSERT_RETURN(crc_split == crc); + + /* Zero length returns the seed */ + WH_TEST_ASSERT_RETURN(wh_Utils_Crc16(WH_UTILS_CRC16_INIT, check, 0) == + WH_UTILS_CRC16_INIT); + WH_TEST_ASSERT_RETURN(wh_Utils_Crc16(WH_UTILS_CRC16_INIT, NULL, 0) == + WH_UTILS_CRC16_INIT); + /* NULL data is treated as zero length regardless of len */ + WH_TEST_ASSERT_RETURN(wh_Utils_Crc16(WH_UTILS_CRC16_INIT, NULL, 5) == + WH_UTILS_CRC16_INIT); return 0; } +#endif /* WOLFHSM_CFG_NVM_FLASH_CRC16 */ + + +/* + * CRC16 integrity checks in the NVM flash backend: corrupted object + * data is caught on full reads and reclaim copies, corrupted metadata + * is caught when the directory is loaded, and metadata-only objects + * keep working. Also checks a failed destroy leaves the cached + * directory matching flash, an interrupted write with corrupt + * metadata blocks new writes until compaction reclaims it, and a + * corrupt overwrite resurrects the previous version. Skipped unless + * built with WOLFHSM_CFG_NVM_FLASH_CRC16. + */ +int whTest_NvmCrc16(void* ctx) +{ + (void)ctx; +#if defined(WOLFHSM_CFG_NVM_FLASH_CRC16) + { + const whFlashCb flashCb[1] = {WH_FLASH_RAMSIM_CB}; + whFlashRamsimCtx flashCtx[1] = {0}; + whFlashRamsimCfg flashCfg[1] = {{ + .size = NVM_FLASH_SIZE, + .sectorSize = NVM_FLASH_SECTOR_SZ, + .pageSize = NVM_FLASH_PAGE_SZ, + .erasedByte = (uint8_t)0, + .memory = _recoveryMemory, + }}; + const whNvmCb cb[1] = {WH_NVM_FLASH_CB}; + whNvmFlashContext context[1] = {0}; + whNvmFlashConfig cfg = { + .cb = flashCb, + .context = flashCtx, + .config = flashCfg, + }; + + /* Fault-injecting flash wrapper for the interrupted-write scenario */ + const whFlashCb flashFaultInjCb[1] = {WH_FLASH_FAULTINJECT_CB}; + whFlashFaultInjectCtx faultInjCtx[1] = {0}; + whFlashFaultInjectCfg faultInjCfg[1] = {{ + .realCb = flashCb, + .realCtx = flashCtx, + .realCfg = flashCfg, + }}; + whNvmFlashConfig faultCfg = { + .cb = flashFaultInjCb, + .context = faultInjCtx, + .config = faultInjCfg, + }; + + uint8_t dataPattern[100]; + uint8_t readBuf[100]; + whNvmMetadata dataMeta = {.id = 200, .label = "CrcDataTest"}; + whNvmMetadata metaMeta = {.id = 201, .label = "CrcMetaTest"}; + whNvmMetadata goodMeta = {.id = 202, .label = "CrcGoodTest"}; + whNvmMetadata cntrMeta = {.id = 203, .label = "CrcCounterTest"}; + whNvmMetadata keepMeta = {.id = 210, .label = "CrcKeepTest"}; + whNvmMetadata badCopyMeta = {.id = 211, .label = "CrcBadCopyTest"}; + whNvmMetadata intrMeta = {.id = 212, .label = "CrcIntrTest"}; + whNvmMetadata postMeta = {.id = 213, .label = "CrcPostTest"}; + whNvmMetadata lenMeta = {.id = 214, .label = "CrcLenTest"}; + whNvmMetadata v1Meta = {.id = 220, .label = "CrcResurrectV1"}; + whNvmMetadata v2Meta = {.id = 220, .label = "CrcResurrectV2"}; + whNvmMetadata metaBuf = {0}; + unsigned char metaData[] = "MetaTestData"; + unsigned char goodData[] = "GoodObjectData"; + unsigned char keepData[] = "KeepThisObject"; + unsigned char v1Data[] = "ResurrectV1Data"; + unsigned char v2Data[] = "ResurrectV2Data!"; + uint8_t intrData[40]; + uint8_t postData[40]; + whNvmId destroyId = 0; + uint32_t availBytes = 0; + uint32_t reclaimBytes = 0; + whNvmId availObjects = 0; + whNvmId reclaimObjects = 0; + int offset = -1; + uint32_t i = 0; + + WH_TEST_RETURN_ON_FAIL(_crc16Vectors()); + + for (i = 0; i < sizeof(dataPattern); i++) { + dataPattern[i] = (uint8_t)(0x5A ^ (i * 7)); + } + for (i = 0; i < sizeof(intrData); i++) { + intrData[i] = (uint8_t)(0xA5 ^ (i * 3)); + postData[i] = (uint8_t)(0x3C ^ (i * 5)); + } + + WH_TEST_PRINT("--CRC16: data corruption detection\n"); + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &dataMeta, (whNvmSize)sizeof(dataPattern), dataPattern)); + + /* Full read is verified and passes */ + WH_TEST_RETURN_ON_FAIL(cb->Read( + context, dataMeta.id, 0, (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(dataPattern, readBuf, sizeof(dataPattern))); + + /* Corrupt one data byte directly in the simulated flash */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, dataPattern, + (uint32_t)sizeof(dataPattern)); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset + 50] ^= 0xFF; + + /* Full read fails CRC. Partial reads are not verified */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->Read(context, dataMeta.id, 0, + (whNvmSize)sizeof(dataPattern), + readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, dataMeta.id, 0, + (whNvmSize)sizeof(dataPattern) - 1, + readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, dataMeta.id, 1, + (whNvmSize)sizeof(dataPattern) - 1, + readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, dataMeta.id, &metaBuf)); + + /* Reclaim must abort when copying the corrupt object */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->DestroyObjects(context, 0, NULL)); + /* Active partition is untouched: object still present */ + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, dataMeta.id, &metaBuf)); + + /* Restore the byte: read and reclaim work again */ + _recoveryMemory[offset + 50] ^= 0xFF; + WH_TEST_RETURN_ON_FAIL(cb->Read( + context, dataMeta.id, 0, (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + + /* Read back from the new partition, re-verified after the copy */ + WH_TEST_RETURN_ON_FAIL(cb->Read( + context, dataMeta.id, 0, (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(dataPattern, readBuf, sizeof(dataPattern))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: metadata corruption detection\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &metaMeta, (whNvmSize)sizeof(metaData), metaData)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &goodMeta, (whNvmSize)sizeof(goodData), goodData)); + + /* Corrupt one on-flash label byte of the first object */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, + (const uint8_t*)"CrcMetaTest", 11); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset + 3] ^= 0xFF; + + /* Reload the directory from flash to force verification */ + memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(_recoveryMemory, 0, NVM_FLASH_SIZE); + flashCfg->initData = _recoveryBackup; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + flashCfg->initData = NULL; + + /* Corrupt object is hidden, healthy object is intact */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, metaMeta.id, &metaBuf)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->Read(context, metaMeta.id, 0, + (whNvmSize)sizeof(metaData), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, goodMeta.id, &metaBuf)); + + /* Corrupt entry is accounted as reclaimable */ + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, + &availObjects, &reclaimBytes, + &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects >= 1); + WH_TEST_ASSERT_RETURN(reclaimBytes >= sizeof(metaData)); + + /* Compaction drops the corrupt object and keeps the healthy one */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, metaMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, goodMeta.id, 0, + (whNvmSize)sizeof(goodData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(goodData, readBuf, sizeof(goodData))); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, + &availObjects, &reclaimBytes, + &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects == 0); + + /* Freed slot and data are reusable */ + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &metaMeta, (whNvmSize)sizeof(metaData), metaData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, metaMeta.id, 0, + (whNvmSize)sizeof(metaData), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: metadata-only object\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject(context, &cntrMeta, 0, NULL)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, cntrMeta.id, &metaBuf)); + WH_TEST_ASSERT_RETURN(metaBuf.len == 0); + /* Zero-length objects have no readable data */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + cb->Read(context, cntrMeta.id, 0, 0, readBuf)); + /* Survives compaction with a vacuous data CRC */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, cntrMeta.id, &metaBuf)); + + /* Metadata CRC still protects it */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, + (const uint8_t*)"CrcCounterTest", 14); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset + 3] ^= 0xFF; + memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(_recoveryMemory, 0, NVM_FLASH_SIZE); + flashCfg->initData = _recoveryBackup; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + flashCfg->initData = NULL; + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, cntrMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: failed destroy leaves directory intact\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &keepMeta, (whNvmSize)sizeof(keepData), keepData)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject(context, &badCopyMeta, + (whNvmSize)sizeof(dataPattern), + dataPattern)); + + /* Corrupt one data byte of the object that is NOT being destroyed */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, dataPattern, + (uint32_t)sizeof(dataPattern)); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset + 10] ^= 0xFF; + + /* Destroy fails when the reclaim copies the corrupt object */ + destroyId = keepMeta.id; + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->DestroyObjects(context, 1, &destroyId)); + + /* The requested object must still be present and readable */ + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, keepMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, keepMeta.id, 0, + (whNvmSize)sizeof(keepData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(keepData, readBuf, sizeof(keepData))); + /* The corrupt object is still visible too */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->Read(context, badCopyMeta.id, 0, + (whNvmSize)sizeof(dataPattern), + readBuf)); + + /* Destroying the corrupt object itself succeeds (it is not copied) + * and must not drop the object whose destroy failed earlier */ + destroyId = badCopyMeta.id; + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 1, &destroyId)); + WH_TEST_ASSERT_RETURN( + WH_ERROR_NOTFOUND == + cb->GetMetadata(context, badCopyMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, keepMeta.id, 0, + (whNvmSize)sizeof(keepData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(keepData, readBuf, sizeof(keepData))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: interrupted write with corrupt metadata\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + + /* Interrupt an add at the count-word program (5th program: epoch, + * metadata, start, data, count), leaving its data on flash */ + faultInjCtx->failAfterPrograms = 5; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == + cb->AddObject(context, &intrMeta, + (whNvmSize)sizeof(intrData), + intrData)); + + /* Also corrupt one on-flash label byte of the interrupted entry */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, + (const uint8_t*)"CrcIntrTest", 11); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset + 3] ^= 0xFF; + + /* Reload the directory from flash */ + memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(_recoveryMemory, 0, NVM_FLASH_SIZE); + flashCfg->initData = _recoveryBackup; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + flashCfg->initData = NULL; + + /* Entry is hidden. Without trusted metadata its extent is unknown, + * so the rest of the data area is reserved and new writes are + * refused */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, intrMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, + &availObjects, &reclaimBytes, + &reclaimObjects)); + WH_TEST_ASSERT_RETURN(availBytes == 0); + WH_TEST_ASSERT_RETURN(reclaimObjects >= 1); + WH_TEST_ASSERT_RETURN(reclaimBytes >= sizeof(intrData)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOSPACE == + cb->AddObject(context, &postMeta, + (whNvmSize)sizeof(postData), + postData)); + + /* Compaction reclaims the interrupted entry and unblocks writes */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, intrMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &postMeta, (whNvmSize)sizeof(postData), postData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, postMeta.id, 0, + (whNvmSize)sizeof(postData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(postData, readBuf, sizeof(postData))); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, + &availObjects, &reclaimBytes, + &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects == 0); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: interrupted write with corrupt length\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + + /* Interrupt an add at the count-word program, as above */ + faultInjCtx->failAfterPrograms = 5; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == + cb->AddObject(context, &lenMeta, + (whNvmSize)sizeof(intrData), + intrData)); + + /* Under-report the entry's on-flash length: len is the two bytes + * before the label, which starts at metadata byte 8 */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, + (const uint8_t*)"CrcLenTest", 10); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset - 2] = 1; + _recoveryMemory[offset - 1] = 0; + + /* Reload the directory from flash */ + memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(_recoveryMemory, 0, NVM_FLASH_SIZE); + flashCfg->initData = _recoveryBackup; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + flashCfg->initData = NULL; + + /* The corrupt length is not trusted: a new add must not land on the + * entry's partially written data, so writes are refused instead */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, lenMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, + &availObjects, &reclaimBytes, + &reclaimObjects)); + WH_TEST_ASSERT_RETURN(availBytes == 0); + WH_TEST_ASSERT_RETURN(reclaimObjects >= 1); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOSPACE == + cb->AddObject(context, &postMeta, + (whNvmSize)sizeof(postData), + postData)); + + /* Compaction reclaims the entry and unblocks writes */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, lenMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &postMeta, (whNvmSize)sizeof(postData), postData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, postMeta.id, 0, + (whNvmSize)sizeof(postData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(postData, readBuf, sizeof(postData))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: corrupt overwrite resurrects previous " + "version\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL( + cb->AddObject(context, &v1Meta, (whNvmSize)sizeof(v1Data), v1Data)); + WH_TEST_RETURN_ON_FAIL( + cb->AddObject(context, &v2Meta, (whNvmSize)sizeof(v2Data), v2Data)); + + /* In session the overwrite is authoritative */ + WH_TEST_RETURN_ON_FAIL(cb->Read(context, v2Meta.id, 0, + (whNvmSize)sizeof(v2Data), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(v2Data, readBuf, sizeof(v2Data))); + + /* Corrupt one on-flash label byte of the newest copy */ + offset = _findFlashPattern(_recoveryMemory, NVM_FLASH_SIZE, + (const uint8_t*)"CrcResurrectV2", 14); + WH_TEST_ASSERT_RETURN(offset >= 0); + _recoveryMemory[offset + 3] ^= 0xFF; + + /* Reload the directory from flash */ + memcpy(_recoveryBackup, _recoveryMemory, NVM_FLASH_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(_recoveryMemory, 0, NVM_FLASH_SIZE); + flashCfg->initData = _recoveryBackup; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + flashCfg->initData = NULL; + + /* Documented caveat: with the newest copy corrupt, the previous + * version becomes visible again */ + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, v1Meta.id, &metaBuf)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(metaBuf.label, v1Meta.label, sizeof(metaBuf.label))); + WH_TEST_ASSERT_RETURN(metaBuf.len == sizeof(v1Data)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, v1Meta.id, 0, + (whNvmSize)sizeof(v1Data), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(v1Data, readBuf, sizeof(v1Data))); + + /* The corrupt newest copy is reclaimable; compaction drops it and + * the previous version remains */ + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, + &availObjects, &reclaimBytes, + &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects == 1); + WH_TEST_ASSERT_RETURN(reclaimBytes >= sizeof(v2Data)); + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, v1Meta.id, 0, + (whNvmSize)sizeof(v1Data), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(v1Data, readBuf, sizeof(v1Data))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + return 0; + } +#else + return WH_TEST_SKIPPED; +#endif /* WOLFHSM_CFG_NVM_FLASH_CRC16 */ +} diff --git a/test-refactor/posix/wh_test_posix_main.c b/test-refactor/posix/wh_test_posix_main.c index da9eb4c22..32ee2ecd0 100644 --- a/test-refactor/posix/wh_test_posix_main.c +++ b/test-refactor/posix/wh_test_posix_main.c @@ -77,6 +77,7 @@ int whTest_FlashUnitOps(void* ctx); int whTest_NvmAddOverwriteDestroy(void* ctx); int whTest_NvmFlashLog(void* ctx); int whTest_NvmRecovery(void* ctx); +int whTest_NvmCrc16(void* ctx); /* POSIX-specific logging tests. The portable log suite (frontend, * macros, ring buffer, mock/ringbuf harness) runs in the Misc group; @@ -296,6 +297,10 @@ int main(void) if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) { miscRc = rc; } + rc = whTestGroup_RunOne("whTest_NvmCrc16", whTest_NvmCrc16, NULL); + if (rc != 0 && rc != WH_TEST_SKIPPED && miscRc == 0) { + miscRc = rc; + } /* POSIX-specific log backend tests. Self-contained (each owns * its own contexts and threads), so run inline before the port * spins up its own server thread. */ diff --git a/test/Makefile b/test/Makefile index 1d7b58d51..b3b551bf8 100644 --- a/test/Makefile +++ b/test/Makefile @@ -142,6 +142,11 @@ ifeq ($(DMA),1) DEF += -DWOLFHSM_CFG_DMA endif +# Enable CRC16 integrity checking in the NVM flash backend +ifeq ($(NVM_FLASH_CRC),1) + DEF += -DWOLFHSM_CFG_NVM_FLASH_CRC16 +endif + # Build LMS/XMSS in verify-only mode (omits private-key, sign, and keygen # paths). May be combined to exercise the mixed (one verify-only) case. ifeq ($(LMS_VERIFY_ONLY),1) diff --git a/test/wh_test_nvm_flash.c b/test/wh_test_nvm_flash.c index 66eaf43f5..eec3a881d 100644 --- a/test/wh_test_nvm_flash.c +++ b/test/wh_test_nvm_flash.c @@ -33,6 +33,7 @@ #include "wolfhsm/wh_nvm_flash.h" #include "wolfhsm/wh_nvm_flash_log.h" #include "wolfhsm/wh_flash_unit.h" +#include "wolfhsm/wh_utils.h" /* NVM simulator backends to use for testing NVM module */ #include "wolfhsm/wh_flash_ramsim.h" @@ -624,6 +625,110 @@ simulateFailureAndRecover(int failAfter, int* dataSize, return 0; } +/* Interrupt an add while a committed object is already in the partition, + * then verify the reloaded directory accounts for both data regions and + * places new objects after them */ +static int simulateFailureWithPrecedingObject(void) +{ + uint8_t memory[FLASH_RAM_SIZE] = {0}; + uint8_t backupMemory[FLASH_RAM_SIZE] = {0}; + const whFlashCb flashCb[1] = {WH_FLASH_RAMSIM_CB}; + whFlashRamsimCtx flashCtx[1] = {0}; + whFlashRamsimCfg flashCfg[1] = {{ + .size = FLASH_RAM_SIZE, + .sectorSize = FLASH_SECTOR_SIZE, + .pageSize = FLASH_PAGE_SIZE, + .erasedByte = (uint8_t)0, + .memory = memory, + }}; + const whFlashCb flashFaultInjCb[1] = {WH_FLASH_FAULTINJECT_CB}; + whFlashFaultInjectCtx faultInjCtx[1] = {0}; + whFlashFaultInjectCfg faultInjCfg[1] = {{ + .realCb = flashCb, + .realCtx = flashCtx, + .realCfg = flashCfg, + }}; + const whNvmCb cb[1] = {WH_NVM_FLASH_CB}; + whNvmFlashContext context[1] = {0}; + whNvmFlashConfig cfg = { + .cb = flashFaultInjCb, + .context = faultInjCtx, + .config = faultInjCfg, + }; + whNvmMetadata firstMeta = {.id = 50, .label = "RecoveryFirst"}; + whNvmMetadata intrMeta = {.id = 51, .label = "RecoveryIntr"}; + whNvmMetadata postMeta = {.id = 52, .label = "RecoveryPost"}; + whNvmMetadata checkMeta = {0}; + uint8_t firstData[64]; + uint8_t intrData[40]; + uint8_t postData[40]; + uint8_t readBuf[64]; + uint32_t availStart = 0; + uint32_t availAfter = 0; + uint32_t reclaimAfter = 0; + whNvmId objsStart = 0; + whNvmId objsAfter = 0; + whNvmId objsReclAfter = 0; + uint32_t i = 0; + + for (i = 0; i < sizeof(firstData); i++) { + firstData[i] = (uint8_t)(0x11 ^ (i * 7)); + } + for (i = 0; i < sizeof(intrData); i++) { + intrData[i] = (uint8_t)(0x22 ^ (i * 3)); + postData[i] = (uint8_t)(0x33 ^ (i * 5)); + } + + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL( + cb->GetAvailable(context, &availStart, &objsStart, NULL, NULL)); + + /* Commit one object so the interrupted entry's data starts at a nonzero + * offset */ + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &firstMeta, (whNvmSize)sizeof(firstData), firstData)); + + /* Interrupt the next add at the count-word program (5th program: epoch, + * metadata, start, data, count) */ + faultInjCtx->failAfterPrograms = 5; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == + cb->AddObject(context, &intrMeta, + (whNvmSize)sizeof(intrData), intrData)); + + /* Reboot onto the dirty flash */ + memcpy(backupMemory, memory, FLASH_RAM_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(memory, 0, FLASH_RAM_SIZE); + flashCfg->initData = backupMemory; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + + /* Committed object is intact, interrupted one is hidden */ + WH_TEST_RETURN_ON_FAIL(cb->Read(context, firstMeta.id, 0, + (whNvmSize)sizeof(firstData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(firstData, readBuf, sizeof(firstData))); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, intrMeta.id, &checkMeta)); + + /* Both data regions are accounted: the committed object as used, the + * interrupted one as reserved and reclaimable */ + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availAfter, &objsAfter, + &reclaimAfter, &objsReclAfter)); + WH_TEST_ASSERT_RETURN(availAfter == + availStart - sizeof(firstData) - sizeof(intrData)); + WH_TEST_ASSERT_RETURN(objsAfter == objsStart - 2); + WH_TEST_ASSERT_RETURN(reclaimAfter == sizeof(intrData)); + WH_TEST_ASSERT_RETURN(objsReclAfter == 1); + + /* A new add lands after both regions instead of on top of them */ + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &postMeta, (whNvmSize)sizeof(postData), postData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, postMeta.id, 0, + (whNvmSize)sizeof(postData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(postData, readBuf, sizeof(postData))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + return 0; +} + int whTest_NvmFlash_Recovery(void) { int test_data_len; @@ -661,9 +766,453 @@ int whTest_NvmFlash_Recovery(void) /* available object should be decremented */ WH_TEST_ASSERT_RETURN(objsAfter == objsBefore - 1); + WH_TEST_PRINT("--simulate failure after a committed object\n"); + WH_TEST_RETURN_ON_FAIL(simulateFailureWithPrecedingObject()); + return 0; } +#if defined(WOLFHSM_CFG_NVM_FLASH_CRC16) + +/* Find needle in haystack. Returns byte offset or -1 if not found */ +static int findFlashPattern(const uint8_t* haystack, uint32_t hay_len, + const uint8_t* needle, uint32_t needle_len) +{ + uint32_t i; + + if ((needle_len == 0) || (needle_len > hay_len)) { + return -1; + } + for (i = 0; i <= hay_len - needle_len; i++) { + if (memcmp(&haystack[i], needle, needle_len) == 0) { + return (int)i; + } + } + return -1; +} + +static int whTest_NvmFlash_Crc16Vectors(void) +{ + const char* check = "123456789"; + uint16_t crc; + uint16_t crc_split; + + /* Known CRC-16/CCITT-FALSE check value */ + crc = wh_Utils_Crc16(WH_UTILS_CRC16_INIT, check, 9); + WH_TEST_ASSERT_RETURN(crc == 0x29B1); + + /* Incremental computation matches one-shot */ + crc_split = wh_Utils_Crc16(WH_UTILS_CRC16_INIT, check, 4); + crc_split = wh_Utils_Crc16(crc_split, check + 4, 5); + WH_TEST_ASSERT_RETURN(crc_split == crc); + + /* Zero length returns the seed */ + WH_TEST_ASSERT_RETURN(wh_Utils_Crc16(WH_UTILS_CRC16_INIT, check, 0) == + WH_UTILS_CRC16_INIT); + WH_TEST_ASSERT_RETURN(wh_Utils_Crc16(WH_UTILS_CRC16_INIT, NULL, 0) == + WH_UTILS_CRC16_INIT); + /* NULL data is treated as zero length regardless of len */ + WH_TEST_ASSERT_RETURN(wh_Utils_Crc16(WH_UTILS_CRC16_INIT, NULL, 5) == + WH_UTILS_CRC16_INIT); + return 0; +} + +int whTest_NvmFlash_Crc16(void) +{ + uint8_t memory[FLASH_RAM_SIZE] = {0}; + uint8_t backupMemory[FLASH_RAM_SIZE] = {0}; + const whFlashCb flashCb[1] = {WH_FLASH_RAMSIM_CB}; + whFlashRamsimCtx flashCtx[1] = {0}; + whFlashRamsimCfg flashCfg[1] = {{ + .size = FLASH_RAM_SIZE, + .sectorSize = FLASH_SECTOR_SIZE, + .pageSize = FLASH_PAGE_SIZE, + .erasedByte = (uint8_t)0, + .memory = memory, + }}; + whNvmFlashConfig cfg = { + .cb = flashCb, + .context = flashCtx, + .config = flashCfg, + }; + whNvmFlashContext context[1] = {0}; + const whNvmCb cb[1] = {WH_NVM_FLASH_CB}; + + /* Fault-injecting flash wrapper for the interrupted-write scenario */ + const whFlashCb flashFaultInjCb[1] = {WH_FLASH_FAULTINJECT_CB}; + whFlashFaultInjectCtx faultInjCtx[1] = {0}; + whFlashFaultInjectCfg faultInjCfg[1] = {{ + .realCb = flashCb, + .realCtx = flashCtx, + .realCfg = flashCfg, + }}; + whNvmFlashConfig faultCfg = { + .cb = flashFaultInjCb, + .context = faultInjCtx, + .config = faultInjCfg, + }; + + uint8_t dataPattern[100]; + uint8_t readBuf[100]; + whNvmMetadata dataMeta = {.id = 200, .label = "CrcDataTest"}; + whNvmMetadata metaMeta = {.id = 201, .label = "CrcMetaTest"}; + whNvmMetadata goodMeta = {.id = 202, .label = "CrcGoodTest"}; + whNvmMetadata cntrMeta = {.id = 203, .label = "CrcCounterTest"}; + whNvmMetadata keepMeta = {.id = 210, .label = "CrcKeepTest"}; + whNvmMetadata badCopyMeta = {.id = 211, .label = "CrcBadCopyTest"}; + whNvmMetadata intrMeta = {.id = 212, .label = "CrcIntrTest"}; + whNvmMetadata postMeta = {.id = 213, .label = "CrcPostTest"}; + whNvmMetadata lenMeta = {.id = 214, .label = "CrcLenTest"}; + whNvmMetadata v1Meta = {.id = 220, .label = "CrcResurrectV1"}; + whNvmMetadata v2Meta = {.id = 220, .label = "CrcResurrectV2"}; + whNvmMetadata metaBuf = {0}; + unsigned char metaData[] = "MetaTestData"; + unsigned char goodData[] = "GoodObjectData"; + unsigned char keepData[] = "KeepThisObject"; + unsigned char v1Data[] = "ResurrectV1Data"; + unsigned char v2Data[] = "ResurrectV2Data!"; + uint8_t intrData[40]; + uint8_t postData[40]; + whNvmId destroyId = 0; + uint32_t availBytes = 0; + uint32_t reclaimBytes = 0; + whNvmId availObjects = 0; + whNvmId reclaimObjects = 0; + int offset = -1; + uint32_t i = 0; + + WH_TEST_RETURN_ON_FAIL(whTest_NvmFlash_Crc16Vectors()); + + for (i = 0; i < sizeof(dataPattern); i++) { + dataPattern[i] = (uint8_t)(0x5A ^ (i * 7)); + } + for (i = 0; i < sizeof(intrData); i++) { + intrData[i] = (uint8_t)(0xA5 ^ (i * 3)); + postData[i] = (uint8_t)(0x3C ^ (i * 5)); + } + + WH_TEST_PRINT("--CRC16: data corruption detection\n"); + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &dataMeta, (whNvmSize)sizeof(dataPattern), dataPattern)); + + /* Full read is verified and passes */ + WH_TEST_RETURN_ON_FAIL(cb->Read(context, dataMeta.id, 0, + (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_ASSERT_RETURN(0 == + memcmp(dataPattern, readBuf, sizeof(dataPattern))); + + /* Corrupt one data byte directly in the simulated flash */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, dataPattern, + (uint32_t)sizeof(dataPattern)); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset + 50] ^= 0xFF; + + /* Full read fails CRC. Partial reads are not verified */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->Read(context, dataMeta.id, 0, + (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read( + context, dataMeta.id, 0, (whNvmSize)sizeof(dataPattern) - 1, readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read( + context, dataMeta.id, 1, (whNvmSize)sizeof(dataPattern) - 1, readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, dataMeta.id, &metaBuf)); + + /* Reclaim must abort when copying the corrupt object */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->DestroyObjects(context, 0, NULL)); + /* Active partition is untouched: object still present */ + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, dataMeta.id, &metaBuf)); + + /* Restore the byte: read and reclaim work again */ + memory[offset + 50] ^= 0xFF; + WH_TEST_RETURN_ON_FAIL(cb->Read(context, dataMeta.id, 0, + (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + + /* Read back from the new partition, re-verified after the copy */ + WH_TEST_RETURN_ON_FAIL(cb->Read(context, dataMeta.id, 0, + (whNvmSize)sizeof(dataPattern), readBuf)); + WH_TEST_ASSERT_RETURN(0 == + memcmp(dataPattern, readBuf, sizeof(dataPattern))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: metadata corruption detection\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &metaMeta, (whNvmSize)sizeof(metaData), metaData)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &goodMeta, (whNvmSize)sizeof(goodData), goodData)); + + /* Corrupt one on-flash label byte of the first object */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, + (const uint8_t*)"CrcMetaTest", 11); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset + 3] ^= 0xFF; + + /* Reload the directory from flash to force verification */ + memcpy(backupMemory, memory, FLASH_RAM_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(memory, 0, FLASH_RAM_SIZE); + flashCfg->initData = backupMemory; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + flashCfg->initData = NULL; + + /* Corrupt object is hidden, healthy object is intact */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, metaMeta.id, &metaBuf)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->Read(context, metaMeta.id, 0, + (whNvmSize)sizeof(metaData), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, goodMeta.id, &metaBuf)); + + /* Corrupt entry is accounted as reclaimable */ + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, &availObjects, + &reclaimBytes, &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects >= 1); + WH_TEST_ASSERT_RETURN(reclaimBytes >= sizeof(metaData)); + + /* Compaction drops the corrupt object and keeps the healthy one */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, metaMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, goodMeta.id, 0, + (whNvmSize)sizeof(goodData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(goodData, readBuf, sizeof(goodData))); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, &availObjects, + &reclaimBytes, &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects == 0); + + /* Freed slot and data are reusable */ + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &metaMeta, (whNvmSize)sizeof(metaData), metaData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, metaMeta.id, 0, + (whNvmSize)sizeof(metaData), readBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: metadata-only object\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject(context, &cntrMeta, 0, NULL)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, cntrMeta.id, &metaBuf)); + WH_TEST_ASSERT_RETURN(metaBuf.len == 0); + /* Zero-length objects have no readable data */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + cb->Read(context, cntrMeta.id, 0, 0, readBuf)); + /* Survives compaction with a vacuous data CRC */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, cntrMeta.id, &metaBuf)); + + /* Metadata CRC still protects it */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, + (const uint8_t*)"CrcCounterTest", 14); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset + 3] ^= 0xFF; + memcpy(backupMemory, memory, FLASH_RAM_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(memory, 0, FLASH_RAM_SIZE); + flashCfg->initData = backupMemory; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + flashCfg->initData = NULL; + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, cntrMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: failed destroy leaves directory intact\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &keepMeta, (whNvmSize)sizeof(keepData), keepData)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &badCopyMeta, (whNvmSize)sizeof(dataPattern), dataPattern)); + + /* Corrupt one data byte of the object that is NOT being destroyed */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, dataPattern, + (uint32_t)sizeof(dataPattern)); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset + 10] ^= 0xFF; + + /* Destroy fails when the reclaim copies the corrupt object */ + destroyId = keepMeta.id; + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->DestroyObjects(context, 1, &destroyId)); + + /* The requested object must still be present and readable */ + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, keepMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, keepMeta.id, 0, + (whNvmSize)sizeof(keepData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(keepData, readBuf, sizeof(keepData))); + /* The corrupt object is still visible too */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTVERIFIED == + cb->Read(context, badCopyMeta.id, 0, + (whNvmSize)sizeof(dataPattern), readBuf)); + + /* Destroying the corrupt object itself succeeds (it is not copied) and + * must not drop the object whose destroy failed earlier */ + destroyId = badCopyMeta.id; + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 1, &destroyId)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, badCopyMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, keepMeta.id, 0, + (whNvmSize)sizeof(keepData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(keepData, readBuf, sizeof(keepData))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: interrupted write with corrupt metadata\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + + /* Interrupt an add at the count-word program (5th program: epoch, + * metadata, start, data, count), leaving its data on flash */ + faultInjCtx->failAfterPrograms = 5; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == + cb->AddObject(context, &intrMeta, + (whNvmSize)sizeof(intrData), intrData)); + + /* Also corrupt one on-flash label byte of the interrupted entry */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, + (const uint8_t*)"CrcIntrTest", 11); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset + 3] ^= 0xFF; + + /* Reload the directory from flash */ + memcpy(backupMemory, memory, FLASH_RAM_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(memory, 0, FLASH_RAM_SIZE); + flashCfg->initData = backupMemory; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + flashCfg->initData = NULL; + + /* Entry is hidden. Without trusted metadata its extent is unknown, so + * the rest of the data area is reserved and new writes are refused */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, intrMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, &availObjects, + &reclaimBytes, &reclaimObjects)); + WH_TEST_ASSERT_RETURN(availBytes == 0); + WH_TEST_ASSERT_RETURN(reclaimObjects >= 1); + WH_TEST_ASSERT_RETURN(reclaimBytes >= sizeof(intrData)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOSPACE == + cb->AddObject(context, &postMeta, + (whNvmSize)sizeof(postData), postData)); + + /* Compaction reclaims the interrupted entry and unblocks writes */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, intrMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &postMeta, (whNvmSize)sizeof(postData), postData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, postMeta.id, 0, + (whNvmSize)sizeof(postData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(postData, readBuf, sizeof(postData))); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, &availObjects, + &reclaimBytes, &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects == 0); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: interrupted write with corrupt length\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + + /* Interrupt an add at the count-word program, as above */ + faultInjCtx->failAfterPrograms = 5; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == + cb->AddObject(context, &lenMeta, + (whNvmSize)sizeof(intrData), intrData)); + + /* Under-report the entry's on-flash length: len is the two bytes before + * the label, which starts at metadata byte 8 */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, + (const uint8_t*)"CrcLenTest", 10); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset - 2] = 1; + memory[offset - 1] = 0; + + /* Reload the directory from flash */ + memcpy(backupMemory, memory, FLASH_RAM_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(memory, 0, FLASH_RAM_SIZE); + flashCfg->initData = backupMemory; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &faultCfg)); + flashCfg->initData = NULL; + + /* The corrupt length is not trusted: a new add must not land on the + * entry's partially written data, so writes are refused instead */ + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, lenMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, &availObjects, + &reclaimBytes, &reclaimObjects)); + WH_TEST_ASSERT_RETURN(availBytes == 0); + WH_TEST_ASSERT_RETURN(reclaimObjects >= 1); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOSPACE == + cb->AddObject(context, &postMeta, + (whNvmSize)sizeof(postData), postData)); + + /* Compaction reclaims the entry and unblocks writes */ + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + cb->GetMetadata(context, lenMeta.id, &metaBuf)); + WH_TEST_RETURN_ON_FAIL(cb->AddObject( + context, &postMeta, (whNvmSize)sizeof(postData), postData)); + WH_TEST_RETURN_ON_FAIL(cb->Read(context, postMeta.id, 0, + (whNvmSize)sizeof(postData), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(postData, readBuf, sizeof(postData))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + WH_TEST_PRINT("--CRC16: corrupt overwrite resurrects previous version\n"); + flashCfg->initData = NULL; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + WH_TEST_RETURN_ON_FAIL( + cb->AddObject(context, &v1Meta, (whNvmSize)sizeof(v1Data), v1Data)); + WH_TEST_RETURN_ON_FAIL( + cb->AddObject(context, &v2Meta, (whNvmSize)sizeof(v2Data), v2Data)); + + /* In session the overwrite is authoritative */ + WH_TEST_RETURN_ON_FAIL( + cb->Read(context, v2Meta.id, 0, (whNvmSize)sizeof(v2Data), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(v2Data, readBuf, sizeof(v2Data))); + + /* Corrupt one on-flash label byte of the newest copy */ + offset = findFlashPattern(memory, FLASH_RAM_SIZE, + (const uint8_t*)"CrcResurrectV2", 14); + WH_TEST_ASSERT_RETURN(offset >= 0); + memory[offset + 3] ^= 0xFF; + + /* Reload the directory from flash */ + memcpy(backupMemory, memory, FLASH_RAM_SIZE); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + memset(memory, 0, FLASH_RAM_SIZE); + flashCfg->initData = backupMemory; + WH_TEST_RETURN_ON_FAIL(cb->Init(context, &cfg)); + flashCfg->initData = NULL; + + /* Documented caveat: with the newest copy corrupt, the previous version + * becomes visible again */ + WH_TEST_RETURN_ON_FAIL(cb->GetMetadata(context, v1Meta.id, &metaBuf)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(metaBuf.label, v1Meta.label, sizeof(metaBuf.label))); + WH_TEST_ASSERT_RETURN(metaBuf.len == sizeof(v1Data)); + WH_TEST_RETURN_ON_FAIL( + cb->Read(context, v1Meta.id, 0, (whNvmSize)sizeof(v1Data), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(v1Data, readBuf, sizeof(v1Data))); + + /* The corrupt newest copy is reclaimable; compaction drops it and the + * previous version remains */ + WH_TEST_RETURN_ON_FAIL(cb->GetAvailable(context, &availBytes, &availObjects, + &reclaimBytes, &reclaimObjects)); + WH_TEST_ASSERT_RETURN(reclaimObjects == 1); + WH_TEST_ASSERT_RETURN(reclaimBytes >= sizeof(v2Data)); + WH_TEST_RETURN_ON_FAIL(cb->DestroyObjects(context, 0, NULL)); + WH_TEST_RETURN_ON_FAIL( + cb->Read(context, v1Meta.id, 0, (whNvmSize)sizeof(v1Data), readBuf)); + WH_TEST_ASSERT_RETURN(0 == memcmp(v1Data, readBuf, sizeof(v1Data))); + WH_TEST_RETURN_ON_FAIL(cb->Cleanup(context)); + + return 0; +} +#endif /* WOLFHSM_CFG_NVM_FLASH_CRC16 */ + #if defined(WOLFHSM_CFG_TEST_POSIX) int whTest_NvmFlash_PosixFileSim(void) @@ -749,6 +1298,11 @@ int whTest_NvmFlash(void) WH_TEST_PRINT("Testing NVM flash recovery mechanism...\n"); WH_TEST_ASSERT(0 == whTest_NvmFlash_Recovery()); +#if defined(WOLFHSM_CFG_NVM_FLASH_CRC16) + WH_TEST_PRINT("Testing NVM flash CRC16 integrity checks...\n"); + WH_TEST_ASSERT(0 == whTest_NvmFlash_Crc16()); +#endif + #if defined(WOLFHSM_CFG_TEST_POSIX) WH_TEST_PRINT("Testing NVM flash with POSIX file sim...\n"); WH_TEST_ASSERT(0 == whTest_NvmFlash_PosixFileSim()); diff --git a/test/wh_test_nvm_flash.h b/test/wh_test_nvm_flash.h index 799679c95..a00cef446 100644 --- a/test/wh_test_nvm_flash.h +++ b/test/wh_test_nvm_flash.h @@ -54,4 +54,14 @@ int whTest_NvmFlashCfg(whNvmFlashConfig* cfg); */ int whTest_NvmFlash_Recovery(void); +#if defined(WOLFHSM_CFG_NVM_FLASH_CRC16) +/* + * Tests NVM flash CRC16 integrity checking: CRC vectors, detection of + * corrupted object data on full reads and reclaim copies, and detection of + * corrupted metadata when the directory is loaded. + * Returns 0 on success, and a non-zero error code on failure + */ +int whTest_NvmFlash_Crc16(void); +#endif + #endif /* TEST_WH_TEST_NVM_FLASH_H_ */ diff --git a/tools/whnvmtool/Makefile b/tools/whnvmtool/Makefile index 963147bdf..853ed1f41 100644 --- a/tools/whnvmtool/Makefile +++ b/tools/whnvmtool/Makefile @@ -95,6 +95,12 @@ ifeq ($(DEBUG_VERBOSE),1) CFLAGS += -g -O0 -DDEBUG -ggdb3 -DWOLFHSM_CFG_DEBUG -DWOLFHSM_CFG_DEBUG_VERBOSE endif +# Enable CRC16 integrity checking in the NVM flash backend. Must match the +# setting of the server that will use the generated image. +ifeq ($(NVM_FLASH_CRC),1) + CFLAGS += -DWOLFHSM_CFG_NVM_FLASH_CRC16 +endif + ifeq ($(ASAN), 1) CFLAGS_EXTRA += -fsanitize=address endif diff --git a/tools/whnvmtool/README.md b/tools/whnvmtool/README.md index 7bf62d80b..e7771fb1f 100644 --- a/tools/whnvmtool/README.md +++ b/tools/whnvmtool/README.md @@ -133,7 +133,7 @@ she 1 4 0 0x00 path/to/she_key.bin The generated NVM image is a binary file that can be used to initialize an instance of `whNvmFlash` or loaded directly into device memory at a device-specific address. In order for a generated NVM image to be compatible with a wolfHSM server implementation, the following must be true: -1. `whnvmtool` must be compiled against the same version of wolfHSM as the server, and be compiled to use the same value of `WOLFHSM_CFG_NVM_OBJECT_COUNT` +1. `whnvmtool` must be compiled against the same version of wolfHSM as the server, and be compiled to use the same value of `WOLFHSM_CFG_NVM_OBJECT_COUNT` and the same `WOLFHSM_CFG_NVM_FLASH_CRC16` setting (build with `NVM_FLASH_CRC=1` to match a CRC-enabled server; a CRC-enabled server treats every object in a non-CRC image as corrupt and discards them at the first compaction, while a non-CRC server loads a CRC image but ignores its CRCs) 2. The partition size specified for the NVM image must match that of the server's `whNvmFlash` provider 3. If using a real flash implementation, the binary NVM image must be programmed to the correct address diff --git a/tools/whnvmtool/test/Makefile b/tools/whnvmtool/test/Makefile index f8d66ccda..5b94d6c1f 100644 --- a/tools/whnvmtool/test/Makefile +++ b/tools/whnvmtool/test/Makefile @@ -62,6 +62,12 @@ ifeq ($(DEBUG_VERBOSE),1) CFLAGS += -g -O0 -DDEBUG -ggdb3 -DWOLFHSM_CFG_DEBUG -DWOLFHSM_CFG_DEBUG_VERBOSE endif +# Enable CRC16 integrity checking in the NVM flash backend. Must match the +# whnvmtool build that generated the image. +ifeq ($(NVM_FLASH_CRC),1) + CFLAGS += -DWOLFHSM_CFG_NVM_FLASH_CRC16 +endif + # Targets all: $(OUT) diff --git a/wolfhsm/wh_nvm_flash.h b/wolfhsm/wh_nvm_flash.h index b928a59c7..16d67d045 100644 --- a/wolfhsm/wh_nvm_flash.h +++ b/wolfhsm/wh_nvm_flash.h @@ -37,11 +37,14 @@ /* In-memory computed status of an Object or Directory */ typedef enum { - NF_STATUS_UNKNOWN = 0, /* State is unknown/not read yet */ - NF_STATUS_FREE = 1, /* State is known to be free/erased */ - NF_STATUS_USED = 2, /* State is known to be used/intact */ - NF_STATUS_DATA_BAD = 3, /* State is known damaged or duplicate data */ - NF_STATUS_META_BAD = 4, /* State is known damaged meta */ + NF_STATUS_UNKNOWN = 0, /* State is unknown/not read yet */ + NF_STATUS_FREE = 1, /* State is known to be free/erased */ + NF_STATUS_USED = 2, /* State is known to be used/intact */ + NF_STATUS_DATA_BAD = 3, /* State is known damaged or duplicate data */ + NF_STATUS_META_BAD = 4, /* State is known damaged meta */ + NF_STATUS_CRC_BAD = 5, /* Metadata failed CRC check */ + NF_STATUS_LEN_BAD = 6, /* Interrupted entry whose metadata failed CRC: + * the extent of its data area is unknown */ } nfStatus; /* In-memory version of an Object or Directory State */ @@ -50,6 +53,10 @@ typedef struct { uint32_t epoch; uint32_t start; uint32_t count; +#ifdef WOLFHSM_CFG_NVM_FLASH_CRC16 + uint16_t crc_meta; /* CRC16 of metadata, from the start state word */ + uint16_t crc_data; /* CRC16 of object data, from the count state word */ +#endif } nfMemState; /* In-memory version of an Object */ @@ -76,13 +83,16 @@ typedef struct whNvmFlashConfig_t { } whNvmFlashConfig; typedef struct whNvmFlashContext_t { - const whFlashCb* cb; /* Flash callbacks */ - void* flash; /* Flash context to use */ - nfMemState state; /* State of active partition */ - nfMemDirectory directory; /* Cache of active objects */ - uint32_t partition_units; /* Size of partition in units */ - int active; /* Which partition (0 or 1) is active */ - int initialized; + const whFlashCb* cb; /* Flash callbacks */ + void* flash; /* Flash context to use */ + nfMemState state; /* State of active partition */ + nfMemDirectory directory; /* Cache of active objects */ + uint32_t partition_units; /* Size of partition in units */ + int active; /* Which partition (0 or 1) is active */ + int initialized; + int directory_bad; /* Directory could not be reloaded from + * flash. All operations are refused until + * the context is reinitialized */ uint8_t WH_PAD[4]; } whNvmFlashContext; diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h index 1d2157697..1d7680c7a 100644 --- a/wolfhsm/wh_settings.h +++ b/wolfhsm/wh_settings.h @@ -101,6 +101,14 @@ * WOLFHSM_CFG_NVM_OBJECT_COUNT - Number of objects in ram and disk directories * Default: 32 * + * WOLFHSM_CFG_NVM_FLASH_CRC16 - If defined, the nvm_flash backend stores a + * CRC16 of each object's metadata and data in the on-flash object state and + * verifies them: metadata when the directory is loaded, data on full-object + * reads and reclaim copies. Changes the on-flash format: images written with + * and without this option are mutually incompatible, and whnvmtool must be + * built with the same setting as the server. + * Default: Not defined + * * WOLFHSM_CFG_SERVER_KEYCACHE_COUNT - Number of RAM keys * Default: 8 * diff --git a/wolfhsm/wh_utils.h b/wolfhsm/wh_utils.h index 2f796904c..9b7d3e8eb 100644 --- a/wolfhsm/wh_utils.h +++ b/wolfhsm/wh_utils.h @@ -67,6 +67,16 @@ uint64_t wh_Utils_Swap64(uint64_t val); uint32_t wh_Utils_htonl(uint32_t hostlong); uint32_t wh_Utils_ntohl(uint32_t networklong); +/** CRC16 functions */ +/* Seed value for wh_Utils_Crc16 */ +#define WH_UTILS_CRC16_INIT ((uint16_t)0xFFFFU) + +/* Compute CRC-16/CCITT-FALSE (poly 0x1021) over len bytes of data. Seed crc + * with WH_UTILS_CRC16_INIT, or pass a previous return value to continue an + * incremental computation. Returns crc unchanged when len is 0 or data is + * NULL. */ +uint16_t wh_Utils_Crc16(uint16_t crc, const void* data, size_t len); + int wh_Utils_memeqzero(uint8_t* buffer, uint32_t size); /* Secure zeroization that resists compiler optimization */