wolfBoot/tools/unit-tests/unit-pkcs11_store.c

922 lines
34 KiB
C

/* unit-pkcs11_store.c
*
* Unit test for PKCS11 storage module
*
*
* Copyright (C) 2026 wolfSSL Inc.
*
* This file is part of wolfBoot.
*
* wolfBoot is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfBoot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
/* Option to enable sign tool debugging */
/* Must also define DEBUG_WOLFSSL in user_settings.h */
#define WOLFBOOT_HASH_SHA256
#define EXT_FLASH
#define PART_UPDATE_EXT
#define NVM_FLASH_WRITEONCE
#if defined(ENCRYPT_WITH_AES256) || defined(ENCRYPT_WITH_AES128)
#define WOLFSSL_AES_COUNTER
#define WOLFSSL_AES_DIRECT
#endif
#if defined(ENCRYPT_WITH_AES256)
#define WOLFSSL_AES_256
#endif
#if defined(ENCRYPT_WITH_CHACHA)
#define HAVE_CHACHA
#endif
#define ECC_TIMING_RESISTANT
#define KEYSTORE_PUBKEY_SIZE KEYSTORE_PUBKEY_SIZE_ECC256
#include <stdio.h>
#include <check.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define XMALLOC_OVERRIDE
#define XMALLOC(n,h,t) malloc(n)
#define XFREE(p,h,t) free(p)
#include "user_settings.h"
#include "wolfssl/wolfcrypt/sha.h"
#include "wolfssl/wolfcrypt/error-crypt.h"
#include "wolfboot/wolfboot.h"
#include "wolfpkcs11/pkcs11.h"
#include "hal.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#define MOCK_ADDRESS 0xCF000000
uint8_t *vault_base = (uint8_t *)MOCK_ADDRESS;
/* Backing file for the mock keyvault. Made unique per process (see main()) so
* concurrent test runs do not collide on a shared /tmp path, with a usable
* default in case a test is ever driven without main() initializing it. */
char vault_path[64] = "/tmp/wolfboot-unit-keyvault.bin";
#include "unit-keystore.c"
#include "pkcs11_store.c"
const uint32_t keyvault_size = KEYVAULT_OBJ_SIZE * KEYVAULT_MAX_ITEMS + 2 * WOLFBOOT_SECTOR_SIZE;
#include "unit-mock-flash.c"
#include "txt_filler.h"
char dante_filler[KEYVAULT_OBJ_SIZE] = DANTE_FILLER;
START_TEST (test_store_and_load_objs) {
CK_ULONG id_tok, id_obj;
int type;
int ret, readonly;
void *store = NULL;
char secret1[] = "Everyone gets Friday off.";
char secret2[] = "This is just a test string.";
char short_string[] = "Short string";
char secret_rd[KEYVAULT_OBJ_SIZE];
type = DYNAMIC_TYPE_ECC;
id_tok = 1;
id_obj = 12;
readonly = 0;
ret = mmap_file(vault_path, vault_base,
keyvault_size, NULL);
ck_assert(ret == 0);
memset(vault_base, 0xEE, keyvault_size);
/* Open the vault, create the object */
fprintf(stderr, "Opening the vault\n");
printf("Flash Keyvault: %p\n", vault_base);
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to open the vault: %d", ret);
ck_assert_msg(store != NULL, "Did not receive a store address");
fprintf(stderr, "open successful\n");
/* Test two subsequent writes */
ret = wolfPKCS11_Store_Write(store, secret1, strlen(secret1) + 1);
ck_assert_int_eq(ret, strlen(secret1) + 1);
ret = wolfPKCS11_Store_Write(store, secret2, strlen(secret2) + 1);
ck_assert_int_eq(ret, strlen(secret2) + 1);
wolfPKCS11_Store_Close(store);
printf("Closed vault. Reopening in RO mode\n");
/* Reopen for reading */
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault in read-only mode: %d", ret);
/* Read out the content */
ret = wolfPKCS11_Store_Read(store, secret_rd, 128);
ck_assert(ret == strlen(secret1) + strlen(secret2) + 2);
ck_assert(strcmp(secret1, secret_rd) == 0);
ck_assert(strcmp(secret2, secret_rd + 1 + strlen(secret1)) == 0);
wolfPKCS11_Store_Close(store);
/* Create a second object with same Ids, different type*/
type = DYNAMIC_TYPE_RSA;
readonly = 0;
fprintf(stderr, "Opening the second vault\n");
printf("Flash Keyvault: %p\n", vault_base);
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to open the 2nd vault: %d", ret);
ck_assert_msg(store != NULL, "Did not receive a store address for 2nd vault");
fprintf(stderr, "open 2 successful\n");
ret = wolfPKCS11_Store_Write(store, secret2, strlen(secret2) + 1);
wolfPKCS11_Store_Close(store);
/* Reopen for reading */
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault in read-only mode: %d", ret);
/* Read out the content */
ret = wolfPKCS11_Store_Read(store, secret_rd, 128);
ck_assert(ret == strlen(secret2) + 1);
ck_assert(strcmp(secret2, secret_rd) == 0);
wolfPKCS11_Store_Close(store);
/* Create more similar objects, different secret */
type = DYNAMIC_TYPE_RSA;
id_tok = 2;
id_obj = 22;
readonly = 0;
fprintf(stderr, "Creating one more vault\n");
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to create vault: %d", ret);
ck_assert_msg(store != NULL, "Did not receive a store address for vault");
fprintf(stderr, "open 2 successful\n");
ret = wolfPKCS11_Store_Write(store, secret1, strlen(secret1) + 1);
id_tok = 3;
id_obj = 23;
readonly = 0;
fprintf(stderr, "Creating one more vault\n");
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to create vault: %d", ret);
ck_assert_msg(store != NULL, "Did not receive a store address for vault");
fprintf(stderr, "open 2 successful\n");
ret = wolfPKCS11_Store_Write(store, secret1, strlen(secret1) + 1);
wolfPKCS11_Store_Close(store);
/* Reopen for reading */
id_tok = 1;
id_obj = 12;
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault in read-only mode: %d", ret);
/* Read out the content */
ret = wolfPKCS11_Store_Read(store, secret_rd, 128);
ck_assert(ret == strlen(secret2) + 1);
ck_assert(strcmp(secret2, secret_rd) == 0);
wolfPKCS11_Store_Close(store);
/* Open non-existing vaults */
id_tok = 5;
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret != 0, "Returned with success with invalid id_tok %d", id_tok);
id_tok = 2;
id_obj = 0;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret != 0, "Returned with success with invalid id_obj %d", id_obj);
type = 0xFF;
id_tok = 2;
id_obj = 23;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret != 0, "Returned with success with invalid type %d", type);
/* Test backup recovery for allocation table */
memset(vault_base, 0xEE, WOLFBOOT_SECTOR_SIZE);
type = DYNAMIC_TYPE_RSA;
id_tok = 1;
id_obj = 12;
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault recovering from alloc table backup: %d", ret);
/* Read out the content */
ret = wolfPKCS11_Store_Read(store, secret_rd, 128);
ck_assert(ret == strlen(secret2) + 1);
ck_assert(strcmp(secret2, secret_rd) == 0);
wolfPKCS11_Store_Close(store);
/* Test backup recovery for object sector */
printf("Test recovery sector...\n");
memcpy(vault_base + WOLFBOOT_SECTOR_SIZE, vault_base + 0x1800,
WOLFBOOT_SECTOR_SIZE);
memset(vault_base + 0x1800, 0xEE, WOLFBOOT_SECTOR_SIZE);
id_tok = 1;
id_obj = 12;
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault recovering from object sector backup: %d", ret);
/* Read out the content */
ret = wolfPKCS11_Store_Read(store, secret_rd, 128);
ck_assert(ret == strlen(secret2) + 1);
ck_assert(strcmp(secret2, secret_rd) == 0);
wolfPKCS11_Store_Close(store);
/* Test with very large payload */
type = DYNAMIC_TYPE_RSA;
id_tok = 3;
id_obj = 33;
readonly = 0;
fprintf(stderr, "Creating one BIG vault\n");
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to create vault: %d", ret);
ck_assert_msg(store != NULL, "Did not receive a store address for vault");
fprintf(stderr, "open 3.33 successful\n");
ret = wolfPKCS11_Store_Write(store, dante_filler, strlen(dante_filler) + 1);
wolfPKCS11_Store_Close(store);
/* Reopen for reading */
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault in read-only mode: %d", ret);
/* Read out the content */
memset(secret_rd, 0, KEYVAULT_OBJ_SIZE);
ret = wolfPKCS11_Store_Read(store, secret_rd, KEYVAULT_OBJ_SIZE);
ck_assert_int_eq(ret, strlen(dante_filler) + 1);
ck_assert(strncmp(dante_filler, secret_rd, strlen(dante_filler) + 1) == 0);
wolfPKCS11_Store_Close(store);
/* Reopen for writing, test truncate */
readonly = 0;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to create vault: %d", ret);
ck_assert_msg(store != NULL, "Did not receive a store address for vault");
fprintf(stderr, "open 3.33 successful\n");
ret = wolfPKCS11_Store_Write(store, short_string, strlen(short_string) + 1);
wolfPKCS11_Store_Close(store);
/* Reopen for reading */
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault in read-only mode: %d", ret);
/* Read out the content */
memset(secret_rd, 0, KEYVAULT_OBJ_SIZE);
ret = wolfPKCS11_Store_Read(store, secret_rd, KEYVAULT_OBJ_SIZE);
ck_assert(ret == strlen(short_string) + 1);
ck_assert(strcmp(short_string, secret_rd) == 0);
wolfPKCS11_Store_Close(store);
/* Remove the object and confirm it is no longer addressable */
ret = wolfPKCS11_Store_Remove(type, id_tok, id_obj);
ck_assert_msg(ret == 0, "Failed to delete vault: %d", ret);
readonly = 1;
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, readonly, &store);
ck_assert_int_eq(ret, NOT_AVAILABLE_E);
/* Second removal attempt should report the object is already gone */
ret = wolfPKCS11_Store_Remove(type, id_tok, id_obj);
ck_assert_int_eq(ret, NOT_AVAILABLE_E);
}
END_TEST
START_TEST(test_cross_sector_write_preserves_length)
{
const int type = DYNAMIC_TYPE_RSA;
const CK_ULONG id_tok = 7;
const CK_ULONG id_obj = 9;
void *store = NULL;
unsigned char *payload;
struct store_handle *handle;
int ret;
payload = malloc(WOLFBOOT_SECTOR_SIZE);
ck_assert_ptr_nonnull(payload);
for (ret = 0; ret < WOLFBOOT_SECTOR_SIZE; ret++)
payload[ret] = (unsigned char)(ret & 0xFF);
ret = mmap_file(vault_path, vault_base,
keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xEE, keyvault_size);
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 0, &store);
ck_assert_int_eq(ret, 0);
ck_assert_ptr_nonnull(store);
ret = wolfPKCS11_Store_Write(store, payload, WOLFBOOT_SECTOR_SIZE);
ck_assert_int_eq(ret, WOLFBOOT_SECTOR_SIZE);
handle = store;
ck_assert_uint_eq(handle->in_buffer_offset,
2 * sizeof(uint32_t) + WOLFBOOT_SECTOR_SIZE);
/* The size is tracked live in the (cached) header node; the flash
* node is committed when the window is closed. */
ck_assert_uint_eq(
((struct obj_hdr *)(sector0_ptr() + STORE_PRIV_HDR_OFFSET))->size,
2 * sizeof(uint32_t) + WOLFBOOT_SECTOR_SIZE);
wolfPKCS11_Store_Close(store);
/* After the close the committed node must carry the same size */
ck_assert_uint_eq(
((struct obj_hdr *)(vault_base + STORE_PRIV_HDR_OFFSET))->size,
2 * sizeof(uint32_t) + WOLFBOOT_SECTOR_SIZE);
free(payload);
}
END_TEST
START_TEST(test_close_clears_handle_state)
{
const int type = DYNAMIC_TYPE_RSA;
const CK_ULONG id_tok = 17;
const CK_ULONG id_obj = 21;
void *store = NULL;
struct store_handle *handle;
int ret;
ret = mmap_file(vault_path, vault_base,
keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xEE, keyvault_size);
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 0, &store);
ck_assert_int_eq(ret, 0);
ck_assert_ptr_nonnull(store);
handle = store;
ck_assert_ptr_nonnull(handle->buffer);
ck_assert_ptr_nonnull(handle->hdr);
ck_assert_uint_ne(handle->in_buffer_offset, 0);
wolfPKCS11_Store_Close(store);
ck_assert_uint_eq(handle->flags, 0);
ck_assert_uint_eq(handle->pos, 0);
ck_assert_ptr_null(handle->buffer);
ck_assert_ptr_null(handle->hdr);
ck_assert_uint_eq(handle->in_buffer_offset, 0);
}
END_TEST
START_TEST(test_delete_object_ignores_metadata_prefix)
{
const int32_t type = DYNAMIC_TYPE_RSA;
const uint32_t tok_id = VAULT_HEADER_MAGIC;
const uint32_t obj_id = 0x01020308U;
uint32_t *words;
uint8_t bitmap_before[BITMAP_SIZE];
int ret;
ret = mmap_file(vault_path, vault_base,
keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xFF, keyvault_size);
words = (uint32_t *)vault_base;
words[0] = VAULT_HEADER_MAGIC;
words[1] = obj_id;
words[2] = (uint32_t)type;
words[3] = 0;
words[4] = 0;
memcpy(bitmap_before, vault_base + sizeof(uint32_t), BITMAP_SIZE);
delete_object(type, tok_id, obj_id);
ck_assert_mem_eq(vault_base + sizeof(uint32_t), bitmap_before, BITMAP_SIZE);
ck_assert_uint_eq(((uint32_t *)vault_base)[0], VAULT_HEADER_MAGIC);
ck_assert_uint_eq(((uint32_t *)vault_base)[1], obj_id);
}
END_TEST
START_TEST(test_delete_object_corrupted_pos_no_oob)
{
const int32_t type = DYNAMIC_TYPE_RSA;
const uint32_t tok_id = 0x0A0B0C0DU;
const uint32_t obj_id = 0x10203040U;
struct obj_hdr *hdr;
int ret;
ret = mmap_file(vault_path, vault_base,
keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xFF, keyvault_size);
/* Valid header magic and zeroed bitmap so check_vault() accepts the
* sector without restoring/reinitializing it. */
((uint32_t *)vault_base)[0] = VAULT_HEADER_MAGIC;
memset(vault_base + sizeof(uint32_t), 0x00, BITMAP_SIZE);
/* Simulate a power-fault-corrupted node: valid tok/obj/type but the
* 'pos' field was never written and is left as erased flash
* (PKCS11_INVALID_ID). delete_object() must not turn this into an
* out-of-bounds bitmap_put(0xFFFFFFFF, 0). */
hdr = NODES_TABLE;
hdr->token_id = tok_id;
hdr->object_id = obj_id;
hdr->type = type;
hdr->pos = PKCS11_INVALID_ID;
hdr->size = 2 * sizeof(uint32_t);
delete_object(type, tok_id, obj_id);
/* If we get here without a crash, the OOB write was avoided. The node
* should also have been invalidated. */
ck_assert_uint_eq(NODES_TABLE->token_id, PKCS11_INVALID_ID);
ck_assert_uint_eq(NODES_TABLE->object_id, PKCS11_INVALID_ID);
}
END_TEST
START_TEST(test_find_object_search_stops_at_header_sector)
{
const int32_t type = DYNAMIC_TYPE_RSA;
const uint32_t tok_id = 0x11223344U;
const uint32_t obj_id = 0x55667788U;
struct obj_hdr *backup_hdr;
uint32_t *payload_ids;
int ret;
ret = mmap_file(vault_path, vault_base,
keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xFF, keyvault_size);
backup_hdr = (struct obj_hdr *)(vault_base + WOLFBOOT_SECTOR_SIZE);
backup_hdr->token_id = tok_id;
backup_hdr->object_id = obj_id;
backup_hdr->type = type;
backup_hdr->pos = 0;
backup_hdr->size = 2 * sizeof(uint32_t);
payload_ids = (uint32_t *)(vault_base + 2 * WOLFBOOT_SECTOR_SIZE);
payload_ids[0] = tok_id;
payload_ids[1] = obj_id;
ck_assert_ptr_null(find_object_header(type, tok_id, obj_id));
ck_assert_ptr_null(find_object_buffer(type, tok_id, obj_id));
}
END_TEST
/* Prove F-4649: shorter overwrite must not leave prior key bytes in flash.
* Write 512 bytes of 0xAA, then reopen and write 50 bytes of 0xBB.
* Raw flash at offsets [8+50 .. 8+512) must be 0xFF (erased), not 0xAA. */
START_TEST(test_shorter_overwrite_erases_residual_key_material)
{
const int type = DYNAMIC_TYPE_RSA;
const CK_ULONG id_tok = 42;
const CK_ULONG id_obj = 84;
void *store = NULL;
struct store_handle *h;
unsigned char large_key[512];
unsigned char small_key[50];
uint8_t *obj_flash;
uint32_t i;
int ret;
memset(large_key, 0xAA, sizeof(large_key));
memset(small_key, 0xBB, sizeof(small_key));
ret = mmap_file(vault_path, vault_base, keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xEE, keyvault_size);
/* Write large key to a fresh slot */
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 0, &store);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Write(store, large_key, sizeof(large_key));
ck_assert_int_eq(ret, (int)sizeof(large_key));
wolfPKCS11_Store_Close(store);
/* Reopen in write mode and store a shorter key */
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 0, &store);
ck_assert_int_eq(ret, 0);
h = store;
obj_flash = h->buffer;
ret = wolfPKCS11_Store_Write(store, small_key, sizeof(small_key));
ck_assert_int_eq(ret, (int)sizeof(small_key));
wolfPKCS11_Store_Close(store);
/* Flash beyond the new payload must be erased (0xFF), not old 0xAA */
for (i = 2 * sizeof(uint32_t) + sizeof(small_key);
i < 2 * sizeof(uint32_t) + sizeof(large_key); i++) {
ck_assert_msg(obj_flash[i] == 0xFF,
"Residual key material at object offset %u: 0x%02x (expected 0xFF)",
i, obj_flash[i]);
}
}
END_TEST
/* A negative length must be rejected before it enters the unsigned
* offset arithmetic: pre-fix, a sufficiently negative len wrapped to a
* large unsigned value in 'in_buffer_offset + len', entered the
* truncation branch, and was silently replaced by the remaining object
* bytes (Read) or the remaining capacity (Write, bypassing the later
* len < 0 guard). */
START_TEST(test_store_rejects_negative_len)
{
CK_ULONG id_tok = 1;
CK_ULONG id_obj = 42;
int type = DYNAMIC_TYPE_ECC;
int ret;
void *store = NULL;
unsigned char rd[16];
unsigned char wr[16];
ret = mmap_file(vault_path, vault_base, keyvault_size, NULL);
ck_assert(ret == 0);
memset(vault_base, 0xEE, keyvault_size);
/* Create the object with 3 bytes of content */
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 0, &store);
ck_assert_msg(ret == 0, "Failed to open the vault: %d", ret);
ret = wolfPKCS11_Store_Write(store, (unsigned char *)"abc", 3);
ck_assert_int_eq(ret, 3);
wolfPKCS11_Store_Close(store);
/* Read: pre-fix the negative len was replaced by the 3 remaining
* bytes and copied out; post-fix it is rejected. */
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 1, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault: %d", ret);
ret = wolfPKCS11_Store_Read(store, rd, -32768);
ck_assert_int_eq(ret, -1);
wolfPKCS11_Store_Close(store);
/* Write: pre-fix the negative len was clamped to the remaining
* capacity (4088) and read that many bytes from the 16-byte buffer
* below; post-fix it is rejected. */
ret = wolfPKCS11_Store_Open(type, id_tok, id_obj, 0, &store);
ck_assert_msg(ret == 0, "Failed to reopen the vault for write: %d", ret);
ret = wolfPKCS11_Store_Write(store, wr, -32768);
ck_assert_int_eq(ret, -1);
wolfPKCS11_Store_Close(store);
}
END_TEST
/* Removing an object must erase the payload from flash, not just
* invalidate the metadata: key material must not remain recoverable
* after the API reports a successful deletion. */
START_TEST(test_remove_erases_payload_from_flash)
{
const int type = DYNAMIC_TYPE_RSA;
const CK_ULONG id_tok_a = 10;
const CK_ULONG id_obj_a = 20;
const CK_ULONG id_tok_b = 30;
const CK_ULONG id_obj_b = 40;
void *store = NULL;
unsigned char key_a[256];
unsigned char key_b[128];
uint8_t *buf_a;
uint8_t *buf_b;
uint32_t i;
int ret;
memset(key_a, 0xAB, sizeof(key_a));
memset(key_b, 0xCD, sizeof(key_b));
ret = mmap_file(vault_path, vault_base, keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xEE, keyvault_size);
/* Two live objects: A gets slot 0, B gets the adjacent slot 1 */
ret = wolfPKCS11_Store_Open(type, id_tok_a, id_obj_a, 0, &store);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Write(store, key_a, sizeof(key_a));
ck_assert_int_eq(ret, (int)sizeof(key_a));
wolfPKCS11_Store_Close(store);
ret = wolfPKCS11_Store_Open(type, id_tok_b, id_obj_b, 0, &store);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Write(store, key_b, sizeof(key_b));
ck_assert_int_eq(ret, (int)sizeof(key_b));
wolfPKCS11_Store_Close(store);
buf_a = find_object_buffer(type, id_tok_a, id_obj_a);
buf_b = find_object_buffer(type, id_tok_b, id_obj_b);
ck_assert_ptr_nonnull(buf_a);
ck_assert_ptr_nonnull(buf_b);
ck_assert_ptr_eq(buf_a, vault_base + 2 * WOLFBOOT_SECTOR_SIZE);
ck_assert_ptr_eq(buf_b, vault_base + 2 * WOLFBOOT_SECTOR_SIZE +
KEYVAULT_OBJ_SIZE);
/* Remove A: the payload must be erased from the raw flash */
ret = wolfPKCS11_Store_Remove(type, id_tok_a, id_obj_a);
ck_assert_int_eq(ret, 0);
/* The 8-byte id prefix is preserved by the erase (it keeps the slot
* identity used by the backup-recovery check); the payload region
* itself must be erased to 0xFF across the whole slot. */
ck_assert_uint_eq(((uint32_t *)buf_a)[0], (uint32_t)id_tok_a);
ck_assert_uint_eq(((uint32_t *)buf_a)[1], (uint32_t)id_obj_a);
for (i = 2 * sizeof(uint32_t); i < KEYVAULT_OBJ_SIZE; i++) {
ck_assert_msg(buf_a[i] == 0xFF,
"Payload survives removal at slot offset %u: 0x%02x",
i, buf_a[i]);
}
/* The sector read-modify-write must leave the neighboring object B
* intact in flash */
for (i = 2 * sizeof(uint32_t);
i < 2 * sizeof(uint32_t) + sizeof(key_b); i++) {
ck_assert_msg(buf_b[i] == 0xCD,
"Neighbor object clobbered at slot offset %u: 0x%02x",
i, buf_b[i]);
}
ret = wolfPKCS11_Store_Open(type, id_tok_b, id_obj_b, 1, &store);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Read(store, key_b, sizeof(key_b));
ck_assert_int_eq(ret, (int)sizeof(key_b));
wolfPKCS11_Store_Close(store);
/* A is no longer addressable */
ret = wolfPKCS11_Store_Open(type, id_tok_a, id_obj_a, 1, &store);
ck_assert_int_eq(ret, NOT_AVAILABLE_E);
ret = wolfPKCS11_Store_Remove(type, id_tok_a, id_obj_a);
ck_assert_int_eq(ret, NOT_AVAILABLE_E);
}
END_TEST
/* A second write window opened while the first is still open must not
* discard the first window's pending writes: both objects survive. */
START_TEST(test_interleaved_write_windows_both_persist)
{
const int type = DYNAMIC_TYPE_RSA;
const CK_ULONG id_tok = 60;
void *store_a = NULL;
void *store_b = NULL;
void *store = NULL;
char first[] = "first window payload";
char second[] = "second window payload";
char rd[64];
int ret;
ret = mmap_file(vault_path, vault_base, keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xEE, keyvault_size);
/* Window A: open + write, left open (dirty sector cache). */
ret = wolfPKCS11_Store_Open(type, id_tok, 1, 0, &store_a);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Write(store_a, first, (int)strlen(first) + 1);
ck_assert_int_eq(ret, (int)strlen(first) + 1);
/* Window B while A is still open: the open re-validates the vault
* and must commit A's batch, not drop it. */
ret = wolfPKCS11_Store_Open(type, id_tok, 2, 0, &store_b);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Write(store_b, second, (int)strlen(second) + 1);
ck_assert_int_eq(ret, (int)strlen(second) + 1);
wolfPKCS11_Store_Close(store_b);
wolfPKCS11_Store_Close(store_a);
ret = wolfPKCS11_Store_Open(type, id_tok, 1, 1, &store);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Read(store, rd, (int)sizeof(rd));
ck_assert_int_eq(ret, (int)strlen(first) + 1);
ck_assert(strcmp(first, rd) == 0);
wolfPKCS11_Store_Close(store);
ret = wolfPKCS11_Store_Open(type, id_tok, 2, 1, &store);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Read(store, rd, (int)sizeof(rd));
ck_assert_int_eq(ret, (int)strlen(second) + 1);
ck_assert(strcmp(second, rd) == 0);
wolfPKCS11_Store_Close(store);
}
END_TEST
/* A reader opened on the same object while a write window holds it must
* see that window's writes, not a NOT_AVAILABLE error or erased flash.
* The first write is committed by the reader's own vault validation; the
* second is issued after the reader is open, so it sits only in the sector
* cache and must be read back through the cache (and the live header
* size), not from the pre-write flash. */
START_TEST(test_concurrent_reader_sees_pending_writes)
{
const int type = DYNAMIC_TYPE_RSA;
const CK_ULONG id_tok = 70;
void *store_w = NULL;
void *store_r = NULL;
char secret[] = "pending write";
char more[] = " more";
char rd[64];
int ret;
ret = mmap_file(vault_path, vault_base, keyvault_size, NULL);
ck_assert_int_eq(ret, 0);
memset(vault_base, 0xEE, keyvault_size);
ret = wolfPKCS11_Store_Open(type, id_tok, 1, 0, &store_w);
ck_assert_int_eq(ret, 0);
ret = wolfPKCS11_Store_Write(store_w, secret, (int)strlen(secret) + 1);
ck_assert_int_eq(ret, (int)strlen(secret) + 1);
/* The write is still pending in the write window. A concurrent
* reader on the same object must observe it, not erased flash. */
ret = wolfPKCS11_Store_Open(type, id_tok, 1, 1, &store_r);
ck_assert_int_eq(ret, 0);
memset(rd, 0, sizeof(rd));
ret = wolfPKCS11_Store_Read(store_r, rd, (int)sizeof(rd));
ck_assert_int_eq(ret, (int)strlen(secret) + 1);
ck_assert(strcmp(secret, rd) == 0);
/* Write more on the still-open writer: this lands only in the sector
* cache (the reader's open already flushed the earlier batch to
* flash). The reader must see it through the cache and the live
* header size; a flash-only or snapshot-size read returns EOF here. */
ret = wolfPKCS11_Store_Write(store_w, more, (int)strlen(more));
ck_assert_int_eq(ret, (int)strlen(more));
memset(rd, 0, sizeof(rd));
ret = wolfPKCS11_Store_Read(store_r, rd, (int)sizeof(rd));
ck_assert_int_eq(ret, (int)strlen(more));
ck_assert(memcmp(more, rd, strlen(more)) == 0);
wolfPKCS11_Store_Close(store_r);
wolfPKCS11_Store_Close(store_w);
}
END_TEST
/* A power cycle loses every byte of RAM state the store keeps: the sector
* cache and the open-handle table. Flash content survives. */
static void vault_power_cycle(void)
{
memset(store_cache, 0, sizeof(store_cache));
memset(cache_sector_mem, 0, sizeof(cache_sector_mem));
memset(openstores_handles, 0, sizeof(openstores_handles));
cache_lru_tick = 0;
locked = 1;
/* A reboot also drops any CPU-side cache of flash. */
hal_cache_invalidate();
}
static int vault_obj_write(int type, CK_ULONG tok, CK_ULONG obj,
const uint8_t *payload, int len)
{
void *store = NULL;
int ret = wolfPKCS11_Store_Open(type, tok, obj, 0, &store);
if (ret != 0)
return ret;
ret = wolfPKCS11_Store_Write(store, (unsigned char *)payload, len);
wolfPKCS11_Store_Close(store);
return ret;
}
static int vault_obj_read(int type, CK_ULONG tok, CK_ULONG obj,
uint8_t *out, int max)
{
void *store = NULL;
int ret = wolfPKCS11_Store_Open(type, tok, obj, 1, &store);
if (ret != 0)
return -1;
ret = wolfPKCS11_Store_Read(store, out, max);
wolfPKCS11_Store_Close(store);
return ret;
}
/* Rewriting an existing object destroys its committed payload. Whatever the
* moment power is lost inside the Open/Write/Close window, the next boot must
* read the object back as the complete old payload, the complete new payload,
* or empty - never a mix of old, new and erased bytes.
*
* That is what the Open-time commit of the truncated header (size = 8) buys:
* without it the previous generation's size stays committed over a payload
* that is being erased and rewritten underneath it. This test drives a power
* failure at every single flash operation of the window to pin the property.
*/
START_TEST (test_power_fail_during_rewrite_never_mixes_generations) {
static uint8_t old_p[2000], new_p[300], rd[KEYVAULT_OBJ_SIZE];
static uint8_t snapshot[KEYVAULT_OBJ_SIZE * KEYVAULT_MAX_ITEMS +
2 * WOLFBOOT_SECTOR_SIZE];
const int type = DYNAMIC_TYPE_ECC;
const CK_ULONG tok = 7, obj = 77;
int i, ret, ops, crash;
for (i = 0; i < (int)sizeof(old_p); i++)
old_p[i] = (uint8_t)('A' + (i % 23));
for (i = 0; i < (int)sizeof(new_p); i++)
new_p[i] = (uint8_t)('a' + (i % 19));
ret = mmap_file(vault_path, vault_base, keyvault_size, NULL);
ck_assert(ret == 0);
memset(vault_base, 0xEE, keyvault_size);
/* Lay down the previous generation, no faults. */
vault_power_cycle();
vault_powerfail_at = -1;
ret = vault_obj_write(type, tok, obj, old_p, (int)sizeof(old_p));
ck_assert_int_eq(ret, (int)sizeof(old_p));
memcpy(snapshot, vault_base, keyvault_size);
/* Count the flash operations a clean rewrite takes. */
vault_power_cycle();
vault_flash_ops = 0;
vault_powerfail_at = -1;
vault_obj_write(type, tok, obj, new_p, (int)sizeof(new_p));
ops = vault_flash_ops;
ck_assert_int_gt(ops, 0);
for (crash = 0; crash <= ops; crash++) {
memcpy(vault_base, snapshot, keyvault_size);
vault_power_cycle();
vault_flash_ops = 0;
vault_powerfail_at = crash;
if (setjmp(vault_powerfail_jmp) == 0) {
vault_obj_write(type, tok, obj, new_p, (int)sizeof(new_p));
}
/* Power returns. */
vault_powerfail_at = -1;
vault_power_cycle();
memset(rd, 0, sizeof(rd));
ret = vault_obj_read(type, tok, obj, rd, (int)sizeof(rd));
if (ret == (int)sizeof(old_p)) {
ck_assert_msg(memcmp(rd, old_p, sizeof(old_p)) == 0,
"power fail at op %d: old-sized payload is not the old "
"payload", crash);
}
else if (ret == (int)sizeof(new_p)) {
ck_assert_msg(memcmp(rd, new_p, sizeof(new_p)) == 0,
"power fail at op %d: new-sized payload is not the new "
"payload", crash);
}
else {
ck_assert_msg(ret <= 0,
"power fail at op %d: object read back %d bytes, neither "
"generation nor empty", crash, ret);
}
}
}
END_TEST
Suite *wolfboot_suite(void)
{
/* Suite initialization */
Suite *s = suite_create("wolfBoot-pkcs11-store");
TCase* tcase_store_and_load_objs = tcase_create("store_and_load_objs");
TCase* tcase_cross_sector_write = tcase_create("cross_sector_write");
TCase* tcase_close = tcase_create("close_state");
TCase* tcase_delete_object = tcase_create("delete_object");
TCase* tcase_delete_corrupted = tcase_create("delete_corrupted_pos");
TCase* tcase_find_bounds = tcase_create("find_bounds");
TCase* tcase_remanence = tcase_create("shorter_overwrite_erases_residual");
TCase* tcase_neg_len = tcase_create("rejects_negative_len");
TCase* tcase_remove_erase = tcase_create("remove_erases_payload");
TCase* tcase_interleaved = tcase_create("interleaved_windows");
TCase* tcase_concurrent_read = tcase_create("concurrent_reader");
TCase* tcase_power_fail = tcase_create("power_fail_rewrite");
tcase_add_test(tcase_store_and_load_objs, test_store_and_load_objs);
tcase_add_test(tcase_cross_sector_write, test_cross_sector_write_preserves_length);
tcase_add_test(tcase_close, test_close_clears_handle_state);
tcase_add_test(tcase_delete_object, test_delete_object_ignores_metadata_prefix);
tcase_add_test(tcase_delete_corrupted, test_delete_object_corrupted_pos_no_oob);
tcase_add_test(tcase_find_bounds, test_find_object_search_stops_at_header_sector);
tcase_add_test(tcase_remanence, test_shorter_overwrite_erases_residual_key_material);
tcase_add_test(tcase_neg_len, test_store_rejects_negative_len);
tcase_add_test(tcase_remove_erase, test_remove_erases_payload_from_flash);
tcase_add_test(tcase_interleaved, test_interleaved_write_windows_both_persist);
tcase_add_test(tcase_concurrent_read, test_concurrent_reader_sees_pending_writes);
tcase_add_test(tcase_power_fail,
test_power_fail_during_rewrite_never_mixes_generations);
suite_add_tcase(s, tcase_store_and_load_objs);
suite_add_tcase(s, tcase_cross_sector_write);
suite_add_tcase(s, tcase_close);
suite_add_tcase(s, tcase_delete_object);
suite_add_tcase(s, tcase_delete_corrupted);
suite_add_tcase(s, tcase_find_bounds);
suite_add_tcase(s, tcase_remanence);
suite_add_tcase(s, tcase_neg_len);
suite_add_tcase(s, tcase_remove_erase);
suite_add_tcase(s, tcase_interleaved);
suite_add_tcase(s, tcase_concurrent_read);
suite_add_tcase(s, tcase_power_fail);
return s;
}
int main(void)
{
int fails;
Suite *s;
SRunner *sr;
/* Use a per-process backing file so parallel test runs (or a stale file
* from a previous run) cannot collide on a shared /tmp path. */
snprintf(vault_path, sizeof(vault_path),
"/tmp/wolfboot-unit-keyvault-%d.bin", (int)getpid());
s = wolfboot_suite();
sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
unlink(vault_path);
return fails;
}