diff --git a/docs/Security-Model.md b/docs/Security-Model.md index c48d2525..e77045f7 100644 --- a/docs/Security-Model.md +++ b/docs/Security-Model.md @@ -154,6 +154,19 @@ interrupted write. The live per-object counter also detects replay of a stale ciphertext unless an attacker can coherently roll back the counter store; see [Threat Model](Threat-Model.md). +Sealed replacement stages the authenticated prior object until the new +counter mapping commits, then destroys the stage. An interrupted replacement +restores that object +without rolling back the global nonce counter. Replacing sealed data with +unsealed data uses the same transaction and retires the old seal counter on +commit. If the recovery copy is missing or invalid, the live object is kept +only if it authenticates under the committed counter; otherwise that object +is discarded and its counter retired. Other objects remain available. +The vault requires wolfHSM's +`wh_NvmFlash` backend, including its capacity and compaction callbacks, and +rejects incompatible backends at initialization. The flash HAL may be supplied +by the target port or the host RAM simulator. + Vault storage rejects its reserved key-object type. Guest cryptographic keys instead use the separate wolfHSM keystore behind `SERVICE_HSM`, where the relay binds operations to the caller's namespace. diff --git a/docs/TF-M-Compatibility.md b/docs/TF-M-Compatibility.md index f54fd6c7..a40099e5 100644 --- a/docs/TF-M-Compatibility.md +++ b/docs/TF-M-Compatibility.md @@ -34,7 +34,7 @@ algorithm or feature is enabled in every build. | wolfTrust exposes the FF-M Non-secure client API through one five-function CMSE gateway. | Implementation detail | The gateway exports framework version, service version, connect, call, and close; Secure Partition entry points are a separate manifest concern. | | Only connection-based IPC services are enabled. | Scoped | The production manifest requests only `WT_MANIFEST_FEATURE_IPC`. SFN, stateless services, and memory-mapped I/O vectors are rejected by the runtime. | | IPC uses fixed copied buffers. | Scoped safety bound | Calls are limited to four vectors and a 1024-byte transfer budget. Services apply smaller bounds. Applications must chunk larger data. | -| A Non-secure connect, call, or close cannot remain pending after the scheduler reaches quiescence for that dispatch. | Scoped deviation | An incomplete message becomes an error. Shipped Non-secure-facing services reply within the dispatch; Secure Partition callers use the begin/finish path when scheduling another partition is required. | +| A Non-secure connect, call, or close cannot remain pending after the scheduler reaches quiescence for that dispatch. | Scoped deviation | An incomplete message becomes a client error. If already claimed, the service retains its message until reply or partition fault; late output is discarded. A late call reply releases the message, but the connection remains in error until the client requests close. If close was already requested, the late reply allows deferred disconnection to proceed. Shipped Non-secure-facing services reply within the dispatch; Secure Partition callers use the begin/finish path when scheduling another partition is required. | | Secure services are linked in one image. | Scoped isolation difference | Service writable state is isolated by unprivileged threads and Secure MPU domains, but executable text is shared rather than separately linked. | | All shipped service loops run as scheduled coroutines. | Implementation difference | Service code uses the standard Secure Partition API; privileged hardware access goes through identity-pinned SVC gates. | | Abnormal Non-secure guest termination reclaims the guest's connections without delivering `PSA_IPC_DISCONNECT` to the affected services. | Scoped deviation | Fault-handler cleanup cannot dispatch a Secure service inline without re-entering the scheduler. Shipped services do not use `psa_set_rhandle()` for per-connection cleanup, but a ported service that depends on disconnect cleanup must account for this behavior. | diff --git a/include/wolftrust/ffm.h b/include/wolftrust/ffm.h index 581b25a1..543ec4da 100644 --- a/include/wolftrust/ffm.h +++ b/include/wolftrust/ffm.h @@ -103,6 +103,7 @@ typedef struct wt_ffm_message_runtime { uint8_t allocated; uint8_t active; uint8_t complete; + uint8_t abandoned; /* Client returned; retain ownership until service reply. */ } wt_ffm_message_runtime_t; struct wt_ffm_runtime { diff --git a/include/wolftrust/services/hsm.h b/include/wolftrust/services/hsm.h index bca47d10..d7728b4b 100644 --- a/include/wolftrust/services/hsm.h +++ b/include/wolftrust/services/hsm.h @@ -160,7 +160,8 @@ int wt_hsm_attest_public_key(uint8_t* publicKey, size_t publicKeyCapacity, /* Gated vault backing (WT-FFM-0047): bind the shared NVM context, then * install wt_hsm_vault_backend into SERVICE_VAULT. wt_hsm_init does both; - * host tests may bind their own (e.g. ramsim-backed) context directly. */ + * requires wh_NvmFlash capacity/compaction callbacks. Host tests may use + * that backend over ramsim. Other NVM backends are rejected with -1. */ struct whNvmContext_t; int wt_hsm_vault_init(struct whNvmContext_t* nvm); struct wt_vault_backend; diff --git a/src/ffm.c b/src/ffm.c index 1971b25e..d5e92a58 100644 --- a/src/ffm.c +++ b/src/ffm.c @@ -29,6 +29,8 @@ #define WT_FFM_HANDLE_GEN_MASK 0x00FFFFFFU #define WT_FFM_HANDLE_CONNECTION 1U #define WT_FFM_HANDLE_MESSAGE 2U +#define WT_FFM_ABANDONED_REPLY 1U +#define WT_FFM_ABANDONED_CLOSE 2U static uint32_t wt_ffm_next_generation(uint32_t generation) { @@ -277,6 +279,25 @@ static int wt_ffm_message_from_handle(wt_ffm_runtime_t* runtime, return WT_FFM_SUCCESS; } +static int wt_ffm_close_abandoned(wt_ffm_runtime_t* runtime, + uint16_t connection_index) +{ + size_t i; + + for (i = 0U; i < WT_FFM_MAX_MESSAGES; i++) { + wt_ffm_message_runtime_t* message = &runtime->messages[i]; + + if (message->allocated != 0U && message->abandoned != 0U && + message->connection_index == connection_index) { + message->abandoned = WT_FFM_ABANDONED_CLOSE; + runtime->connections[connection_index].state = + WT_IPC_CONNECTION_DISCONNECTING; + return 1; + } + } + return 0; +} + static void wt_ffm_update_service_signal(wt_ffm_runtime_t* runtime, uint16_t service_index) { @@ -651,6 +672,12 @@ psa_handle_t wt_ffm_connect(wt_ffm_runtime_t* runtime, wt_ffm_enqueue(runtime, service_index, message_index); ret = wt_ffm_dispatch_message(runtime, message_index); status = message->reply_status; + if (ret != WT_FFM_SUCCESS && message->active != 0U) { + message->abandoned = WT_FFM_ABANDONED_CLOSE; + return ret == WT_FFM_ERROR_RESOURCE ? + (psa_handle_t)PSA_ERROR_CONNECTION_BUSY : + (psa_handle_t)PSA_ERROR_GENERIC_ERROR; + } if (ret != WT_FFM_SUCCESS) { /* A refused CONNECT dispatch must unlink the message before its slot * is released, or the slot aliases the next queued request. */ @@ -747,6 +774,13 @@ psa_status_t wt_ffm_call(wt_ffm_runtime_t* runtime, ret = wt_ffm_dispatch_message(runtime, message_index); if (ret != WT_FFM_SUCCESS) { connection->state = WT_IPC_CONNECTION_ERROR; + if (message->active != 0U) { + connection->error_latch = 1U; + message->abandoned = WT_FFM_ABANDONED_REPLY; + (void)memset(message->client_output, 0, + sizeof(message->client_output)); + return PSA_ERROR_GENERIC_ERROR; + } wt_ffm_dequeue_message(runtime, connection->service_index, message_index); wt_ffm_release_message(runtime, message_index); @@ -823,6 +857,8 @@ int wt_ffm_close(wt_ffm_runtime_t* runtime, psa_client_id_t caller, if (connection->state != WT_IPC_CONNECTION_IDLE && connection->state != WT_IPC_CONNECTION_ERROR) return WT_FFM_ERROR_STATE; + if (wt_ffm_close_abandoned(runtime, connection_index) != 0) + return WT_FFM_SUCCESS; if (wt_ffm_alloc_message(runtime, &message_index) != WT_FFM_SUCCESS) return WT_FFM_ERROR_RESOURCE; @@ -834,6 +870,10 @@ int wt_ffm_close(wt_ffm_runtime_t* runtime, psa_client_id_t caller, message->type = PSA_IPC_DISCONNECT; wt_ffm_enqueue(runtime, connection->service_index, message_index); ret = wt_ffm_dispatch_message(runtime, message_index); + if (ret != WT_FFM_SUCCESS && message->active != 0U) { + message->abandoned = WT_FFM_ABANDONED_CLOSE; + return ret; + } if (ret != WT_FFM_SUCCESS) { wt_ffm_dequeue_message(runtime, connection->service_index, message_index); @@ -988,6 +1028,10 @@ int wt_ffm_close_begin(wt_ffm_runtime_t* runtime, psa_client_id_t caller, if (connection->state != WT_IPC_CONNECTION_IDLE && connection->state != WT_IPC_CONNECTION_ERROR) return WT_FFM_ERROR_STATE; + if (wt_ffm_close_abandoned(runtime, connection_index) != 0) { + *msg_index = WT_FFM_QUEUE_NONE; + return WT_FFM_SUCCESS; + } if (wt_ffm_alloc_message(runtime, &message_index) != WT_FFM_SUCCESS) return WT_FFM_ERROR_RESOURCE; @@ -1048,6 +1092,11 @@ int wt_ffm_fail_partition_messages(wt_ffm_runtime_t* runtime, message->complete = 1U; runtime->connections[message->connection_index].state = WT_IPC_CONNECTION_ERROR; + if (message->abandoned != 0U) { + if (message->abandoned == WT_FFM_ABANDONED_CLOSE) + wt_ffm_release_connection(runtime, message->connection_index); + wt_ffm_release_message(runtime, (uint16_t)i); + } failed++; } @@ -1575,6 +1624,35 @@ int wt_ffm_write(wt_ffm_runtime_t* runtime, int32_t partition_id, return WT_FFM_SUCCESS; } +static void wt_ffm_finish_abandoned(wt_ffm_runtime_t* runtime, + uint16_t message_index) +{ + wt_ffm_message_runtime_t* message = &runtime->messages[message_index]; + uint16_t connection_index = message->connection_index; + wt_ffm_connection_runtime_t* connection = + &runtime->connections[connection_index]; + int disconnect; + + disconnect = (message->type == PSA_IPC_CONNECT && + message->reply_status == PSA_SUCCESS) || + (message->type >= PSA_IPC_CALL && + message->abandoned == WT_FFM_ABANDONED_CLOSE); + if (!disconnect && message->type < PSA_IPC_CALL) + wt_ffm_release_connection(runtime, connection_index); + wt_ffm_release_message(runtime, message_index); + if (disconnect) { + /* Reuse the replied slot so cleanup cannot fail on pool exhaustion. */ + message->allocated = 1U; + message->abandoned = WT_FFM_ABANDONED_CLOSE; + message->caller = connection->caller; + message->connection_index = connection_index; + message->service_index = connection->service_index; + message->type = PSA_IPC_DISCONNECT; + connection->state = WT_IPC_CONNECTION_DISCONNECTING; + wt_ffm_enqueue(runtime, message->service_index, message_index); + } +} + int wt_ffm_reply(wt_ffm_runtime_t* runtime, int32_t partition_id, psa_handle_t msg_handle, psa_status_t status) { @@ -1622,5 +1700,7 @@ int wt_ffm_reply(wt_ffm_runtime_t* runtime, int32_t partition_id, message->reply_status = status; message->active = 0U; message->complete = 1U; + if (message->abandoned != 0U) + wt_ffm_finish_abandoned(runtime, message_index); return WT_FFM_SUCCESS; } diff --git a/src/monitor.c b/src/monitor.c index bdc85ef8..8c9c4080 100644 --- a/src/monitor.c +++ b/src/monitor.c @@ -256,19 +256,19 @@ static void wt_tick_restart_backoff(void) if (runtime->remaining_delay_ticks > 0U) { runtime->remaining_delay_ticks--; - if (runtime->remaining_delay_ticks == 0U && - runtime->state == WT_GUEST_RESTARTING) { - /* A relaunch is a launch: the image must still match its pin - * before the domain is re-entered. */ - if (wt_verify_guest_launch((wt_guest_id_t)i) == - WT_GUEST_VERIFY_OK) { - wt_partition_reset_runtime(&g_scheduler.configs[i], - runtime); - } - else { - runtime->state = WT_GUEST_FAULTED; - g_wt_quarantine_events++; - } + } + if (runtime->remaining_delay_ticks == 0U && + runtime->state == WT_GUEST_RESTARTING) { + /* A relaunch is a launch: the image must still match its pin + * before the domain is re-entered. */ + if (wt_verify_guest_launch((wt_guest_id_t)i) == + WT_GUEST_VERIFY_OK) { + wt_partition_reset_runtime(&g_scheduler.configs[i], + runtime); + } + else { + runtime->state = WT_GUEST_FAULTED; + g_wt_quarantine_events++; } } } diff --git a/src/services/wolfhsm/wt_hsm_vault.c b/src/services/wolfhsm/wt_hsm_vault.c index ef0921a9..d8d7d648 100644 --- a/src/services/wolfhsm/wt_hsm_vault.c +++ b/src/services/wolfhsm/wt_hsm_vault.c @@ -34,6 +34,7 @@ #include "wolfhsm/wh_error.h" #include "wolfhsm/wh_common.h" #include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_nvm_flash.h" #include "wolfhsm/wh_flash_unit.h" /* Vault NVM id window: plain-NVM id space (type nibble 0), disjoint from the @@ -43,6 +44,7 @@ #define WT_HSM_VAULT_LABEL_MAGIC 0x31565457UL /* "WTV1" little-endian */ #define WT_HSM_VAULT_TABLE_MAGIC 0x43565457UL /* "WTVC" little-endian */ +#define WT_HSM_VAULT_STAGE_ID 0x0123U #define WT_HSM_VAULT_FLAG_MASK \ (WT_VAULT_FLAG_WRITE_ONCE | WT_VAULT_FLAG_NO_CONFIDENTIALITY | \ @@ -52,7 +54,8 @@ * global increments on every sealed write and is persisted BEFORE the sealed * object, so a power loss can never make a GCM nonce repeat; slot[] holds the * counter each in-window object was sealed under, so a replayed (rolled-back) - * ciphertext fails tag authentication on the next read. */ + * ciphertext fails tag authentication on the next read. reserved holds the + * slot plus one while its prior object is staged for rollback. */ typedef struct wt_hsm_vault_table { uint32_t magic; uint32_t reserved; @@ -69,7 +72,11 @@ static uint8_t g_vault_pt[WT_VAULT_OBJECT_MAX]; int wt_hsm_vault_init(whNvmContext* nvm) { - if (nvm == NULL) { + /* Reservations require physical append slots and flash compaction. */ + if (nvm == NULL || nvm->cb == NULL || + nvm->cb->GetAvailable != wh_NvmFlash_GetAvailable || + nvm->cb->DestroyObjects != wh_NvmFlash_DestroyObjects) { + g_vault_nvm = NULL; return -1; } g_vault_nvm = nvm; @@ -91,6 +98,9 @@ static void wt_hsm_vault_zeroize(uint8_t* buf, size_t len) } } +static psa_status_t wt_hsm_vault_map_err(int rc); +static psa_status_t wt_hsm_vault_recover(wt_hsm_vault_table_t* table); + static psa_status_t wt_hsm_vault_table_load(wt_hsm_vault_table_t* table) { whNvmMetadata meta; @@ -100,46 +110,48 @@ static psa_status_t wt_hsm_vault_table_load(wt_hsm_vault_table_t* table) if (rc == WH_ERROR_NOTFOUND) { (void)memset(table, 0, sizeof(*table)); table->magic = WT_HSM_VAULT_TABLE_MAGIC; - return PSA_SUCCESS; + return wt_hsm_vault_recover(table); } if (rc != WH_ERROR_OK || meta.len != sizeof(*table)) { return PSA_ERROR_STORAGE_FAILURE; } rc = wh_Nvm_Read(g_vault_nvm, WT_HSM_VAULT_TABLE_ID, 0U, (whNvmSize)sizeof(*table), (uint8_t*)table); - if (rc != WH_ERROR_OK || table->magic != WT_HSM_VAULT_TABLE_MAGIC) { + if (rc != WH_ERROR_OK || table->magic != WT_HSM_VAULT_TABLE_MAGIC || + table->reserved > WT_HSM_VAULT_ID_COUNT) { return PSA_ERROR_STORAGE_FAILURE; } - return PSA_SUCCESS; + return wt_hsm_vault_recover(table); } -static psa_status_t wt_hsm_vault_map_err(int rc); - /* Gate every pool write: a doomed add on a full data pool fails mid-write * with NOTBLANK and poisons later adds, so compact reclaimable space when * that frees enough and otherwise report INSUFFICIENT_STORAGE before any - * write starts. Object adds pass the counter table as headroom so the pool - * can never fill past the point where a sealed REMOVE's table rewrite — - * the operation that frees space — still fits. */ -static psa_status_t wt_hsm_vault_reserve(whNvmSize len, whNvmSize headroom) + * write starts. Transaction callers include every write needed to commit or + * roll back so recovery cannot run out of space. */ +static uint32_t wt_hsm_vault_storage_size(size_t len) +{ + return (uint32_t)(WHFU_BYTES2UNITS(len) * WHFU_BYTES_PER_UNIT); +} + +static psa_status_t wt_hsm_vault_reserve(uint32_t need_size, + uint16_t need_objects) { uint32_t avail_size; uint32_t reclaim_size; - uint32_t need_size; uint16_t avail_objects; uint16_t reclaim_objects; int rc; - need_size = (uint32_t)(WHFU_BYTES2UNITS(len) * WHFU_BYTES_PER_UNIT) + - (uint32_t)(WHFU_BYTES2UNITS(headroom) * WHFU_BYTES_PER_UNIT); rc = wh_Nvm_GetAvailable(g_vault_nvm, &avail_size, &avail_objects, &reclaim_size, &reclaim_objects); if (rc != WH_ERROR_OK) { return wt_hsm_vault_map_err(rc); } - if (avail_size < need_size || avail_objects == 0U) { + if (avail_size < need_size || avail_objects < need_objects) { if (avail_size + reclaim_size >= need_size && - (uint32_t)avail_objects + (uint32_t)reclaim_objects > 0U) { + (uint32_t)avail_objects + (uint32_t)reclaim_objects >= + need_objects) { rc = wh_Nvm_DestroyObjects(g_vault_nvm, 0U, NULL); if (rc != WH_ERROR_OK) { return wt_hsm_vault_map_err(rc); @@ -158,7 +170,8 @@ static psa_status_t wt_hsm_vault_table_store(const wt_hsm_vault_table_t* table) psa_status_t status; int rc; - status = wt_hsm_vault_reserve((whNvmSize)sizeof(*table), 0U); + status = wt_hsm_vault_reserve( + wt_hsm_vault_storage_size(sizeof(*table)), 1U); if (status != PSA_SUCCESS) { return status; } @@ -209,6 +222,152 @@ uint32_t wt_hsm_vault_flags_of(const uint8_t* label) return flags; } +static psa_status_t wt_hsm_vault_read_sealed(whNvmId id, + const whNvmMetadata* meta, + uint64_t counter) +{ + size_t pt_len; + psa_status_t status; + + if (g_vault_sealer == NULL) { + return PSA_ERROR_NOT_SUPPORTED; + } + if (counter == 0U || + (wt_hsm_vault_flags_of(meta->label) & + WT_VAULT_FLAG_SEALED) == 0U || + meta->len < WT_VAULT_SEAL_TAG_LEN || + meta->len > sizeof(g_vault_ct)) { + return PSA_ERROR_INVALID_SIGNATURE; + } + status = wt_hsm_vault_map_err( + wh_Nvm_ReadChecked(g_vault_nvm, id, 0U, meta->len, g_vault_ct)); + if (status != PSA_SUCCESS) { + return status; + } + pt_len = (size_t)meta->len - WT_VAULT_SEAL_TAG_LEN; + status = g_vault_sealer->unseal(meta->label, WH_NVM_LABEL_LEN, counter, + g_vault_ct, meta->len, g_vault_pt); + if (status != PSA_SUCCESS) { + wt_hsm_vault_zeroize(g_vault_pt, pt_len); + } + return status; +} + +static psa_status_t wt_hsm_vault_destroy_stage(void) +{ + whNvmMetadata meta; + whNvmId id = WT_HSM_VAULT_STAGE_ID; + int rc; + + rc = wh_Nvm_GetMetadata(g_vault_nvm, id, &meta); + if (rc == WH_ERROR_NOTFOUND) { + return PSA_SUCCESS; + } + if (rc != WH_ERROR_OK) { + return PSA_ERROR_STORAGE_FAILURE; + } + return wt_hsm_vault_map_err( + wh_Nvm_DestroyObjects(g_vault_nvm, 1U, &id)); +} + +static psa_status_t wt_hsm_vault_recover_live(wt_hsm_vault_table_t* table) +{ + whNvmMetadata meta; + uint32_t slot = table->reserved - 1U; + whNvmId id = (whNvmId)(WT_HSM_VAULT_ID_BASE + slot); + psa_status_t status = PSA_ERROR_DOES_NOT_EXIST; + int rc; + + rc = wh_Nvm_GetMetadata(g_vault_nvm, id, &meta); + if (rc == WH_ERROR_OK) { + status = wt_hsm_vault_read_sealed(id, &meta, table->slot[slot]); + } + else if (rc != WH_ERROR_NOTFOUND) { + return PSA_ERROR_STORAGE_FAILURE; + } + if (status == PSA_SUCCESS) { + wt_hsm_vault_zeroize(g_vault_pt, + (size_t)meta.len - WT_VAULT_SEAL_TAG_LEN); + } + else if (status == PSA_ERROR_INVALID_SIGNATURE || + status == PSA_ERROR_NOT_PERMITTED || + status == PSA_ERROR_DOES_NOT_EXIST) { + if (rc == WH_ERROR_OK) { + /* The uncommitted replacement may carry WRITE_ONCE. */ + status = wt_hsm_vault_map_err( + wh_Nvm_DestroyObjects(g_vault_nvm, 1U, &id)); + if (status != PSA_SUCCESS) { + return status; + } + } + table->slot[slot] = 0U; + } + else { + return status; + } + table->reserved = 0U; + status = wt_hsm_vault_table_store(table); + if (status != PSA_SUCCESS) { + return status; + } + return wt_hsm_vault_destroy_stage(); +} + +static psa_status_t wt_hsm_vault_recover(wt_hsm_vault_table_t* table) +{ + whNvmMetadata meta; + whNvmMetadata stage_meta; + whNvmId id; + uint32_t slot; + size_t pt_len; + psa_status_t status; + + if (table->reserved == 0U) { + return wt_hsm_vault_destroy_stage(); + } + slot = table->reserved - 1U; + id = (whNvmId)(WT_HSM_VAULT_ID_BASE + slot); + status = wt_hsm_vault_map_err( + wh_Nvm_GetMetadata(g_vault_nvm, WT_HSM_VAULT_STAGE_ID, &stage_meta)); + if (status == PSA_SUCCESS) { + /* Live metadata may be torn; the committed counter binds the stage. */ + status = wt_hsm_vault_read_sealed(WT_HSM_VAULT_STAGE_ID, + &stage_meta, table->slot[slot]); + } + if (status == PSA_ERROR_DOES_NOT_EXIST || + status == PSA_ERROR_NOT_PERMITTED || + status == PSA_ERROR_INVALID_SIGNATURE) { + return wt_hsm_vault_recover_live(table); + } + if (status != PSA_SUCCESS) { + return status; + } + pt_len = (size_t)stage_meta.len - WT_VAULT_SEAL_TAG_LEN; + wt_hsm_vault_zeroize(g_vault_pt, pt_len); + meta = stage_meta; + meta.id = id; + meta.flags = WH_NVM_FLAGS_SENSITIVE; + status = wt_hsm_vault_reserve( + wt_hsm_vault_storage_size(meta.len) + + wt_hsm_vault_storage_size(sizeof(*table)), + 2U); + if (status != PSA_SUCCESS) { + return status; + } + /* The uncommitted replacement may already carry WRITE_ONCE. */ + status = wt_hsm_vault_map_err( + wh_Nvm_AddObject(g_vault_nvm, &meta, meta.len, g_vault_ct)); + if (status != PSA_SUCCESS) { + return status; + } + table->reserved = 0U; + status = wt_hsm_vault_table_store(table); + if (status != PSA_SUCCESS) { + return status; + } + return wt_hsm_vault_destroy_stage(); +} + /* Shared directory lookup for privileged vault backends: find the * (owner, sub, uid) object in the vault id window. Returns PSA_SUCCESS * with the id + metadata, or PSA_ERROR_DOES_NOT_EXIST. out_free_id receives @@ -284,12 +443,17 @@ static psa_status_t wt_hsm_vault_set(int32_t owner, int32_t sub, const uint8_t* data, size_t len) { whNvmMetadata meta; + whNvmMetadata stage_meta; wt_hsm_vault_table_t table; whNvmId id = WH_NVM_ID_INVALID; whNvmId free_id = WH_NVM_ID_INVALID; + uint32_t slot = 0U; + uint32_t need_size; + uint64_t counter = 0U; whNvmSize store_len; const uint8_t* store_data; psa_status_t status; + int replacement = 0; if (g_vault_nvm == NULL) { return PSA_ERROR_NOT_SUPPORTED; @@ -301,6 +465,10 @@ static psa_status_t wt_hsm_vault_set(int32_t owner, int32_t sub, if ((flags & WT_VAULT_FLAG_SEALED) != 0U && g_vault_sealer == NULL) { return PSA_ERROR_NOT_SUPPORTED; } + status = wt_hsm_vault_table_load(&table); + if (status != PSA_SUCCESS) { + return status; + } status = wt_hsm_vault_lookup(owner, sub, uid, &id, &meta, &free_id); if (status == PSA_SUCCESS) { /* A storage SET must never overwrite a key object (WT-FFM-0046), @@ -310,6 +478,9 @@ static psa_status_t wt_hsm_vault_set(int32_t owner, int32_t sub, (WT_VAULT_FLAG_KEY | WT_VAULT_FLAG_WRITE_ONCE)) != 0U) { return PSA_ERROR_NOT_PERMITTED; } + replacement = + (wt_hsm_vault_flags_of(meta.label) & + WT_VAULT_FLAG_SEALED) != 0U; } else if (status == PSA_ERROR_DOES_NOT_EXIST) { if (free_id == WH_NVM_ID_INVALID) { @@ -321,6 +492,46 @@ static psa_status_t wt_hsm_vault_set(int32_t owner, int32_t sub, return status; } + slot = id - WT_HSM_VAULT_ID_BASE; + store_data = data; + store_len = (whNvmSize)len; + if ((flags & WT_VAULT_FLAG_SEALED) != 0U) { + store_len = (whNvmSize)(len + WT_VAULT_SEAL_TAG_LEN); + } + if (replacement != 0) { + status = wt_hsm_vault_read_sealed(id, &meta, table.slot[slot]); + if (status == PSA_ERROR_INVALID_SIGNATURE) { + replacement = 0; + } + else if (status != PSA_SUCCESS) { + return status; + } + else { + wt_hsm_vault_zeroize( + g_vault_pt, (size_t)meta.len - WT_VAULT_SEAL_TAG_LEN); + } + } + need_size = wt_hsm_vault_storage_size(store_len) + + wt_hsm_vault_storage_size(sizeof(table)); + if (replacement != 0) { + need_size += 2U * wt_hsm_vault_storage_size(meta.len) + + wt_hsm_vault_storage_size(sizeof(table)); + status = wt_hsm_vault_reserve(need_size, 4U); + } + else { + status = wt_hsm_vault_reserve( + need_size, + (uint16_t)(((flags & WT_VAULT_FLAG_SEALED) != 0U || + table.slot[slot] != 0U) ? 2U : 1U)); + } + if (status != PSA_SUCCESS) { + return status; + } + if (replacement != 0) { + stage_meta = meta; + stage_meta.id = WT_HSM_VAULT_STAGE_ID; + stage_meta.flags = WH_NVM_FLAGS_SENSITIVE; + } (void)memset(&meta, 0, sizeof(meta)); meta.id = id; meta.access = WH_NVM_ACCESS_ANY; @@ -330,37 +541,54 @@ static psa_status_t wt_hsm_vault_set(int32_t owner, int32_t sub, WH_NVM_FLAGS_NONDESTROYABLE; } wt_hsm_vault_make_label(meta.label, owner, sub, uid, flags); - store_data = data; - store_len = (whNvmSize)len; if ((flags & WT_VAULT_FLAG_SEALED) != 0U) { /* Persist the bumped counter before any ciphertext exists so a power * loss can never repeat a GCM nonce (WT-FFM-0048). */ - status = wt_hsm_vault_table_load(&table); + table.global++; + counter = table.global; + } + if (replacement != 0 || (flags & WT_VAULT_FLAG_SEALED) != 0U || + table.slot[slot] != 0U) { + if (replacement != 0) { + table.reserved = slot + 1U; + } + else { + table.slot[slot] = counter; + } + status = wt_hsm_vault_table_store(&table); if (status != PSA_SUCCESS) { return status; } - table.global++; - table.slot[id - WT_HSM_VAULT_ID_BASE] = table.global; - status = wt_hsm_vault_table_store(&table); + } + if (replacement != 0) { + status = wt_hsm_vault_map_err( + wh_Nvm_AddObject(g_vault_nvm, &stage_meta, stage_meta.len, + g_vault_ct)); if (status != PSA_SUCCESS) { return status; } + } + if ((flags & WT_VAULT_FLAG_SEALED) != 0U) { status = g_vault_sealer->seal(meta.label, WH_NVM_LABEL_LEN, - table.global, data, len, g_vault_ct); + counter, data, len, g_vault_ct); if (status != PSA_SUCCESS) { return status; } store_data = g_vault_ct; - store_len = (whNvmSize)(len + WT_VAULT_SEAL_TAG_LEN); } meta.len = store_len; - status = wt_hsm_vault_reserve(store_len, - (whNvmSize)sizeof(wt_hsm_vault_table_t)); + status = wt_hsm_vault_map_err( + wh_Nvm_AddObjectChecked(g_vault_nvm, &meta, store_len, store_data)); + if (status != PSA_SUCCESS || replacement == 0) { + return status; + } + table.slot[slot] = counter; + table.reserved = 0U; + status = wt_hsm_vault_table_store(&table); if (status != PSA_SUCCESS) { return status; } - return wt_hsm_vault_map_err( - wh_Nvm_AddObjectChecked(g_vault_nvm, &meta, store_len, store_data)); + return wt_hsm_vault_destroy_stage(); } static psa_status_t wt_hsm_vault_get(int32_t owner, int32_t sub, @@ -378,6 +606,10 @@ static psa_status_t wt_hsm_vault_get(int32_t owner, int32_t sub, if (g_vault_nvm == NULL) { return PSA_ERROR_NOT_SUPPORTED; } + status = wt_hsm_vault_table_load(&table); + if (status != PSA_SUCCESS) { + return status; + } status = wt_hsm_vault_lookup(owner, sub, uid, &id, &meta, NULL); if (status != PSA_SUCCESS) { return status; @@ -389,38 +621,17 @@ static psa_status_t wt_hsm_vault_get(int32_t owner, int32_t sub, return PSA_ERROR_NOT_PERMITTED; } if ((wt_hsm_vault_flags_of(meta.label) & WT_VAULT_FLAG_SEALED) != 0U) { - if (g_vault_sealer == NULL) { - return PSA_ERROR_NOT_SUPPORTED; - } - if (meta.len < WT_VAULT_SEAL_TAG_LEN || - meta.len > sizeof(g_vault_ct)) { - return PSA_ERROR_STORAGE_FAILURE; - } - status = wt_hsm_vault_map_err( - wh_Nvm_ReadChecked(g_vault_nvm, id, 0U, meta.len, g_vault_ct)); - if (status != PSA_SUCCESS) { - return status; - } - status = wt_hsm_vault_table_load(&table); - if (status != PSA_SUCCESS) { - return status; - } if (table.slot[id - WT_HSM_VAULT_ID_BASE] == 0U) { /* A sealed object with no live counter is a rolled-back or * resurrected ciphertext — fail closed. */ return PSA_ERROR_INVALID_SIGNATURE; } - pt_len = (size_t)meta.len - WT_VAULT_SEAL_TAG_LEN; - status = g_vault_sealer->unseal( - meta.label, WH_NVM_LABEL_LEN, - table.slot[id - WT_HSM_VAULT_ID_BASE], g_vault_ct, meta.len, - g_vault_pt); + status = wt_hsm_vault_read_sealed( + id, &meta, table.slot[id - WT_HSM_VAULT_ID_BASE]); if (status != PSA_SUCCESS) { - /* GCM decrypts before the tag compare, so a failed unseal can - * leave unauthenticated plaintext in the persistent buffer. */ - wt_hsm_vault_zeroize(g_vault_pt, pt_len); return status; } + pt_len = (size_t)meta.len - WT_VAULT_SEAL_TAG_LEN; if (offset > pt_len) { wt_hsm_vault_zeroize(g_vault_pt, pt_len); return PSA_ERROR_INVALID_ARGUMENT; @@ -459,11 +670,16 @@ static psa_status_t wt_hsm_vault_get_info(int32_t owner, int32_t sub, uint64_t uid, wt_vault_info_t* info) { whNvmMetadata meta; + wt_hsm_vault_table_t table; psa_status_t status; if (g_vault_nvm == NULL) { return PSA_ERROR_NOT_SUPPORTED; } + status = wt_hsm_vault_table_load(&table); + if (status != PSA_SUCCESS) { + return status; + } status = wt_hsm_vault_lookup(owner, sub, uid, NULL, &meta, NULL); if (status != PSA_SUCCESS) { return status; @@ -493,6 +709,10 @@ static psa_status_t wt_hsm_vault_remove(int32_t owner, int32_t sub, if (g_vault_nvm == NULL) { return PSA_ERROR_NOT_SUPPORTED; } + status = wt_hsm_vault_table_load(&table); + if (status != PSA_SUCCESS) { + return status; + } status = wt_hsm_vault_lookup(owner, sub, uid, &id, &meta, NULL); if (status != PSA_SUCCESS) { return status; @@ -504,10 +724,6 @@ static psa_status_t wt_hsm_vault_remove(int32_t owner, int32_t sub, if ((wt_hsm_vault_flags_of(meta.label) & WT_VAULT_FLAG_SEALED) != 0U) { /* Retire the counter first: a later flash-level resurrection of the * destroyed ciphertext then fails authentication (WT-FFM-0048). */ - status = wt_hsm_vault_table_load(&table); - if (status != PSA_SUCCESS) { - return status; - } table.slot[id - WT_HSM_VAULT_ID_BASE] = 0U; status = wt_hsm_vault_table_store(&table); if (status != PSA_SUCCESS) { diff --git a/tests/host/ffm/main.c b/tests/host/ffm/main.c index 9bdc36d3..f38a1ad4 100644 --- a/tests/host/ffm/main.c +++ b/tests/host/ffm/main.c @@ -46,6 +46,8 @@ typedef struct test_context { int reject_write; int reply_programmer_error; int refuse_dispatch; + psa_msg_t delayed_message; + int delay_result; } test_context_t; static unsigned int g_checks; @@ -1077,12 +1079,195 @@ static void test_refused_dispatch_queue_consistency(void) "queue consistent\n"); } +static int delayed_dispatch(void* context, wt_ffm_runtime_t* runtime, + int32_t partition_id) +{ + test_context_t* test = (test_context_t*)context; + + test->dispatches++; + EXPECT_INT(wt_ffm_get(runtime, partition_id, TEST_SERVICE_SIGNAL, + &test->delayed_message), PSA_SUCCESS); + return test->delay_result; +} + +static void expect_ffm_pools_empty(const wt_ffm_runtime_t* runtime) +{ + size_t i; + + for (i = 0U; i < WT_FFM_MAX_MESSAGES; i++) + EXPECT_INT(runtime->messages[i].allocated, 0); + for (i = 0U; i < WT_FFM_MAX_CONNECTIONS; i++) + EXPECT_INT(runtime->connections[i].allocated, 0); +} + +static void test_delayed_synchronous_reply(void) +{ + wt_ffm_runtime_t runtime; + test_context_t context; + wt_ffm_port_ops_t ops = g_port_ops; + psa_handle_t handle; + psa_handle_t message_handle; + uint8_t request[3] = { 'a', 'b', 'c' }; + uint8_t copied[3]; + uint8_t response[2]; + psa_invec input = { request, sizeof(request) }; + psa_outvec output = { response, sizeof(response) }; + unsigned int mode; + size_t i; + + for (mode = 0U; mode < 4U; mode++) { + test_init(&runtime, &context); + handle = wt_ffm_connect(&runtime, TEST_NS_CLIENT, TEST_SERVICE_SID, + 3U); + EXPECT_TRUE(PSA_HANDLE_IS_VALID(handle)); + context.delay_result = (mode & 1U) != 0U ? + WT_FFM_ERROR_NOT_READY : WT_FFM_SUCCESS; + if ((mode & 2U) != 0U) { + EXPECT_INT(wt_ffm_register_partition(&runtime, TEST_PARTITION_ID, + delayed_dispatch, &context), WT_FFM_SUCCESS); + } + else { + ops.dispatch = delayed_dispatch; + runtime.ops = &ops; + } + (void)memset(response, 0, sizeof(response)); + EXPECT_INT(wt_ffm_call(&runtime, TEST_NS_CLIENT, handle, + PSA_IPC_CALL, &input, 1U, &output, 1U), + PSA_ERROR_GENERIC_ERROR); + message_handle = context.delayed_message.handle; + if ((mode & 1U) != 0U) { + EXPECT_INT(wt_ffm_close(&runtime, TEST_NS_CLIENT, handle), + WT_FFM_SUCCESS); + EXPECT_INT(runtime.services[0].queue_head, WT_FFM_QUEUE_NONE); + } + EXPECT_SIZE(wt_ffm_read(&runtime, TEST_PARTITION_ID, message_handle, + 0U, copied, sizeof(copied)), sizeof(copied)); + EXPECT_TRUE(memcmp(copied, request, sizeof(copied)) == 0); + EXPECT_INT(wt_ffm_write(&runtime, TEST_PARTITION_ID, message_handle, + 0U, "OK", 2U), WT_FFM_SUCCESS); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, message_handle, + PSA_SUCCESS), WT_FFM_SUCCESS); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, message_handle, + PSA_SUCCESS), WT_FFM_ERROR_HANDLE); + EXPECT_INT(response[0], 0); + EXPECT_INT(response[1], 0); + EXPECT_SIZE(output.len, sizeof(response)); + if ((mode & 1U) != 0U) { + (void)delayed_dispatch(&context, &runtime, TEST_PARTITION_ID); + } + else { + EXPECT_INT(wt_ffm_call(&runtime, TEST_NS_CLIENT, handle, + PSA_IPC_CALL, &input, 1U, &output, 1U), + PSA_ERROR_PROGRAMMER_ERROR); + EXPECT_INT(wt_ffm_close(&runtime, TEST_NS_CLIENT, handle), + WT_FFM_ERROR_NOT_READY); + } + EXPECT_INT(context.delayed_message.type, PSA_IPC_DISCONNECT); + EXPECT_TRUE(context.delayed_message.rhandle == TEST_RHANDLE); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, + context.delayed_message.handle, PSA_SUCCESS), + WT_FFM_SUCCESS); + expect_ffm_pools_empty(&runtime); + + for (i = 0U; i < WT_FFM_MAX_CONNECTIONS + 1U; i++) { + EXPECT_INT(wt_ffm_connect(&runtime, TEST_NS_CLIENT, + TEST_SERVICE_SID, 3U), PSA_ERROR_GENERIC_ERROR); + message_handle = context.delayed_message.handle; + EXPECT_INT(wt_ffm_set_rhandle(&runtime, TEST_PARTITION_ID, + message_handle, TEST_RHANDLE), WT_FFM_SUCCESS); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, + message_handle, PSA_SUCCESS), WT_FFM_SUCCESS); + (void)delayed_dispatch(&context, &runtime, TEST_PARTITION_ID); + EXPECT_INT(context.delayed_message.type, PSA_IPC_DISCONNECT); + EXPECT_TRUE(context.delayed_message.rhandle == TEST_RHANDLE); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, + context.delayed_message.handle, PSA_SUCCESS), + WT_FFM_SUCCESS); + expect_ffm_pools_empty(&runtime); + } + EXPECT_INT(wt_ffm_connect(&runtime, TEST_NS_CLIENT, TEST_SERVICE_SID, + 3U), PSA_ERROR_GENERIC_ERROR); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, + context.delayed_message.handle, + PSA_ERROR_CONNECTION_REFUSED), WT_FFM_SUCCESS); + expect_ffm_pools_empty(&runtime); + EXPECT_INT(context.panics, 0); + } + (void)printf("PASS: WT-FFM-0023/0024/0026/0034 synchronous IPC retains " + "messages until delayed replies\n"); +} + +static void test_delayed_reply_fault_cleanup(void) +{ + wt_ffm_runtime_t runtime; + test_context_t context; + psa_handle_t handle = PSA_NULL_HANDLE; + uint16_t pending; + unsigned int mode; + + for (mode = 0U; mode < 5U; mode++) { + test_init(&runtime, &context); + if (mode >= 2U) { + handle = wt_ffm_connect(&runtime, TEST_NS_CLIENT, + TEST_SERVICE_SID, 3U); + EXPECT_TRUE(PSA_HANDLE_IS_VALID(handle)); + } + EXPECT_INT(wt_ffm_register_partition(&runtime, TEST_PARTITION_ID, + delayed_dispatch, &context), WT_FFM_SUCCESS); + if (mode < 2U) { + EXPECT_INT(wt_ffm_connect(&runtime, TEST_NS_CLIENT, + TEST_SERVICE_SID, 3U), PSA_ERROR_GENERIC_ERROR); + if (mode == 1U) { + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, + context.delayed_message.handle, PSA_SUCCESS), + WT_FFM_SUCCESS); + } + } + else if (mode < 4U) { + EXPECT_INT(wt_ffm_call(&runtime, TEST_NS_CLIENT, handle, + PSA_IPC_CALL, NULL, 0U, NULL, 0U), + PSA_ERROR_GENERIC_ERROR); + if (mode == 3U) { + EXPECT_INT(wt_ffm_close_begin(&runtime, TEST_NS_CLIENT, + handle, &pending), + WT_FFM_SUCCESS); + EXPECT_INT(pending, WT_FFM_QUEUE_NONE); + } + } + else { + EXPECT_INT(wt_ffm_close(&runtime, TEST_NS_CLIENT, handle), + WT_FFM_ERROR_NOT_READY); + } + EXPECT_INT(wt_ffm_fail_partition_messages(&runtime, TEST_PARTITION_ID, + PSA_ERROR_COMMUNICATION_FAILURE), 1); + EXPECT_INT(wt_ffm_fail_partition_messages(&runtime, TEST_PARTITION_ID, + PSA_ERROR_COMMUNICATION_FAILURE), 0); + EXPECT_INT(runtime.services[0].queue_head, WT_FFM_QUEUE_NONE); + EXPECT_INT(runtime.services[0].queue_tail, WT_FFM_QUEUE_NONE); + EXPECT_INT(runtime.partitions[0].asserted_signals, 0); + if (mode == 2U) { + EXPECT_INT(wt_ffm_call(&runtime, TEST_NS_CLIENT, handle, + PSA_IPC_CALL, NULL, 0U, NULL, 0U), + PSA_ERROR_PROGRAMMER_ERROR); + EXPECT_INT(wt_ffm_close(&runtime, TEST_NS_CLIENT, handle), + WT_FFM_ERROR_NOT_READY); + EXPECT_INT(wt_ffm_reply(&runtime, TEST_PARTITION_ID, + context.delayed_message.handle, PSA_SUCCESS), + WT_FFM_SUCCESS); + } + expect_ffm_pools_empty(&runtime); + } + (void)printf("PASS: WT-FFM-0017 delayed reply fault cleanup\n"); +} + int main(void) { test_arguments(); test_error_latch_and_omitted_write(); test_predispatch_error_drops_connection(); test_refused_dispatch_queue_consistency(); + test_delayed_synchronous_reply(); + test_delayed_reply_fault_cleanup(); test_doorbell_signal(); test_eoi_signal(); test_irq_route_and_assert(); diff --git a/tests/host/ps_service/main.c b/tests/host/ps_service/main.c index 5532b896..2847592b 100644 --- a/tests/host/ps_service/main.c +++ b/tests/host/ps_service/main.c @@ -49,6 +49,7 @@ #define TEST_VAULT_ID_BASE 0x0100U #define TEST_VAULT_ID_COUNT 32U +#define TEST_VAULT_STAGE_ID 0x0123U #define RAMSIM_SIZE (64 * 1024) #define RAMSIM_SECTOR 4096 @@ -62,11 +63,28 @@ static whFlashRamsimCtx g_ramsim_ctx; static const whFlashCb g_ramsim_cb[1] = {WH_FLASH_RAMSIM_CB}; static whNvmFlashConfig g_nvm_flash_cfg; static whNvmFlashContext g_nvm_flash_ctx; -static const whNvmCb g_nvm_cb[1] = {WH_NVM_FLASH_CB}; +static whNvmCb g_nvm_cb[1] = {WH_NVM_FLASH_CB}; static whNvmConfig g_nvm_cfg; static whNvmContext g_nvm_ctx; static int g_failures; +static whNvmId g_fail_add_id = WH_NVM_ID_INVALID; +static unsigned int g_fail_add_skips; + +static int test_nvm_add(void* context, whNvmMetadata* meta, + whNvmSize data_len, const uint8_t* data) +{ + if (meta != NULL && meta->id == g_fail_add_id) { + if (g_fail_add_skips > 0U) { + g_fail_add_skips--; + } + else { + g_fail_add_id = WH_NVM_ID_INVALID; + return WH_ERROR_ABORTED; + } + } + return wh_NvmFlash_AddObject(context, meta, data_len, data); +} static void check(int ok, const char* what) { @@ -159,6 +177,9 @@ static const wt_system_manifest_t g_manifest = { * reboot re-seeds the sim from a snapshot of the pre-reset flash image. */ static int test_nvm_up(int reboot) { + g_fail_add_id = WH_NVM_ID_INVALID; + g_fail_add_skips = 0U; + g_nvm_cb[0].AddObject = test_nvm_add; (void)memset(&g_ramsim_cfg, 0, sizeof(g_ramsim_cfg)); g_ramsim_cfg.memory = g_flash_memory; g_ramsim_cfg.size = RAMSIM_SIZE; @@ -363,14 +384,379 @@ static int test_find_stored(size_t plain_len, whNvmId* out_id, return -1; } +static int test_fill_to_available(whNvmId target) +{ + static whNvmId next_id = 0x0200U; + whNvmMetadata meta; + whNvmId available; + int rc; + + rc = wh_Nvm_DestroyObjects(&g_nvm_ctx, 0U, NULL); + if (rc != WH_ERROR_OK) { + return -1; + } + do { + rc = wh_Nvm_GetAvailable(&g_nvm_ctx, NULL, &available, NULL, NULL); + if (rc != WH_ERROR_OK || available < target) { + return -1; + } + if (available > target) { + (void)memset(&meta, 0, sizeof(meta)); + meta.id = next_id++; + meta.access = WH_NVM_ACCESS_ANY; + rc = wh_Nvm_AddObject(&g_nvm_ctx, &meta, 0U, NULL); + if (rc != WH_ERROR_OK) { + return -1; + } + } + } while (available > target); + + return 0; +} + +static void test_unsealed_replacement(void) +{ + static const uint8_t original[] = "sealed value"; + static const uint8_t updated[] = "plain value"; + whNvmMetadata old_meta; + whNvmId id; + uint8_t old_ct[sizeof(original) + WT_VAULT_SEAL_TAG_LEN]; + uint8_t buffer[sizeof(original)]; + size_t got; + unsigned int step; + psa_status_t status; + + for (step = 0U; step < 6U; step++) { + check(test_nvm_up(0) == 0, "initialized vault transition test"); + status = wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x6001ULL, WT_VAULT_FLAG_SEALED, original, sizeof(original)); + check(status == PSA_SUCCESS, "created sealed transition source"); + if (status != PSA_SUCCESS) { + return; + } + if (test_find_stored(sizeof(original), &id, &old_meta) != 0) { + check(0, "located sealed transition source"); + return; + } + check(wh_Nvm_Read(&g_nvm_ctx, id, 0U, old_meta.len, old_ct) == + WH_ERROR_OK, "saved sealed transition source"); + if (step == 5U) { + old_ct[0] ^= 1U; + check(wh_Nvm_AddObject(&g_nvm_ctx, &old_meta, old_meta.len, + old_ct) == WH_ERROR_OK, + "prepared invalid sealed transition source"); + old_ct[0] ^= 1U; + } + if (step < 4U) { + g_fail_add_id = step == 0U ? 0x0123U : + step == 2U ? id : WT_HSM_VAULT_TABLE_ID; + g_fail_add_skips = step == 3U ? 1U : 0U; + } + status = wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x6001ULL, WT_VAULT_FLAG_WRITE_ONCE, updated, sizeof(updated)); + check(status == (step < 4U ? PSA_ERROR_STORAGE_FAILURE : PSA_SUCCESS), + "unsealed replacement reports its transaction result"); + check(test_nvm_up(1) == 0, "rebooted after unsealed replacement"); + status = wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x6001ULL, 0U, buffer, sizeof(buffer), &got); + if (step < 4U) { + check(status == PSA_SUCCESS && got == sizeof(original) && + memcmp(buffer, original, sizeof(original)) == 0, + "WT-FFM-0048 failed unsealed replacement preserves source"); + } + else { + check(status == PSA_SUCCESS && got == sizeof(updated) && + memcmp(buffer, updated, sizeof(updated)) == 0, + "committed unsealed replacement survives reboot"); + check(wh_Nvm_AddObjectWithReclaim(&g_nvm_ctx, &old_meta, + old_meta.len, old_ct) == WH_ERROR_OK, + "restored prior sealed object for counter check"); + status = wt_hsm_vault_backend.get(TEST_PS_PARTITION, + TEST_NS_GUEST0, 0x6001ULL, 0U, buffer, sizeof(buffer), &got); + check(status == PSA_ERROR_INVALID_SIGNATURE, + "WT-FFM-0048 unsealed replacement retires sealed counter"); + status = wt_hsm_vault_backend.set(TEST_PS_PARTITION, + TEST_NS_GUEST0, 0x6001ULL, WT_VAULT_FLAG_SEALED, + updated, sizeof(updated)); + check(status == PSA_SUCCESS, + "a retired sealed object can be rewritten"); + status = wt_hsm_vault_backend.get(TEST_PS_PARTITION, + TEST_NS_GUEST0, 0x6001ULL, 0U, buffer, sizeof(buffer), &got); + check(status == PSA_SUCCESS && got == sizeof(updated) && + memcmp(buffer, updated, sizeof(updated)) == 0, + "rewritten retired object is readable"); + } + } +} + +static void test_recovery_containment(void) +{ + static const uint8_t original[] = "committed source"; + static const uint8_t updated[] = "replacement"; + whNvmMetadata meta; + whNvmId id; + whNvmId stage_id = 0x0123U; + wt_vault_info_t info; + uint8_t buffer[sizeof(original) + WT_VAULT_SEAL_TAG_LEN]; + size_t got; + unsigned int scenario; + uint32_t flags; + psa_status_t status; + + for (scenario = 0U; scenario < 12U; scenario++) { + check(test_nvm_up(0) == 0, "initialized recovery containment test"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x7001ULL, WT_VAULT_FLAG_SEALED, original, + sizeof(original)) == PSA_SUCCESS, + "created recovery source"); + if (test_find_stored(sizeof(original), &id, &meta) != 0) { + check(0, "located recovery source"); + return; + } + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST1, + 0x7002ULL, WT_VAULT_FLAG_SEALED, original, + sizeof(original)) == PSA_SUCCESS, + "created unrelated object"); + g_fail_add_id = scenario < 3U ? id : WT_HSM_VAULT_TABLE_ID; + g_fail_add_skips = scenario < 3U ? 0U : 1U; + flags = scenario / 3U == 2U ? WT_VAULT_FLAG_WRITE_ONCE : + WT_VAULT_FLAG_SEALED; + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x7001ULL, flags, updated, sizeof(updated)) == + PSA_ERROR_STORAGE_FAILURE, + "interrupted the replacement"); + if (scenario >= 9U) { + check(wh_Nvm_DestroyObjects(&g_nvm_ctx, 1U, &id) == WH_ERROR_OK, + "removed incomplete target"); + } + if (scenario % 3U == 0U) { + check(wh_Nvm_DestroyObjects(&g_nvm_ctx, 1U, &stage_id) == + WH_ERROR_OK, "removed unavailable recovery copy"); + } + else { + check(wh_Nvm_Read(&g_nvm_ctx, stage_id, 0U, meta.len, buffer) == + WH_ERROR_OK, "read recovery copy"); + if (scenario % 3U == 1U) { + buffer[0] ^= 1U; + } + else { + meta.flags |= WH_NVM_FLAGS_NONEXPORTABLE; + } + meta.id = stage_id; + check(wh_Nvm_AddObject(&g_nvm_ctx, &meta, meta.len, buffer) == + WH_ERROR_OK, "invalidated recovery copy"); + } + check(test_nvm_up(1) == 0, "rebooted before recovery containment"); + if (scenario >= 3U && scenario < 9U) { + g_fail_add_id = WT_HSM_VAULT_TABLE_ID; + check(wt_hsm_vault_backend.get(TEST_PS_PARTITION, + TEST_NS_GUEST1, 0x7002ULL, 0U, buffer, sizeof(buffer), + &got) == PSA_ERROR_STORAGE_FAILURE, + "interrupted recovery cleanup reports a write failure"); + check(test_nvm_up(1) == 0, "rebooted during recovery cleanup"); + } + status = wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST1, + 0x7002ULL, 0U, buffer, sizeof(buffer), &got); + check(status == PSA_SUCCESS && got == sizeof(original) && + memcmp(buffer, original, sizeof(original)) == 0, + "WT-FFM-0048 recovery failure leaves unrelated data readable"); + check(wh_Nvm_GetMetadata(&g_nvm_ctx, TEST_VAULT_STAGE_ID, &meta) == + WH_ERROR_NOTFOUND, + "completed recovery destroys its recovery stage"); + status = wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x7001ULL, 0U, buffer, sizeof(buffer), &got); + check(scenario < 3U ? + status == PSA_SUCCESS && got == sizeof(original) && + memcmp(buffer, original, sizeof(original)) == 0 : + status == PSA_ERROR_DOES_NOT_EXIST, + "recovery keeps only an authenticated committed object"); + check(wt_hsm_vault_backend.get_info(TEST_PS_PARTITION, + TEST_NS_GUEST1, 0x7002ULL, &info) == PSA_SUCCESS && + info.size == sizeof(original), + "unrelated metadata remains available"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST1, + 0x7002ULL, WT_VAULT_FLAG_SEALED, updated, + sizeof(updated)) == PSA_SUCCESS, + "unrelated sealed replacements remain available"); + check(wt_hsm_vault_backend.remove(TEST_PS_PARTITION, TEST_NS_GUEST1, + 0x7002ULL) == PSA_SUCCESS, + "unrelated removal remains available"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x7001ULL, WT_VAULT_FLAG_SEALED, updated, + sizeof(updated)) == PSA_SUCCESS, + "affected slot can be rewritten after recovery"); + } +} + +static void test_authenticated_recovery(void) +{ + static const uint8_t original[] = "authenticated source"; + static const uint8_t updated[] = "uncommitted value"; + whNvmMetadata meta; + whNvmId id; + uint8_t buffer[sizeof(original) + WT_VAULT_SEAL_TAG_LEN]; + size_t got; + psa_status_t status; + + check(test_nvm_up(0) == 0, "initialized authenticated recovery test"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x9001ULL, WT_VAULT_FLAG_SEALED, original, sizeof(original)) == + PSA_SUCCESS, "created authenticated recovery source"); + if (test_find_stored(sizeof(original), &id, &meta) != 0) { + check(0, "located authenticated recovery source"); + return; + } + g_fail_add_id = WT_HSM_VAULT_TABLE_ID; + g_fail_add_skips = 1U; + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x9001ULL, WT_VAULT_FLAG_SEALED, updated, sizeof(updated)) == + PSA_ERROR_STORAGE_FAILURE, + "interrupted replacement retains authenticated recovery copy"); + check(wh_Nvm_GetMetadata(&g_nvm_ctx, id, &meta) == WH_ERROR_OK && + wh_Nvm_Read(&g_nvm_ctx, id, 0U, meta.len, buffer) == WH_ERROR_OK, + "read uncommitted target"); + meta.label[8] ^= 1U; + check(wh_Nvm_AddObject(&g_nvm_ctx, &meta, meta.len, buffer) == WH_ERROR_OK, + "invalidated uncommitted target identity"); + check(test_nvm_up(1) == 0, "rebooted with invalid target identity"); + status = wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x9001ULL, 0U, buffer, sizeof(buffer), &got); + check(status == PSA_SUCCESS && got == sizeof(original) && + memcmp(buffer, original, sizeof(original)) == 0, + "WT-FFM-0048 authenticated recovery ignores invalid live identity"); +} + +static void test_recovery_counter_binding(void) +{ + static const uint8_t original[] = "committed source"; + static const uint8_t other[] = "other identity"; + whNvmMetadata meta; + whNvmId id; + uint8_t buffer[sizeof(original) + WT_VAULT_SEAL_TAG_LEN]; + size_t got; + + check(test_nvm_up(0) == 0, "initialized recovery counter test"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0xA001ULL, WT_VAULT_FLAG_SEALED, original, sizeof(original)) == + PSA_SUCCESS, "created recovery counter source"); + if (test_find_stored(sizeof(original), &id, &meta) != 0) { + check(0, "located recovery counter source"); + return; + } + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST1, + 0xA002ULL, WT_VAULT_FLAG_SEALED, other, sizeof(other)) == + PSA_SUCCESS, "created distinct recovery identity"); + g_fail_add_id = id; + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0xA001ULL, WT_VAULT_FLAG_SEALED, other, sizeof(other)) == + PSA_ERROR_STORAGE_FAILURE, + "interrupted replacement before target write"); + if (test_find_stored(sizeof(other), &id, &meta) != 0) { + check(0, "located distinct recovery identity"); + return; + } + check(wh_Nvm_Read(&g_nvm_ctx, id, 0U, meta.len, buffer) == WH_ERROR_OK, + "read distinct sealed object"); + meta.id = 0x0123U; + check(wh_Nvm_AddObject(&g_nvm_ctx, &meta, meta.len, buffer) == WH_ERROR_OK, + "staged distinct sealed object"); + check(test_nvm_up(1) == 0, "rebooted with distinct recovery identity"); + check(wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0xA001ULL, 0U, buffer, sizeof(buffer), &got) == PSA_SUCCESS && + got == sizeof(original) && + memcmp(buffer, original, sizeof(original)) == 0, + "WT-FFM-0048 recovery counter rejects another sealed identity"); + check(wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST1, + 0xA002ULL, 0U, buffer, sizeof(buffer), &got) == PSA_SUCCESS && + got == sizeof(other) && memcmp(buffer, other, sizeof(other)) == 0, + "distinct sealed identity remains unchanged"); +} + +static void test_invalid_sealed_length(void) +{ + static const uint8_t original[] = "valid value"; + static const uint8_t invalid[WT_VAULT_OBJECT_MAX + WT_VAULT_SEAL_TAG_LEN + 1U]; + whNvmMetadata meta; + whNvmId id; + uint8_t buffer[sizeof(original)]; + size_t got; + unsigned int oversized; + + for (oversized = 0U; oversized < 2U; oversized++) { + check(test_nvm_up(0) == 0, "initialized sealed length test"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x8001ULL, WT_VAULT_FLAG_SEALED, original, + sizeof(original)) == PSA_SUCCESS, + "created sealed length source"); + if (test_find_stored(sizeof(original), &id, &meta) != 0) { + check(0, "located sealed length source"); + return; + } + meta.len = oversized != 0U ? (whNvmSize)sizeof(invalid) : 0U; + check(wh_Nvm_AddObject(&g_nvm_ctx, &meta, meta.len, invalid) == + WH_ERROR_OK, "stored invalid sealed length"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x8001ULL, WT_VAULT_FLAG_SEALED, original, + sizeof(original)) == PSA_SUCCESS, + "invalid sealed length does not prevent a rewrite"); + check(wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0x8001ULL, 0U, buffer, sizeof(buffer), &got) == PSA_SUCCESS && + got == sizeof(original) && + memcmp(buffer, original, sizeof(original)) == 0, + "rewritten malformed object is readable"); + } +} + +static void test_replacement_stage_cleanup(void) +{ + static const uint8_t original[] = "staged source"; + static const uint8_t updated[] = "staged replacement"; + whNvmMetadata meta; + whNvmId id; + uint8_t buffer[sizeof(updated) + WT_VAULT_SEAL_TAG_LEN]; + size_t got; + + check(test_nvm_up(0) == 0, "initialized stage cleanup test"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0xB001ULL, WT_VAULT_FLAG_SEALED, original, sizeof(original)) == + PSA_SUCCESS, "created stage cleanup source"); + check(wt_hsm_vault_backend.set(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0xB001ULL, WT_VAULT_FLAG_SEALED, updated, sizeof(updated)) == + PSA_SUCCESS, "committed staged replacement"); + check(wh_Nvm_GetMetadata(&g_nvm_ctx, TEST_VAULT_STAGE_ID, &meta) == + WH_ERROR_NOTFOUND, + "committed replacement destroys its recovery stage"); + if (test_find_stored(sizeof(updated), &id, &meta) != 0) { + check(0, "located committed stage cleanup object"); + return; + } + check(wh_Nvm_Read(&g_nvm_ctx, id, 0U, meta.len, buffer) == WH_ERROR_OK, + "read committed object for orphan cleanup test"); + meta.id = TEST_VAULT_STAGE_ID; + meta.flags = WH_NVM_FLAGS_SENSITIVE; + check(wh_Nvm_AddObject(&g_nvm_ctx, &meta, meta.len, buffer) == WH_ERROR_OK, + "created orphan recovery stage"); + check(wt_hsm_vault_backend.get(TEST_PS_PARTITION, TEST_NS_GUEST0, + 0xB001ULL, 0U, buffer, sizeof(buffer), &got) == PSA_SUCCESS && + got == sizeof(updated) && + memcmp(buffer, updated, sizeof(updated)) == 0, + "orphan stage cleanup preserves the committed object"); + check(wh_Nvm_GetMetadata(&g_nvm_ctx, TEST_VAULT_STAGE_ID, &meta) == + WH_ERROR_NOTFOUND, + "next operation destroys an orphan recovery stage"); +} + int main(void) { static const uint8_t secret_v1[] = "ps-secret-version-one"; static const uint8_t secret_v2[] = "ps-secret-version-TWO"; + static const uint8_t secret_v3[] = "ps-secret-version-THR"; static const uint8_t secret_wo[] = "ps-write-once-secret"; uint8_t old_ct[sizeof(secret_v1) + WT_VAULT_SEAL_TAG_LEN]; whNvmMetadata old_meta; whNvmMetadata meta; + whNvmCb incompatible_cb = WH_NVM_FLASH_CB; + whNvmContext incompatible_nvm; whNvmId stored_id = 0U; wt_ffm_runtime_t runtime; wt_storage_service_ctx_t ps_ctx; @@ -388,6 +774,13 @@ int main(void) (void)fprintf(stderr, "NVM/sealer bring-up failed\n"); return 1; } + incompatible_nvm = g_nvm_ctx; + incompatible_cb.GetAvailable = NULL; + incompatible_nvm.cb = &incompatible_cb; + check(wt_hsm_vault_init(&incompatible_nvm) != 0, + "vault rejects an incompatible NVM capacity contract"); + check(wt_hsm_vault_init(&g_nvm_ctx) == 0, + "vault accepts the flash backend with a RAM simulator"); if (test_runtime_up(&runtime, &ps_ctx) != 0) { (void)fprintf(stderr, "runtime bring-up failed\n"); return 1; @@ -453,11 +846,40 @@ int main(void) old_meta = meta; check(wh_Nvm_Read(&g_nvm_ctx, stored_id, 0U, old_meta.len, old_ct) == WH_ERROR_OK, "captured v1 ciphertext for replay"); + g_fail_add_id = stored_id; + status = ps_set(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + secret_v2, sizeof(secret_v2)); + check(status == PSA_ERROR_STORAGE_FAILURE, + "failed replacement reports storage failure"); + (void)memset(buffer, 0, sizeof(buffer)); + status = ps_get(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + buffer, sizeof(buffer), &got); + check(status == PSA_SUCCESS && got == sizeof(secret_v1) && + memcmp(buffer, secret_v1, sizeof(secret_v1)) == 0, + "failed replacement rolls back to the prior object"); + status = ps_set(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, secret_v2, sizeof(secret_v2)); - check(status == PSA_SUCCESS, "guest0 ps_set updates to v2"); + check(status == PSA_SUCCESS, "guest0 ps_set commits v2"); + g_fail_add_id = WT_HSM_VAULT_TABLE_ID; + g_fail_add_skips = 1U; + status = ps_set(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + secret_v3, sizeof(secret_v3)); + check(status == PSA_ERROR_STORAGE_FAILURE, + "interrupted replacement reports storage failure"); check(wh_Nvm_AddObject(&g_nvm_ctx, &old_meta, old_meta.len, old_ct) == - WH_ERROR_OK, "replayed v1 ciphertext into the NVM object"); + WH_ERROR_OK, "replayed stale ciphertext before recovery"); + check(test_nvm_up(1) == 0, + "reinitialized NVM with an incomplete replacement"); + (void)memset(buffer, 0, sizeof(buffer)); + status = ps_get(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + buffer, sizeof(buffer), &got); + check(status == PSA_SUCCESS && got == sizeof(secret_v2) && + memcmp(buffer, secret_v2, sizeof(secret_v2)) == 0, + "reboot restores the authenticated prior object"); + check(wh_Nvm_AddObjectWithReclaim(&g_nvm_ctx, &old_meta, old_meta.len, + old_ct) == WH_ERROR_OK, + "replayed v1 ciphertext into the NVM object"); status = ps_get(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, buffer, sizeof(buffer), &got); check(status == PSA_ERROR_INVALID_SIGNATURE, @@ -514,11 +936,39 @@ int main(void) memcmp(buffer, secret_wo, sizeof(secret_wo)) == 0, "WT-FFM-0045 WRITE_ONCE sealed object survives reboot"); + check(test_fill_to_available(4U) == 0, + "prepared exact replacement transaction capacity"); + status = ps_set(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + secret_v3, sizeof(secret_v3)); + check(status == PSA_SUCCESS, + "sealed replacement uses exact transaction capacity"); + check(test_fill_to_available(4U) == 0, + "prepared exact rollback transaction capacity"); + g_fail_add_id = WT_HSM_VAULT_TABLE_ID; + g_fail_add_skips = 1U; + status = ps_set(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + secret_v2, sizeof(secret_v2)); + check(status == PSA_ERROR_STORAGE_FAILURE, + "near-capacity interrupted replacement reports failure"); + (void)memset(buffer, 0, sizeof(buffer)); + status = ps_get(&runtime, TEST_NS_GUEST0, handle_g0, 0x5001ULL, 0U, + buffer, sizeof(buffer), &got); + check(status == PSA_SUCCESS && got == sizeof(secret_v3) && + memcmp(buffer, secret_v3, sizeof(secret_v3)) == 0, + "near-capacity recovery preserves the prior object"); + if (wt_ffm_close(&runtime, TEST_NS_GUEST0, handle_g0) != WT_FFM_SUCCESS) { (void)fprintf(stderr, "psa_close after reboot failed\n"); return 1; } + test_unsealed_replacement(); + test_recovery_containment(); + test_authenticated_recovery(); + test_recovery_counter_binding(); + test_invalid_sealed_length(); + test_replacement_stage_cleanup(); + if (g_failures != 0) { return 1; }