From 33f51385b14189c43d4f3917ff1a9d2341e335a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 15:00:03 +0000 Subject: [PATCH 01/16] LMS/HSS private keys (Step 2, part 1): state-file engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for --enable-lms-private (EXPERIMENTAL): the internal engine for stateful LMS/HSS private keys. Not yet wired to the PKCS#11 entry points (C_GenerateKeyPair / C_Sign) or advertised — that follows in part 2. - Build: --enable-lms-private / WOLFPKCS11_LMS_PRIVATE (implies --enable-lms), autotools + CMake. - internal.h: compile guards (needs sign-capable wolfSSL LMS, LMS, store; not TPM store), derived WOLFPKCS11_STATEFUL_SIG_ANY/_PRIVATE, WP11_Object state fields, WP11_FLAG_STATEFUL_STATE_VALID, WP11_INIT_HSS_SIGN, prototypes. - internal.c: durable file-store plumbing (fsync of data + parent directory before the atomic rename commits); AES-GCM-with-AAD helpers; the generic durable state-file framework (per-key 64-bit nonce, AAD-bound header with the signature counter, poison-on-failure, optional relax-fsync env knob); HSS thin wrappers and wolfSSL write/read state callbacks; keygen, stateful sign, and remaining-signature accounting; private-key shell encode/decode (reload of durable state) keeping the Step-1 public-key format unchanged; and orphan state-file cleanup on destroy. Signing persists the advanced one-time-signature state durably (fsync + atomic rename) before releasing each signature; wolfSSL zeroizes the signature if that persist fails, preventing OTS-key reuse. All configurations build clean with -Werror (default, --enable-lms, --enable-lms-private); verify-only pkcs11v3test passes with no regression. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- CMakeLists.txt | 29 + cmake/options.h.in | 2 + configure.ac | 37 + src/internal.c | 1544 +++++++++++++++++++++++++++++++++++++---- wolfpkcs11/internal.h | 42 +- 5 files changed, 1506 insertions(+), 148 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf4ada64..c7a866f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -533,6 +533,35 @@ if(WOLFPKCS11_LMS) list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS") endif() +# LMS/HSS keygen + signing (EXPERIMENTAL, stateful private keys; default: disabled) +add_option("WOLFPKCS11_LMS_PRIVATE" + "Enable wolfPKCS11 LMS/HSS keygen and signing (EXPERIMENTAL, stateful private keys; default: disabled)" + "no" "yes;no" +) + +if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_LMS) + message(STATUS "WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS — enabling WOLFPKCS11_LMS automatically") + override_cache(WOLFPKCS11_LMS "yes") + if(NOT WOLFPKCS11_PKCS11_V3_2) + override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") + if(NOT WOLFPKCS11_PKCS11_V3_0) + override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_PKCS11_V3_0") + endif() + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_PKCS11_V3_2") + endif() + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS") +endif() + +if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_SHA256) + message(STATUS "LMS/HSS requires SHA-256 support (disabled); disabling WOLFPKCS11_LMS_PRIVATE") + override_cache(WOLFPKCS11_LMS_PRIVATE "no") +endif() + +if(WOLFPKCS11_LMS_PRIVATE) + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS_PRIVATE") +endif() + # XMSS/XMSS^MT (RFC 8391) verify + public-key import (default: disabled) add_option("WOLFPKCS11_XMSS" "Enable wolfPKCS11 XMSS/XMSS^MT verification and public-key import (default: disabled)" diff --git a/cmake/options.h.in b/cmake/options.h.in index 594bcdb3..f1234dee 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -98,6 +98,8 @@ extern "C" { #cmakedefine WOLFPKCS11_MLKEM #undef WOLFPKCS11_LMS #cmakedefine WOLFPKCS11_LMS +#undef WOLFPKCS11_LMS_PRIVATE +#cmakedefine WOLFPKCS11_LMS_PRIVATE #undef WOLFPKCS11_XMSS #cmakedefine WOLFPKCS11_XMSS #undef WOLFPKCS11_TPM diff --git a/configure.ac b/configure.ac index 2ef69ffa..9a4877b8 100644 --- a/configure.ac +++ b/configure.ac @@ -596,6 +596,42 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS" fi +# LMS/HSS key generation and signing (EXPERIMENTAL). Stateful private keys: +# a one-time signature leaf must never be reused, so signing durably persists +# the advanced key state before releasing each signature. Implies --enable-lms +# and requires a sign-capable wolfSSL LMS build (not --enable-lms=verify-only). +AC_ARG_ENABLE([lms-private], + [AS_HELP_STRING([--enable-lms-private],[Enable LMS/HSS keygen and signing (EXPERIMENTAL, stateful private keys, default: disabled)])], + [ ENABLED_LMS_PRIVATE=$enableval ], + [ ENABLED_LMS_PRIVATE=no ] + ) + +if test "$ENABLED_LMS_PRIVATE" = "yes" && test "$ENABLED_LMS" = "no" +then + AC_MSG_NOTICE([--enable-lms-private implies --enable-lms; enabling]) + ENABLED_LMS=yes + if test "$ENABLED_PKCS11V3_2" = "no"; then + ENABLED_PKCS11V3_2=yes + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_PKCS11_V3_2" + if test "$ENABLED_PKCS11V3_0" = "no"; then + ENABLED_PKCS11V3_0=yes + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_PKCS11_V3_0" + fi + fi + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS" +fi + +if test "$ENABLED_SHA256" = "no" && test "$ENABLED_LMS_PRIVATE" = "yes" +then + echo "LMS/HSS requires SHA-256 support (disabled), disabling LMS/HSS keygen+sign" + ENABLED_LMS_PRIVATE=no +fi + +if test "$ENABLED_LMS_PRIVATE" = "yes" +then + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS_PRIVATE" +fi + # XMSS/XMSS^MT (RFC 8391) verification and public-key import. Requires wolfSSL # built with --enable-xmss (WOLFSSL_HAVE_XMSS). Verify + public-key import only. AC_ARG_ENABLE([xmss], @@ -815,6 +851,7 @@ echo " * HKDF: $ENABLED_HKDF" echo " * ML-DSA: $ENABLED_MLDSA" echo " * ML-KEM: $ENABLED_MLKEM" echo " * LMS/HSS verify: $ENABLED_LMS" +echo " * LMS/HSS sign+keygen (EXP): $ENABLED_LMS_PRIVATE" echo " * XMSS/XMSS^MT verify: $ENABLED_XMSS" echo " * NSS modifications: $ENABLED_NSS" echo " * Default token path: $WOLFPKCS11_DEFAULT_TOKEN_PATH" diff --git a/src/internal.c b/src/internal.c index 3423de7a..87701f26 100644 --- a/src/internal.c +++ b/src/internal.c @@ -304,6 +304,12 @@ struct WP11_Object { byte onToken:1; /* Object on token or session */ byte local:1; /* Locally created object */ word32 opFlag; /* Flags of operations allowed */ +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + word64 statefulStateId; /* Non-zero nonce keying the durable */ + /* signature-state file (0 = none) */ + word64 statefulSigCount; /* Signatures produced so far (bound */ + /* into the state-file AAD) */ +#endif char startDate[8]; /* Start date of usage */ char endDate[8]; /* End data of usage */ @@ -1173,10 +1179,30 @@ typedef struct WP11_FileStoreCtx { XFILE file; int is_write; int has_temp; +#ifdef WOLFPKCS11_LMS_PRIVATE + int durable; /* fsync file + directory before commit succeeds */ +#endif char final_name[WP11_STORE_MAX_PATH]; char temp_name[WP11_STORE_MAX_PATH]; } WP11_FileStoreCtx; +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Mark a write-mode file storage context as requiring full durability (fsync + * of file data + parent directory) before the close path returns success. + * Call after the store is opened for write, before writing. */ +static void wolfPKCS11_Store_SetDurable(void* store, int durable) +{ +#ifndef WOLFPKCS11_TPM_STORE + WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; + if (ctx != NULL) + ctx->durable = durable ? 1 : 0; +#else + (void)store; + (void)durable; +#endif +} +#endif + static void wolfPKCS11_StoreAbortTemp(WP11_FileStoreCtx* ctx) { if (ctx != NULL && ctx->has_temp) { @@ -1202,6 +1228,45 @@ static int wolfPKCS11_StoreCommitTemp(WP11_FileStoreCtx* ctx) if (rename(ctx->temp_name, ctx->final_name) != 0) { ret = READ_ONLY_E; } +#ifdef WOLFPKCS11_LMS_PRIVATE + /* Durable mode: fsync the parent directory so the rename itself is + * persisted (the directory entry, not just the file data). Otherwise a + * crash could lose the rename and roll the signature state back. */ + if (ret == 0 && ctx->durable) { + char dirPath[WP11_STORE_MAX_PATH]; + int n = (int)XSTRLEN(ctx->final_name); + int slash = -1, i; + + for (i = n - 1; i >= 0; i--) { + if (ctx->final_name[i] == '/') { + slash = i; + break; + } + } + if (slash <= 0) { + dirPath[0] = (slash == 0) ? '/' : '.'; + dirPath[1] = '\0'; + } + else if (slash < (int)sizeof(dirPath)) { + XMEMCPY(dirPath, ctx->final_name, slash); + dirPath[slash] = '\0'; + } + else { + ret = READ_ONLY_E; + } + if (ret == 0) { + int dirFd = open(dirPath, O_RDONLY); + if (dirFd >= 0) { + if (fsync(dirFd) != 0) + ret = READ_ONLY_E; + close(dirFd); + } + else { + ret = READ_ONLY_E; + } + } + } +#endif #endif if (ret == 0) { @@ -1778,13 +1843,19 @@ int wolfPKCS11_Store_Open(int type, CK_ULONG id1, CK_ULONG id2, int read, * * @param [in] store Context for operation. */ -void wolfPKCS11_Store_Close(void* store) +/* Close the store and report whether the durable commit succeeded. A plain + * void close would mask a failed fsync/rename, which for stateful signature + * state would risk releasing a signature whose advanced state was not + * persisted. Returns 0 on success, non-zero if a durable flush or the + * temp-file commit failed. */ +static int wolfPKCS11_Store_CloseAndReport(void* store) { #ifdef WOLFPKCS11_TPM_STORE WP11_TpmStore* tpmStore = (WP11_TpmStore*)store; #else WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; #endif + int ret = 0; #ifdef WOLFPKCS11_DEBUG_STORE printf("Store close: %p\n", store); @@ -1796,8 +1867,21 @@ void wolfPKCS11_Store_Close(void* store) #else if (ctx != NULL) { int commitRet = 0; + int fsyncFailed = 0; if (ctx->file != XBADFILE && ctx->file != NULL) { +#ifdef WOLFPKCS11_LMS_PRIVATE + /* Durable mode: flush file data to disk before close so the + * rename in CommitTemp commits a fully-persisted file. */ + if (ctx->durable && ctx->is_write) { + (void)XFFLUSH(ctx->file); + { + int fd = fileno(ctx->file); + if (fd < 0 || fsync(fd) != 0) + fsyncFailed = 1; + } + } +#endif XFCLOSE(ctx->file); ctx->file = XBADFILE; } @@ -1810,16 +1894,26 @@ void wolfPKCS11_Store_Close(void* store) printf("Store commit failed for %s (ret %d)\n", ctx->final_name, commitRet); #endif + ret = commitRet; } } else if (ctx->has_temp) { wolfPKCS11_StoreAbortTemp(ctx); } + if (ret == 0 && fsyncFailed) + ret = READ_ONLY_E; + XMEMSET(ctx, 0, sizeof(*ctx)); XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); } #endif + return ret; +} + +void wolfPKCS11_Store_Close(void* store) +{ + (void)wolfPKCS11_Store_CloseAndReport(store); } /** @@ -3208,6 +3302,51 @@ static int wp11_DecryptData(byte* out, byte* data, int len, byte* key, return ret; } +#ifdef WOLFPKCS11_LMS_PRIVATE +/* AES-GCM encrypt with caller-supplied AAD. The tag is appended to the + * ciphertext (out[len .. len+AES_BLOCK_SIZE-1]). Used for HSS state files so + * the file header (magic, version, parameters, sig count) is bound to the + * ciphertext via authenticated additional data; any tampering with those + * bytes is detected at decrypt time. */ +static int wp11_EncryptDataAAD(byte* out, const byte* data, int len, + const byte* key, int keySz, const byte* iv, int ivSz, + const byte* aad, int aadSz, int devId) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, devId); + if (ret == 0) + ret = wc_AesGcmSetKey(&aes, key, keySz); + if (ret == 0) { + ret = wc_AesGcmEncrypt(&aes, out, data, len, iv, ivSz, out + len, + AES_BLOCK_SIZE, aad, aadSz); + } + wc_AesFree(&aes); + + return ret; +} + +static int wp11_DecryptDataAAD(byte* out, const byte* data, int len, + const byte* key, int keySz, const byte* iv, int ivSz, + const byte* aad, int aadSz, int devId) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, devId); + if (ret == 0) + ret = wc_AesGcmSetKey(&aes, key, keySz); + if (ret == 0) { + ret = wc_AesGcmDecrypt(&aes, out, data, len, iv, ivSz, data + len, + AES_BLOCK_SIZE, aad, aadSz); + } + wc_AesFree(&aes); + + return ret; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + /** * "Decode" the certificate. * @@ -5296,180 +5435,1076 @@ static int wp11_Object_Store_MlKemKey(WP11_Object* object, int tokenId, } #endif /* WOLFPKCS11_MLKEM */ -#ifdef WOLFPKCS11_LMS +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE /* =========================================================================== - * LMS/HSS (RFC 8554): verification and public-key import. - * - * Verify-only: this build imports raw HSS public keys and verifies - * signatures. There is no private-key/keygen/sign or persisted signature - * state here. The public key is self-describing, so wolfSSL derives the - * parameter set from the key bytes on import; the CKA_HSS_LEVELS / - * CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes are optional and, when - * supplied, are validated against the imported key. - * ========================================================================= */ - -/* Map tree height -> RFC 8554 LMS typecode. Returns 0 on unknown height. */ -static CK_LMS_TYPE wp11_HssHeightToLmsType(int h) + * Stateful hash-based signature framework + * =========================================================================== + * + * Shared infrastructure for stateful hash-based signature schemes. The first + * concrete user is LMS/HSS (RFC 8554, this file's WOLFPKCS11_LMS_PRIVATE + * block). Future schemes (XMSS, XMSS^MT — RFC 8391) plug into the same + * framework via thin wrappers. + * + * Contract every stateful-sig scheme MUST honour: + * + * 1. Token-resident only. Session-only stateful keys are unsafe (a process + * crash between sign and cleanup can release a signature whose OTS + * index was never persisted). Reject CKA_TOKEN=FALSE in C_GenerateKeyPair + * with CKR_TEMPLATE_INCONSISTENT. + * + * 2. State file keyed by a per-key 64-bit nonce, NOT by the object's + * position in the token list. The nonce is generated at keygen time + * and stored in the shell file's last 8 bytes. WP11_Object holds a + * copy in `statefulStateId`. Token-list reordering (Add/Remove, + * Slot_Store) must NEVER move or delete the state file. + * + * 3. State persistence is synchronous and durable. The wolfSSL write + * callback writes the encrypted state file with fsync + atomic rename + * + parent-directory fsync (unless WOLFPKCS11_STATEFUL_RELAX_FSYNC=1) + * BEFORE returning success — so wolfSSL never releases a signature + * whose state advance has not reached stable storage. + * + * 4. State file uses AES-GCM (slot-key) with the AAD-bound header below. + * Tampering with parameters or the persisted sigCount fails decrypt. + * + * 5. SigCount tracked in the wp11 layer (`statefulSigCount`), persisted + * inside the AAD-bound header, and used to compute KEYS_REMAINING — + * no reads from wolfSSL key internals (priv_raw layout is not public + * API and varies between backends). + * + * 6. Poison-on-failure. Any state-write failure or sign error clears + * WP11_FLAG_STATEFUL_STATE_VALID; subsequent signs return + * CKR_DEVICE_ERROR until the object is reloaded from disk + * (wolfSSL Reload restores in-memory state from the durable file). + * + * 7. Locking: all post-keygen state-mutating paths run under the token + * lock (priv->lock points at &token->lock for token-resident keys). + * EXCEPTION: during the keygen function itself, priv is thread-local + * (no other thread has the handle yet, and priv->lock has not been + * assigned by WP11_Session_AddObject), so the write callback fires + * unlocked. This is safe only because no concurrent reader can find + * priv before keygen returns. + * + * On-disk layouts: + * + * Shell file (per scheme; non-secret metadata): + * [u32 magic][u32 version][scheme-specific params + pub][u64 stateId] + * The trailer (stateId) is always the last 8 bytes — that's how + * wp11_Stateful_PeekStateIdFromShell finds it without parsing. + * + * State file (AES-GCM, AAD-bound): + * AAD: [u32 magic][u32 version][scheme params (P bytes)][u64 sigCount] + * [u32 ivLen][iv (12 bytes)] + * [u32 ctLen][ciphertext (privSz + 16-byte GCM tag)] + */ + +/* Big-endian u32 reader. Always built — used by verify-only shell parsing. */ +static word32 wp11_Stateful_ReadU32(const byte* p) { - switch (h) { - case 5: return CKL_LMS_SHA256_M32_H5; - case 10: return CKL_LMS_SHA256_M32_H10; - case 15: return CKL_LMS_SHA256_M32_H15; - case 20: return CKL_LMS_SHA256_M32_H20; - case 25: return CKL_LMS_SHA256_M32_H25; - default: return 0; - } + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; } -/* Map Winternitz -> LMOTS typecode. Returns 0 on unknown. */ -static CK_LMOTS_TYPE wp11_HssWToLmotsType(int w) +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* The remaining byte helpers are only needed for sign-capable schemes + * (encoding/decoding the AAD-bound state header and the shell trailer). */ +static word64 wp11_Stateful_ReadU64(const byte* p) { - switch (w) { - case 1: return CKL_LMOTS_SHA256_N32_W1; - case 2: return CKL_LMOTS_SHA256_N32_W2; - case 4: return CKL_LMOTS_SHA256_N32_W4; - case 8: return CKL_LMOTS_SHA256_N32_W8; - default: return 0; - } + return ((word64)p[0] << 56) | ((word64)p[1] << 48) + | ((word64)p[2] << 40) | ((word64)p[3] << 32) + | ((word64)p[4] << 24) | ((word64)p[5] << 16) + | ((word64)p[6] << 8) | (word64)p[7]; } -/* Import a raw HSS public key into the given wolfSSL LmsKey. wolfSSL derives - * the parameter set from the key bytes (RFC 8554 self-describing public key), - * so no parameters need to be set first. On failure the (partially - * initialized) key is NOT freed here; the caller owns it. */ -static int wp11_HssImportPub(LmsKey* key, int devId, const byte* pub, - word32 pubLen) +static void wp11_Stateful_WriteU32(byte* p, word32 v) { - int ret; - - ret = wc_LmsKey_Init(key, NULL, devId); - if (ret == 0) - ret = wc_LmsKey_ImportPubRaw(key, pub, pubLen); - return ret; + p[0] = (byte)(v >> 24); + p[1] = (byte)(v >> 16); + p[2] = (byte)(v >> 8); + p[3] = (byte)v; } -/* Validate caller-supplied HSS parameter attributes against the parameters - * wolfSSL derived from the just-imported public key. Per the PKCS#11 v3.3 HSS - * profile the public-key parameters are the individual CK_ULONG attributes - * CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE (top-level tree height encoding) and - * CKA_HSS_LMOTS_TYPE (Winternitz encoding). Each is optional; a supplied value - * that does not match the key is rejected with BAD_FUNC_ARG. levelsAttr, - * lmsAttr and lmotsAttr are the raw attribute buffers (NULL when absent) with - * lengths levelsLen/lmsLen/lmotsLen. */ -static int wp11_HssCheckParamAttrs(LmsKey* key, - const byte* levelsAttr, word32 levelsLen, - const byte* lmsAttr, word32 lmsLen, - const byte* lmotsAttr, word32 lmotsLen) +static void wp11_Stateful_WriteU64(byte* p, word64 v) { - int ret, levels = 0, height = 0, winternitz = 0; - CK_ULONG v; - - ret = wc_LmsKey_GetParameters(key, &levels, &height, &winternitz); - if (ret != 0) - return ret; - if (levelsAttr != NULL) { - if (levelsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, levelsAttr, sizeof(v)); - if (v != (CK_ULONG)levels) - return BAD_FUNC_ARG; - } - if (lmsAttr != NULL) { - if (lmsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, lmsAttr, sizeof(v)); - if (v != (CK_ULONG)wp11_HssHeightToLmsType(height)) - return BAD_FUNC_ARG; - } - if (lmotsAttr != NULL) { - if (lmotsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, lmotsAttr, sizeof(v)); - if (v != (CK_ULONG)wp11_HssWToLmotsType(winternitz)) - return BAD_FUNC_ARG; - } - return 0; + p[0] = (byte)(v >> 56); + p[1] = (byte)(v >> 48); + p[2] = (byte)(v >> 40); + p[3] = (byte)(v >> 32); + p[4] = (byte)(v >> 24); + p[5] = (byte)(v >> 16); + p[6] = (byte)(v >> 8); + p[7] = (byte)v; } -/* Serialize the public key: keyData holds the raw RFC 8554 HSS public key. */ -static int wp11_Object_Encode_HssKey(WP11_Object* object) -{ - int ret; - word32 pubLen = 0; +/* IV length for the AES-GCM-encrypted state file. The full 96-bit GCM + * standard IV; same for every scheme. */ +#define WP11_STATEFUL_IV_LEN 12 - if (object->objClass != CKO_PUBLIC_KEY) - return NOT_AVAILABLE_E; /* verify-only: no private-key objects */ +/* Cached env-var read once at first use: when 1, the state-write callback + * skips fsync() (file and directory). DANGEROUS: documented as non-production + * only. Default is to always fsync. The variable applies to ALL stateful + * hash-based signature schemes (LMS/HSS today; XMSS in the future) — there + * is no per-scheme knob, since the durability requirement is identical. */ +static int wp11_StatefulRelaxFsync = -1; - ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); - if (ret == 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) - ret = MEMORY_E; - } - if (ret == 0) { - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, object->keyData, - &pubLen); - if (ret == 0) - object->keyDataLen = (int)pubLen; - } - /* On failure release the buffer so a later store does not skip re-encoding - * (keyData != NULL) and persist an uninitialised buffer with a stale - * length. This is public key material only, so no zeroization is needed. */ - if (ret != 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; +static int wp11_StatefulShouldFsync(void) +{ + if (wp11_StatefulRelaxFsync < 0) { +#ifndef WOLFPKCS11_NO_ENV + const char* v = XGETENV("WOLFPKCS11_STATEFUL_RELAX_FSYNC"); + wp11_StatefulRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; +#else + wp11_StatefulRelaxFsync = 0; +#endif + if (wp11_StatefulRelaxFsync) { + /* Single, prominent stderr line — the README documents this is + * non-production. Operators who reach this path must see it. */ + fprintf(stderr, + "wolfPKCS11: WARNING: WOLFPKCS11_STATEFUL_RELAX_FSYNC=1 is " + "set. Stateful hash-signature state writes will skip fsync; " + "a power loss can roll back the leaf-index advance and cause " + "OTS-key REUSE. Do NOT use this setting in production.\n"); + } } - return ret; + return wp11_StatefulRelaxFsync ? 0 : 1; } -/* Reconstruct the live wolfSSL key from the serialized public key in keyData. */ -static int wp11_Object_Decode_HssKey(WP11_Object* object) +/* State files use a 64-bit per-key nonce. The public storage entry points + * (wolfPKCS11_Store_Open / Store_Remove) take ids as CK_ULONG, which is + * 32-bit on LLP64 platforms (Windows) — passing the nonce through them + * truncates it and shrinks the collision space from 2^64 to 2^32. The + * helpers below thread word64 through, formatting the path with %016llx, + * so the nonce stays 64-bit on every platform. + * + * WOLFPKCS11_TPM_STORE is forbidden combined with WOLFPKCS11_LMS_PRIVATE + * (compile-time #error in wolfpkcs11/internal.h), so this code path is + * file-backed-storage only. */ +static int wp11_Stateful_StateFile_Path(const char* schemeFilePrefix, + int tokenId, word64 stateId, char* name, int nameLen) { - int ret; - - if (object->objClass != CKO_PUBLIC_KEY) - return NOT_AVAILABLE_E; + const char* str = NULL; + char homePath[256]; + /* "/wp11__priv_state_%016lx_%016llx". Reserve enough for + * the longest scheme prefix we might use ("xmssmtkey" = 9). */ + enum { WP11_STATEFUL_STATE_SUFFIX_RESERVE = 72 }; - /* Reload from the stored raw public key; wolfSSL re-derives the parameter - * set from the key bytes (no separate parameters are persisted). */ - ret = wp11_HssImportPub(object->data.lmsKey, object->devId, - object->keyData, (word32)object->keyDataLen); - /* The object owns the live key on all paths (the import helper never frees - * it), so encoded stays 0 for WP11_Object_Free to do the single free. */ - object->encoded = 0; - return ret; + if (schemeFilePrefix == NULL || schemeFilePrefix[0] == '\0') + return -1; +#ifndef WOLFPKCS11_NO_ENV + str = XGETENV("WOLFPKCS11_TOKEN_PATH"); +#endif +#ifdef WOLFPKCS11_NSS + if (str == NULL) + str = storeDir; +#endif + if (str == NULL) { + const char* homeDir = NULL; +#if defined(_WIN32) || defined(_MSC_VER) + homeDir = XGETENV("%APPDIR%"); + if (homeDir != NULL && XSTRLEN(homeDir) <= sizeof(homePath) - 13) { + int len = XSNPRINTF(homePath, sizeof(homePath), + "%s\\wolfPKCS11", homeDir); + if (len > 0 && len < (int)sizeof(homePath)) + str = homePath; + } +#else + homeDir = XGETENV("HOME"); + if (homeDir != NULL && XSTRLEN(homeDir) <= sizeof(homePath) - 13) { + int len = XSNPRINTF(homePath, sizeof(homePath), + "%s/.wolfPKCS11", homeDir); + if (len > 0 && len < (int)sizeof(homePath)) + str = homePath; + } +#endif + } +#ifdef WOLFPKCS11_DEFAULT_TOKEN_PATH + if (str == NULL) + str = WC_STRINGIFY(WOLFPKCS11_DEFAULT_TOKEN_PATH); +#endif + if (str == NULL) + return -1; + if (nameLen <= WP11_STATEFUL_STATE_SUFFIX_RESERVE) + return -1; + if (XSTRLEN(str) + XSTRLEN(schemeFilePrefix) > + (size_t)(nameLen - WP11_STATEFUL_STATE_SUFFIX_RESERVE - 1)) { + return -1; + } + return XSNPRINTF(name, nameLen, + "%s/wp11_%s_priv_state_%016lx_%016llx", + str, schemeFilePrefix, (unsigned long)tokenId, + (unsigned long long)stateId); } -static int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) +/* Open the state file for read or atomic-rename write. Mirrors + * wolfPKCS11_Store_OpenSz's file-mode body but uses the word64-safe path. */ +static int wp11_Stateful_StateFile_Open(const char* schemeFilePrefix, + int tokenId, word64 stateId, int read, void** store) { int ret; - void* storage = NULL; + char name[WP11_STORE_MAX_PATH] = ""; + WP11_FileStoreCtx* ctx = NULL; - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PUB, tokenId, - objId, &storage); +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) + return NOT_AVAILABLE_E; +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + ret = 0; + else + ret = -1; if (ret == 0) { - ret = wp11_storage_read_alloc_array(storage, &object->keyData, - &object->keyDataLen); - wp11_storage_close(storage); + ctx = (WP11_FileStoreCtx*)XMALLOC(sizeof(*ctx), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (ctx == NULL) + ret = MEMORY_E; } - return ret; -} - -static int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret = 0; - void* storage = NULL; - - if (object->keyData == NULL) - ret = wp11_Object_Encode_HssKey(object); - if (ret == 0) - ret = wp11_storage_open(WOLFPKCS11_STORE_HSSKEY_PUB, tokenId, objId, - object->keyDataLen, &storage); if (ret == 0) { - ret = wp11_storage_write_array(storage, object->keyData, + char dirPath[WP11_STORE_MAX_PATH]; + const char* lastSlash = NULL; + size_t nameSz = XSTRLEN(name); + size_t i; + XMEMSET(ctx, 0, sizeof(*ctx)); + ctx->file = XBADFILE; + ctx->is_write = (read == 0); + if (nameSz >= sizeof(ctx->final_name)) + ret = READ_ONLY_E; + else + XMEMCPY(ctx->final_name, name, nameSz + 1); + + if (ret == 0 && read) { + ctx->file = XFOPEN(name, "rb"); + if (ctx->file == NULL) + ret = NOT_AVAILABLE_E; + } + else if (ret == 0) { + for (i = 0; i < nameSz; i++) { + if (name[i] == '/' || name[i] == '\\') + lastSlash = &name[i]; + } + if (lastSlash == NULL) { + ret = READ_ONLY_E; + } + else { + int dirLen = (int)(lastSlash - name); + if (dirLen <= 0 || dirLen >= (int)sizeof(dirPath)) { + ret = READ_ONLY_E; + } + else { + XMEMCPY(dirPath, name, dirLen); + dirPath[dirLen] = '\0'; + ret = wolfPKCS11_StoreEnsureDir(dirPath); + if (ret == 0) + ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); + } + } + } + } + if (ret == 0 && (ctx->file == NULL || ctx->file == XBADFILE)) + ret = READ_ONLY_E; + if (ret == 0) { + *store = ctx; + } + else if (ctx != NULL) { + if (ctx->file != NULL && ctx->file != XBADFILE) + XFCLOSE(ctx->file); + if (ctx->has_temp) + wolfPKCS11_StoreAbortTemp(ctx); + XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + return ret; +} + +static int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, + int tokenId, word64 stateId) +{ + int ret; + char name[WP11_STORE_MAX_PATH]; + +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) + return NOT_AVAILABLE_E; +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + return remove(name); + return -1; +} + +/* Sanity caps for inputs to the generic state-blob helpers. The wolfSSL + * state buffer for any practical scheme is well below 1 MiB; the scheme + * parameter region is small (HSS = 12 bytes, XMSS = 4-12 bytes). Reject + * absurd inputs up front so the size-arithmetic below cannot overflow. */ +#define WP11_STATEFUL_PRIV_MAX (1U << 20) /* 1 MiB */ +#define WP11_STATEFUL_PARAMS_MAX 256U + +/* Persist the encrypted state file durably. Caller passes the scheme's + * (schemeFilePrefix, magic, version) and a packed scheme-params buffer + * that is bound into the GCM AAD. On a successful return the state file + * at (slot_id, statefulStateId) is fsync'd and renamed atomically. + * + * AAD-bound layout (caught by AES-GCM tag): + * [u32 magic][u32 version][u32 schemeParamLen][schemeParamBytes] + * [u64 sigCount] + * Including schemeParamLen in the AAD diagnoses thin-wrapper version skew + * cleanly (a length mismatch surfaces as a header-mismatch BAD_FUNC_ARG + * before decrypt rather than as opaque AES-GCM tag failure). */ +static int wp11_Stateful_WriteStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* schemeParamBytes, word32 schemeParamLen, + const byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* hdr = NULL; + word32 hdrLen; + byte* ct = NULL; + word32 ctLen; + int tokenId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + /* Stateful keys are token-resident only — reject any attempt to + * persist state for a session-only object. The HSS write CB enforces + * this too, but the framework helper enforces it for all schemes. */ + if (!o->onToken) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (schemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + /* Defensive caps: prevent size-arithmetic overflow. */ + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; + + ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) + return MEMORY_E; + + wp11_Stateful_WriteU32(hdr + 0, magic); + wp11_Stateful_WriteU32(hdr + 4, version); + wp11_Stateful_WriteU32(hdr + 8, schemeParamLen); + if (schemeParamLen > 0) + XMEMCPY(hdr + 12, schemeParamBytes, schemeParamLen); + wp11_Stateful_WriteU64(hdr + 12 + schemeParamLen, o->statefulSigCount); + + /* Fresh GCM nonce per write (NEVER reuse object->iv). */ + ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_STATEFUL_IV_LEN); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return MEMORY_E; + } + + ret = wp11_EncryptDataAAD(ct, priv, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); + if (ret != 0) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag — + * unused for file storage, but kept for parity with TPM_STORE. */ + (void)(hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen); + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 0, &storage); + if (ret == 0) { + if (wp11_StatefulShouldFsync()) + wolfPKCS11_Store_SetDurable(storage, 1); + ret = wp11_storage_write(storage, hdr, (int)hdrLen); + if (ret == 0) { + byte ivLenBuf[4]; + wp11_Stateful_WriteU32(ivLenBuf, WP11_STATEFUL_IV_LEN); + ret = wp11_storage_write(storage, ivLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, iv, WP11_STATEFUL_IV_LEN); + if (ret == 0) { + byte ctLenBuf[4]; + wp11_Stateful_WriteU32(ctLenBuf, ctLen); + ret = wp11_storage_write(storage, ctLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, ct, (int)ctLen); + /* Capture the close-and-commit result. A void close would mask + * a failed rename/fsync; that would let wolfSSL release a signature + * whose state advance never reached durable storage. */ + { + int closeRet = wolfPKCS11_Store_CloseAndReport(storage); + if (ret == 0) + ret = closeRet; + } + } + + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +/* Read and decrypt the state file. The full header is verified as AAD by + * the AES-GCM tag; tampering with magic, version, scheme params, scheme- + * params length, or the persisted sigCount fails decrypt. The expected + * scheme params are passed by the caller (typically: re-pack from the + * in-memory wolfSSL key); a mismatch returns BAD_FUNC_ARG before decrypt. + * The decrypted sigCount is restored into o->statefulSigCount. */ +static int wp11_Stateful_ReadStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* expectedSchemeParamBytes, word32 schemeParamLen, + byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte* hdr = NULL; + word32 hdrLen; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* ct = NULL; + word32 m, v, persistedParamLen, ivLen, ctLen; + word64 sigCount = 0; + int tokenId; + + if (o == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (expectedSchemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; + + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) { + wc_ForceZero(priv, privSz); + return MEMORY_E; + } + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 1, &storage); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_ForceZero(priv, privSz); + return ret; + } + + ret = wp11_storage_read(storage, hdr, (int)hdrLen); + if (ret == 0) { + m = wp11_Stateful_ReadU32(hdr); + v = wp11_Stateful_ReadU32(hdr + 4); + persistedParamLen = wp11_Stateful_ReadU32(hdr + 8); + sigCount = wp11_Stateful_ReadU64(hdr + 12 + schemeParamLen); + if (m != magic || v != version) { + ret = BAD_FUNC_ARG; + } + else if (persistedParamLen != schemeParamLen) { + /* Scheme-params length mismatch — diagnoses thin-wrapper + * version skew before AES-GCM tag failure. */ + ret = BAD_FUNC_ARG; + } + else if (schemeParamLen > 0 && + XMEMCMP(hdr + 12, expectedSchemeParamBytes, schemeParamLen) + != 0) { + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ivLen = wp11_Stateful_ReadU32(buf); + if (ivLen != WP11_STATEFUL_IV_LEN) + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) + ret = wp11_storage_read(storage, iv, WP11_STATEFUL_IV_LEN); + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ctLen = wp11_Stateful_ReadU32(buf); + if (ctLen < AES_BLOCK_SIZE || + ctLen - AES_BLOCK_SIZE != privSz) { + ret = BAD_FUNC_ARG; + } + } + } + if (ret == 0) { + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) + ret = MEMORY_E; + } + if (ret == 0) + ret = wp11_storage_read(storage, ct, (int)ctLen); + if (ret == 0) { + ret = wp11_DecryptDataAAD(priv, ct, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); + /* AES-GCM authentication failure manifests here. */ + } + wp11_storage_close(storage); + if (ct != NULL) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ret == 0) { + /* AES-GCM verified the header; sigCount cannot have been rolled + * back without detection. Restore into the in-memory object. */ + o->statefulSigCount = sigCount; + } + if (ret != 0) + wc_ForceZero(priv, privSz); + return ret; +} + +/* Recover the per-key stateId from the on-disk shell file. The shell + * format is otherwise scheme-specific, but by contract the LAST 8 bytes + * are always the u64 stateId. Used by Unstore to find the state file + * before deleting it, when the in-memory object hasn't been decoded yet. + * + * Validates the shell's magic + version against caller-supplied values + * before returning the trailer. Without this check, a truncated, replaced, + * or wrong-scheme file at the same (tokenId, objId) would yield a garbage + * stateId that the caller then passes to Store_Remove — silently no-op'ing + * or deleting the wrong file while the real state file orphans on disk. + * + * Returns BAD_FUNC_ARG if magic or version don't match (e.g., wrong scheme, + * file truncated below the header size, or the file is something else). */ +static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, + int storeTypeShell, word32 expectedShellMagic, word32 expectedShellVersion, + word64* outStateId) +{ + int ret; + void* storage = NULL; + unsigned char* buf = NULL; + int bufLen = 0; + + if (outStateId == NULL) + return BAD_FUNC_ARG; + *outStateId = 0; + + ret = wp11_storage_open_readonly(storeTypeShell, + (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); + if (ret != 0) + return ret; + ret = wp11_storage_read_alloc_array(storage, &buf, &bufLen); + wp11_storage_close(storage); + if (ret != 0) + return ret; + + /* Need at least [u32 magic][u32 version] + [u64 stateId trailer]. */ + if (bufLen < 16) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + if (wp11_Stateful_ReadU32(buf) != expectedShellMagic || + wp11_Stateful_ReadU32(buf + 4) != expectedShellVersion) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + *outStateId = wp11_Stateful_ReadU64(buf + (bufLen - 8)); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return 0; +} + +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE (inner) */ +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE (outer) */ +#ifdef WOLFPKCS11_LMS +/* =========================================================================== + * LMS/HSS (RFC 8554): verification and public-key import. + * + * Verify-only: this build imports raw HSS public keys and verifies + * signatures. There is no private-key/keygen/sign or persisted signature + * state here. The public key is self-describing, so wolfSSL derives the + * parameter set from the key bytes on import; the CKA_HSS_LEVELS / + * CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes are optional and, when + * supplied, are validated against the imported key. + * ========================================================================= */ + +/* Map tree height -> RFC 8554 LMS typecode. Returns 0 on unknown height. */ +static CK_LMS_TYPE wp11_HssHeightToLmsType(int h) +{ + switch (h) { + case 5: return CKL_LMS_SHA256_M32_H5; + case 10: return CKL_LMS_SHA256_M32_H10; + case 15: return CKL_LMS_SHA256_M32_H15; + case 20: return CKL_LMS_SHA256_M32_H20; + case 25: return CKL_LMS_SHA256_M32_H25; + default: return 0; + } +} + +/* Map Winternitz -> LMOTS typecode. Returns 0 on unknown. */ +static CK_LMOTS_TYPE wp11_HssWToLmotsType(int w) +{ + switch (w) { + case 1: return CKL_LMOTS_SHA256_N32_W1; + case 2: return CKL_LMOTS_SHA256_N32_W2; + case 4: return CKL_LMOTS_SHA256_N32_W4; + case 8: return CKL_LMOTS_SHA256_N32_W8; + default: return 0; + } +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Forward parameter maps (RFC 8554 typecode -> height / Winternitz), used by + * keygen to translate the CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes. + * Return 0 on unknown. */ +static int wp11_HssLmsTypeToHeight(CK_LMS_TYPE t) +{ + switch (t) { + case CKL_LMS_SHA256_M32_H5: return 5; + case CKL_LMS_SHA256_M32_H10: return 10; + case CKL_LMS_SHA256_M32_H15: return 15; + case CKL_LMS_SHA256_M32_H20: return 20; + case CKL_LMS_SHA256_M32_H25: return 25; + default: return 0; + } +} + +static int wp11_HssLmotsTypeToW(CK_LMOTS_TYPE t) +{ + switch (t) { + case CKL_LMOTS_SHA256_N32_W1: return 1; + case CKL_LMOTS_SHA256_N32_W2: return 2; + case CKL_LMOTS_SHA256_N32_W4: return 4; + case CKL_LMOTS_SHA256_N32_W8: return 8; + default: return 0; + } +} + +/* On-disk magic + version for the HSS private-key shell file: non-secret + * metadata (parameters + cached public key) with the 8-byte stateId nonce as + * the last 8 bytes. Persisted at keygen so token load can call + * wc_LmsKey_SetParameters before wc_LmsKey_Reload and locate the state file. */ +#define WP11_HSS_SHELL_MAGIC 0x48535350UL /* "HSSP" */ +#define WP11_HSS_SHELL_VERSION 1U + +/* On-disk magic + version for the encrypted HSS state file. The header + * (levels/height/winternitz + persisted signature counter) is bound into the + * AES-GCM tag via AAD, so tampering is detected at decrypt time. */ +#define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ +#define WP11_HSS_STATE_VERSION 1U + +/* HSS scheme param region inside the AAD-bound state header: 3 u32s. */ +#define WP11_HSS_SCHEME_PARAM_LEN 12U + +/* Serialize the private-key shell header: + * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] + * [u32 pubLen][pub]. The 8-byte stateId trailer is appended by the caller. + * Returns bytes written, or BUFFER_E. */ +static int wp11_HssPackShellHeader(byte* out, word32 outLen, int levels, + int height, int winternitz, const byte* pub, word32 pubLen) +{ + word32 needed = 4 + 4 + 4 + 4 + 4 + 4 + pubLen; + word32 idx = 0; + + if (out == NULL || outLen < needed) + return BUFFER_E; + wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_MAGIC); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_VERSION); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)levels); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)height); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)winternitz); idx += 4; + wp11_Stateful_WriteU32(out + idx, pubLen); idx += 4; + if (pub != NULL && pubLen > 0) + XMEMCPY(out + idx, pub, pubLen); + idx += pubLen; + return (int)idx; +} + +/* Parse the private-key shell header. On success *pub points into the input + * buffer (no allocation) and *pubLen is its length. */ +static int wp11_HssParseShellHeader(const byte* in, word32 inLen, + int* levels, int* height, int* winternitz, + const byte** pub, word32* pubLen) +{ + word32 idx = 0; + word32 magic, version, pl; + + if (in == NULL || inLen < 24) + return BAD_FUNC_ARG; + magic = wp11_Stateful_ReadU32(in + idx); idx += 4; + version = wp11_Stateful_ReadU32(in + idx); idx += 4; + if (magic != WP11_HSS_SHELL_MAGIC || version != WP11_HSS_SHELL_VERSION) + return BAD_FUNC_ARG; + *levels = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *height = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *winternitz = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + pl = wp11_Stateful_ReadU32(in + idx); idx += 4; + /* Cap pubLen against the max HSS public-key size so a malformed shell + * cannot force a giant ImportPubRaw. */ + if (pl > HSS_MAX_PUBLIC_KEY_LEN) + return BAD_FUNC_ARG; + if (idx + pl > inLen) + return BAD_FUNC_ARG; + *pub = in + idx; + *pubLen = pl; + return 0; +} + +/* Pack the HSS scheme params (levels|height|winternitz, big-endian u32s) into + * the AAD-bound region of the state header. */ +static void wp11_Hss_PackSchemeParams(byte out[WP11_HSS_SCHEME_PARAM_LEN], + int levels, int height, int winternitz) +{ + wp11_Stateful_WriteU32(out + 0, (word32)levels); + wp11_Stateful_WriteU32(out + 4, (word32)height); + wp11_Stateful_WriteU32(out + 8, (word32)winternitz); +} + +/* HSS state-file write, keyed on disk by (slot_id, statefulStateId). Thin + * wrapper over the generic durable state-blob writer. */ +static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, + word32 privSz) +{ + int levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_WriteStateBlob(o, "hsskey", + (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); +} + +/* HSS state-file read. Validates the in-memory key's params against the + * AAD-bound header before AES-GCM decrypt. */ +static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) +{ + int levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_ReadStateBlob(o, "hsskey", + (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); +} + +/* wolfSSL write callback: invoked by wc_LmsKey_MakeKey and wc_LmsKey_Sign + * after each state advance. Must return WC_LMS_RC_SAVED_TO_NV_MEMORY on + * success; any other value aborts the sign (wolfSSL zeroizes the signature). */ +static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + + if (o == NULL || priv == NULL || privSz == 0) + return WC_LMS_RC_BAD_ARG; + /* Stateful HSS keys are always token-resident in this build. */ + if (!o->onToken || o->statefulStateId == 0) + return WC_LMS_RC_WRITE_FAIL; + if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_WRITE_FAIL; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + + if (o == NULL) + return WC_LMS_RC_BAD_ARG; + if (wp11_Hss_ReadStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_READ_FAIL; + return WC_LMS_RC_READ_TO_MEMORY; +} + +/* Recover the statefulStateId from the on-disk HSS shell (last 8 bytes), + * validating the shell magic+version first. Used by orphan cleanup when the + * object has not been decoded into memory. */ +static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, + word64* outStateId) +{ + return wp11_Stateful_PeekStateIdFromShell(tokenId, objId, + WOLFPKCS11_STORE_HSSKEY_PRIV, (word32)WP11_HSS_SHELL_MAGIC, + WP11_HSS_SHELL_VERSION, outStateId); +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Import a raw HSS public key into the given wolfSSL LmsKey. wolfSSL derives + * the parameter set from the key bytes (RFC 8554 self-describing public key), + * so no parameters need to be set first. On failure the (partially + * initialized) key is NOT freed here; the caller owns it. */ +static int wp11_HssImportPub(LmsKey* key, int devId, const byte* pub, + word32 pubLen) +{ + int ret; + + ret = wc_LmsKey_Init(key, NULL, devId); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(key, pub, pubLen); + return ret; +} + +/* Validate caller-supplied HSS parameter attributes against the parameters + * wolfSSL derived from the just-imported public key. Per the PKCS#11 v3.3 HSS + * profile the public-key parameters are the individual CK_ULONG attributes + * CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE (top-level tree height encoding) and + * CKA_HSS_LMOTS_TYPE (Winternitz encoding). Each is optional; a supplied value + * that does not match the key is rejected with BAD_FUNC_ARG. levelsAttr, + * lmsAttr and lmotsAttr are the raw attribute buffers (NULL when absent) with + * lengths levelsLen/lmsLen/lmotsLen. */ +static int wp11_HssCheckParamAttrs(LmsKey* key, + const byte* levelsAttr, word32 levelsLen, + const byte* lmsAttr, word32 lmsLen, + const byte* lmotsAttr, word32 lmotsLen) +{ + int ret, levels = 0, height = 0, winternitz = 0; + CK_ULONG v; + + ret = wc_LmsKey_GetParameters(key, &levels, &height, &winternitz); + if (ret != 0) + return ret; + if (levelsAttr != NULL) { + if (levelsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, levelsAttr, sizeof(v)); + if (v != (CK_ULONG)levels) + return BAD_FUNC_ARG; + } + if (lmsAttr != NULL) { + if (lmsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, lmsAttr, sizeof(v)); + if (v != (CK_ULONG)wp11_HssHeightToLmsType(height)) + return BAD_FUNC_ARG; + } + if (lmotsAttr != NULL) { + if (lmotsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, lmotsAttr, sizeof(v)); + if (v != (CK_ULONG)wp11_HssWToLmotsType(winternitz)) + return BAD_FUNC_ARG; + } + return 0; +} + +/* Serialize the public key: keyData holds the raw RFC 8554 HSS public key. */ +static int wp11_Object_Encode_HssKey(WP11_Object* object) +{ + int ret; + word32 pubLen = 0; + + if (object->objClass == CKO_PUBLIC_KEY) { + /* Public key: raw RFC 8554 public key, unencrypted (Step-1 format). */ + ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); + if (ret == 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, object->keyData, + &pubLen); + if (ret == 0) + object->keyDataLen = (int)pubLen; + } + /* On failure release the buffer so a later store does not skip + * re-encoding and persist an uninitialised buffer. Public key material + * only, so no zeroization needed. */ + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + /* Private key: a non-secret metadata shell (params + cached public + * key + 8-byte stateId trailer). The secret one-time-signature state + * is NEVER written to keyData — it lives only in the AES-GCM state + * file written by the wolfSSL write callback. */ + int levels = 0, height = 0, winternitz = 0; + byte pub[HSS_MAX_PUBLIC_KEY_LEN]; + int hdrLen; + word32 needed; + + pubLen = sizeof(pub); + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, + &winternitz); + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, pub, &pubLen); + if (ret == 0 && object->statefulStateId == 0) + ret = BAD_FUNC_ARG; + if (ret == 0) { + needed = 24 + pubLen + 8; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(needed, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + hdrLen = wp11_HssPackShellHeader(object->keyData, needed, levels, + height, winternitz, pub, pubLen); + if (hdrLen < 0) { + ret = hdrLen; + } + else { + wp11_Stateful_WriteU64(object->keyData + hdrLen, + object->statefulStateId); + object->keyDataLen = hdrLen + 8; + } + } + wc_ForceZero(pub, sizeof(pub)); + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; + } +#endif + return NOT_AVAILABLE_E; +} + +/* Reconstruct the live wolfSSL key from keyData. Public keys import the raw + * key directly; private keys parse the shell, reload the durable signature + * state (via the read callback), and mark the state valid. */ +static int wp11_Object_Decode_HssKey(WP11_Object* object) +{ + int ret; + + if (object->objClass == CKO_PUBLIC_KEY) { + /* Raw RFC 8554 public key; wolfSSL re-derives the parameter set. */ + ret = wp11_HssImportPub(object->data.lmsKey, object->devId, + object->keyData, (word32)object->keyDataLen); + object->encoded = (ret != 0) ? 1 : 0; + return ret; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + int levels = 0, height = 0, winternitz = 0; + const byte* pub = NULL; + word32 pubLen = 0; + word32 idx = 0; + + ret = wp11_HssParseShellHeader(object->keyData, + (word32)object->keyDataLen, &levels, &height, &winternitz, + &pub, &pubLen); + if (ret == 0) { + idx = 24 + pubLen; /* 8-byte stateId trailer follows the pub */ + if (idx + 8 > (word32)object->keyDataLen) + ret = BAD_FUNC_ARG; + } + if (ret == 0) { + object->statefulStateId = + wp11_Stateful_ReadU64(object->keyData + idx); + if (object->statefulStateId == 0) + ret = BAD_FUNC_ARG; + } + if (ret == 0) + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, object->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, height, + winternitz); + /* Wire callbacks before Reload: Reload reads the encrypted state via + * the read callback; the write callback is needed for later signing. */ + if (ret == 0) + ret = wc_LmsKey_SetWriteCb(object->data.lmsKey, + wp11_Hss_WriteState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetReadCb(object->data.lmsKey, + wp11_Hss_ReadState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetContext(object->data.lmsKey, object); + if (ret == 0) + ret = wc_LmsKey_Reload(object->data.lmsKey); + /* Reload does not restore key->pub; import the cached shell pub so + * ExportPubRaw/Sign stay consistent with the stored record. */ + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(object->data.lmsKey, pub, pubLen); + if (ret == 0) { + object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + object->encoded = 0; + } + else { + /* Clear the validity flag so a later Sign cannot use a half-built + * key, and free the wolfSSL key. encoded=1 stops Object_Free from + * freeing the wolfSSL key a second time. */ + object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + wc_LmsKey_Free(object->data.lmsKey); + object->encoded = 1; + } + return ret; + } +#endif + return NOT_AVAILABLE_E; +} + +static int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; +#endif + ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); + if (ret == 0) { + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +static int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret = 0; + void* storage = NULL; + int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; +#endif + if (object->keyData == NULL) + ret = wp11_Object_Encode_HssKey(object); + if (ret == 0) + ret = wp11_storage_open(storeType, tokenId, objId, + object->keyDataLen, &storage); + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, object->keyDataLen); wp11_storage_close(storage); } @@ -5596,6 +6631,202 @@ int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, return ret; } +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Generate an HSS key pair. levels/lmsType/lmotsType come from the v3.3 + * keygen template attributes; wolfSSL requires uniform parameters across + * levels. The private key's one-time-signature state is durably persisted at + * genesis (via the write callback invoked inside wc_LmsKey_MakeKey) before + * this returns success. */ +int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + CK_ULONG levels, CK_LMS_TYPE lmsType, + CK_LMOTS_TYPE lmotsType, WP11_Slot* slot) +{ + int ret; + int nLevels, height, winternitz; + WC_RNG rng; + byte pubBuf[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubLen = sizeof(pubBuf); + int stateWritten = 0; + + if (pub == NULL || priv == NULL || slot == NULL || + pub->data.lmsKey == NULL || priv->data.lmsKey == NULL) { + return BAD_FUNC_ARG; + } + + /* Translate the v3.3 typecodes to wolfSSL (levels, height, winternitz), + * enforcing the wolfSSL level limit. */ + nLevels = (int)levels; + height = wp11_HssLmsTypeToHeight(lmsType); + winternitz = wp11_HssLmotsTypeToW(lmotsType); + if (nLevels < 1 || nLevels > LMS_MAX_LEVELS || height == 0 || + winternitz == 0) { + return BAD_FUNC_ARG; + } + + /* Generate the per-key 64-bit nonce keying the state file. Must be + * non-zero (0 is the "none" sentinel); retry on the vanishing chance of + * zero, else treat as an RNG failure. */ + { + byte nonceBuf[8]; + int tries; + for (tries = 0; tries < 4; tries++) { + ret = WP11_Slot_GenerateRandom(slot, nonceBuf, sizeof(nonceBuf)); + if (ret != 0) + return ret; + priv->statefulStateId = wp11_Stateful_ReadU64(nonceBuf); + if (priv->statefulStateId != 0) + break; + } + if (priv->statefulStateId == 0) + return RNG_FAILURE_E; + } + + /* Mark on-token so the write CB commits; AddObject formalizes the + * registration shortly. priv->lock is assigned later by Session_AddObject; + * during keygen no other thread holds a handle, so the CB fires unlocked. */ + priv->onToken = 1; + priv->statefulSigCount = 0; + + ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, priv->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(priv->data.lmsKey, nLevels, height, + winternitz); + /* Wire callbacks BEFORE MakeKey so genesis state is persisted durably. */ + if (ret == 0) + ret = wc_LmsKey_SetWriteCb(priv->data.lmsKey, wp11_Hss_WriteState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetReadCb(priv->data.lmsKey, wp11_Hss_ReadState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetContext(priv->data.lmsKey, priv); + + if (ret == 0) + ret = Rng_New(&slot->token.rng, &slot->token.rngLock, &rng); + if (ret == 0) { + /* MakeKey invokes the write CB to persist genesis state; on success + * the genesis state file is durable on disk. */ + ret = wc_LmsKey_MakeKey(priv->data.lmsKey, &rng); + Rng_Free(&rng); + if (ret == 0) + stateWritten = 1; + } + + /* Mirror parameters + public key into the public-key object. */ + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(priv->data.lmsKey, pubBuf, &pubLen); + if (ret == 0) + ret = wc_LmsKey_Init(pub->data.lmsKey, NULL, pub->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(pub->data.lmsKey, nLevels, height, + winternitz); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(pub->data.lmsKey, pubBuf, pubLen); + + if (ret == 0) { + priv->local = pub->local = 1; + priv->keyGenMech = pub->keyGenMech = CKM_HSS_KEY_PAIR_GEN; + priv->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + } + else { + wc_LmsKey_Free(priv->data.lmsKey); + if (stateWritten && priv->statefulStateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, + priv->statefulStateId); + } + priv->statefulStateId = 0; + } + + wc_ForceZero(pubBuf, sizeof(pubBuf)); + return ret; +} + +/* Sign a raw message with an HSS private key. State is advanced and persisted + * (via the write CB inside wc_LmsKey_Sign) BEFORE the signature is returned. + * On any failure the in-memory state is poisoned (STATE_VALID cleared) to + * force a reload from durable storage before the next attempt. Token-resident + * keys always have priv->lock == &token->lock. */ +int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, + word32* sigLen, WP11_Object* priv) +{ + int ret = 0; + word32 outLen = 0; + + if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || + sigLen == NULL || priv->lock == NULL) { + return BAD_FUNC_ARG; + } + + WP11_Lock_LockRW(priv->lock); + + if ((priv->opFlag & WP11_FLAG_STATEFUL_STATE_VALID) == 0) { + ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ + } + if (ret == 0) { + word64 prevCount = priv->statefulSigCount; + priv->statefulSigCount = prevCount + 1; + outLen = *sigLen; + ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, + (int)dataLen); + if (ret == 0) { + *sigLen = outLen; + } + else { + /* Write CB failed (durable state may lag the in-memory advance) + * or the key is exhausted. Poison until reloaded; zero any bytes + * wolfSSL wrote and revert the in-memory counter. */ + priv->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + if (outLen > 0) + XMEMSET(sig, 0, outLen); + priv->statefulSigCount = prevCount; + } + } + + WP11_Lock_UnlockRW(priv->lock); + return ret; +} + +/* Remaining one-time keys for an HSS private key. Uses the wp11-layer + * statefulSigCount (persisted in the GCM-authenticated state header) rather + * than reaching into wolfSSL LmsKey internals. total = 2^(levels*height); + * saturates at UINT32_MAX for large parameter sets. */ +int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) +{ + int levels = 0, height = 0, winternitz = 0; + int totalH; + word64 used, total; + + if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) + return BAD_FUNC_ARG; + + if (wc_LmsKey_SigsLeft(key->data.lmsKey) == 0) { + *remaining = 0; + return 0; + } + if (wc_LmsKey_GetParameters(key->data.lmsKey, &levels, &height, + &winternitz) != 0) { + *remaining = 0; + return BAD_FUNC_ARG; + } + totalH = levels * height; + if (totalH <= 0) { + *remaining = 0; + return BAD_FUNC_ARG; + } + if (totalH >= 64) { + *remaining = 0xFFFFFFFFU; + return 0; + } + total = ((word64)1) << totalH; + used = key->statefulSigCount; + if (used >= total) + *remaining = 0; + else if ((total - used) > 0xFFFFFFFFULL) + *remaining = 0xFFFFFFFFU; + else + *remaining = (word32)(total - used); + return 0; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + /* Serve HSS-specific attributes from a public-key object. */ static int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, byte* data, CK_ULONG* len) @@ -6900,6 +8131,25 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) #endif #ifdef WOLFPKCS11_LMS case CKK_HSS: + #ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + /* Remove the encrypted signature-state file too. Recover the + * per-key nonce from memory if decoded, else peek it from the + * on-disk shell (validating magic/version). Otherwise the + * state file would orphan on disk after C_DestroyObject. */ + word64 stateId = object->statefulStateId; + if (stateId == 0) { + (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, + &stateId); + } + if (stateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", tokenId, + stateId); + } + storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV; + break; + } + #endif storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; break; #endif diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index db888d52..6c67a3a8 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -133,6 +133,30 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #error Compiling with XMSS requires XMSS support in wolfSSL (--enable-xmss). #endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFSSL_LMS_VERIFY_ONLY) +#error LMS/HSS keygen+sign (--enable-lms-private) needs a sign-capable wolfSSL LMS build, not verify-only. +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_LMS) +#error WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS. +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_NO_STORE) +#error WOLFPKCS11_LMS_PRIVATE requires object storage for durable signature state (not WOLFPKCS11_NO_STORE). +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_TPM_STORE) +#error WOLFPKCS11_LMS_PRIVATE is incompatible with WOLFPKCS11_TPM_STORE (encrypted state blobs exceed TPM NV capacity). +#endif + +/* Shared stateful hash-based signature framework selectors. _ANY covers the + * shell/byte utilities compiled in both verify-only and sign builds; _PRIVATE + * gates the encrypted durable-state infrastructure (keygen + signing). Future + * XMSS private-key support reuses the same framework. */ +#if defined(WOLFPKCS11_LMS) +#define WOLFPKCS11_STATEFUL_SIG_ANY +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) +#define WOLFPKCS11_STATEFUL_SIG_PRIVATE +#endif + /* We need the next two for NSS, just for storage, even if we have no algos */ #ifndef WC_MD5_DIGEST_SIZE #define WC_MD5_DIGEST_SIZE 16 @@ -246,6 +270,12 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_FLAG_NOT_COPYABLE 0x00200000 #define WP11_FLAG_NOT_DESTROYABLE 0x00400000 #define WP11_FLAG_NOT_MODIFIABLE 0x00800000 +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* Set when a stateful private key's one-time-signature state is live and + * usable; cleared ("poisoned") on any keygen/sign/reload failure so a + * subsequent sign returns CKR_DEVICE_ERROR instead of risking OTS reuse. */ +#define WP11_FLAG_STATEFUL_STATE_VALID 0x01000000 +#endif /* Flags for token. */ #define WP11_TOKEN_FLAG_USER_PIN_SET 0x00000001 @@ -293,7 +323,7 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_INIT_TLS_MAC_VERIFY 0x0071 #define WP11_INIT_MLDSA_SIGN 0x0080 #define WP11_INIT_MLDSA_VERIFY 0x0081 -#define WP11_INIT_HSS_SIGN 0x0090 /* Reserved for future use */ +#define WP11_INIT_HSS_SIGN 0x0090 #define WP11_INIT_HSS_VERIFY 0x0091 #define WP11_INIT_XMSS_SIGN 0x00A0 /* Reserved for future use */ #define WP11_INIT_XMSS_VERIFY 0x00A1 @@ -615,6 +645,16 @@ WP11_LOCAL int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, WP11_LOCAL int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, word32 dataLen, int* stat, WP11_Object* pub); +#ifdef WOLFPKCS11_LMS_PRIVATE +WP11_LOCAL int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + CK_ULONG levels, CK_LMS_TYPE lmsType, + CK_LMOTS_TYPE lmotsType, + WP11_Slot* slot); +WP11_LOCAL int WP11_Hss_Sign(unsigned char* data, word32 dataLen, + unsigned char* sig, word32* sigLen, + WP11_Object* priv); +WP11_LOCAL int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining); +#endif #endif #ifdef WOLFPKCS11_XMSS From 1c6c51672f70c997cf0ee59b994015bf98754372 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 15:16:49 +0000 Subject: [PATCH 02/16] LMS/HSS private keys (Step 2, part 2): PKCS#11 wiring Wire the stateful LMS/HSS engine to the PKCS#11 API so keygen and signing work end to end. - crypto.c: C_GenerateKeyPair CKM_HSS_KEY_PAIR_GEN (parameters from the v3.3 key-template attributes CKA_HSS_LEVELS / CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE; no mechanism parameter; CKA_TOKEN=TRUE required on both arms since stateful keys must be token-resident); C_SignInit / C_Sign CKM_HSS (poisoned state -> CKR_DEVICE_ERROR); relax the empty-value rejection in WP11_Object_SetHssKey so keygen can create objects that are populated afterward, while still rejecting private-key imports with a value. - slot.c: advertise CKM_HSS_KEY_PAIR_GEN and add CKF_SIGN / a key-pair-gen mechanism info under WOLFPKCS11_LMS_PRIVATE. - internal.c: WP11_Hss_SigLen; serve CKA_HSS_KEYS_REMAINING live from the private key; map the WOLFPKCS11_STORE_HSSKEY_PRIV store type to a filename. - internal.c decode/encode correctness: a reloaded signing key cannot also hold its public root (wolfSSL keeps a key in signing OR verify state, not both), so drop the post-Reload ImportPubRaw and reuse the immutable cached shell on re-encode instead of re-exporting the public root. Verified end to end (keygen -> sign -> verify; finalize/re-initialize reloads the durable state with no rollback; a second sign after reload decrements the remaining count by exactly one). No regression: verify-only pkcs11v3test 18/18, default 4/4, all build clean with -Werror. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- src/crypto.c | 111 ++++++++++++++++++++++++++++++++++++++++++ src/internal.c | 86 ++++++++++++++++++++++++++++---- src/slot.c | 24 +++++++-- wolfpkcs11/internal.h | 1 + 4 files changed, 210 insertions(+), 12 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index f8596104..f0305371 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -5054,6 +5054,16 @@ CK_RV C_SignInit(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMechanism, init |= WP11_INIT_MLDSA_SIGN; break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS: + if (type != CKK_HSS) + return CKR_KEY_TYPE_INCONSISTENT; + if (pMechanism->pParameter != NULL || + pMechanism->ulParameterLen != 0) + return CKR_MECHANISM_PARAM_INVALID; + init |= WP11_INIT_HSS_SIGN; + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -5441,6 +5451,30 @@ CK_RV C_Sign(CK_SESSION_HANDLE hSession, CK_BYTE_PTR pData, *pulSignatureLen = sigLen; break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS: + if (!WP11_Session_IsOpInitialized(session, WP11_INIT_HSS_SIGN)) + return CKR_OPERATION_NOT_INITIALIZED; + + sigLen = WP11_Hss_SigLen(obj); + if (sigLen == 0) + return CKR_FUNCTION_FAILED; + if (pSignature == NULL) { + *pulSignatureLen = sigLen; + return CKR_OK; + } + if (sigLen > (word32)*pulSignatureLen) + return CKR_BUFFER_TOO_SMALL; + + ret = WP11_Hss_Sign(pData, (word32)ulDataLen, pSignature, &sigLen, + obj); + if (ret == NOT_AVAILABLE_E) + return CKR_DEVICE_ERROR; /* poisoned; reload required */ + if (ret != 0) + return CKR_FUNCTION_FAILED; + *pulSignatureLen = sigLen; + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -7926,6 +7960,83 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, } break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS_KEY_PAIR_GEN: { + CK_ATTRIBUTE_PTR aLevels = NULL, aLms = NULL, aLmots = NULL; + CK_ATTRIBUTE_PTR aTokPub = NULL, aTokPriv = NULL; + CK_ULONG hssLevels; + CK_LMS_TYPE hssLms; + CK_LMOTS_TYPE hssLmots; + + /* v3.3: the keygen mechanism carries no parameter. */ + if (pMechanism->pParameter != NULL || + pMechanism->ulParameterLen != 0) { + return CKR_MECHANISM_PARAM_INVALID; + } + /* Stateful keys must be token-resident: a session-only key that + * vanishes on a crash could release a signature whose one-time + * index was never persisted. Require CKA_TOKEN=TRUE on both arms. */ + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, + CKA_TOKEN, &aTokPub); + FindAttributeType(pPrivateKeyTemplate, ulPrivateKeyAttributeCount, + CKA_TOKEN, &aTokPriv); + if (aTokPub == NULL || aTokPub->pValue == NULL || + aTokPub->ulValueLen != sizeof(CK_BBOOL) || + *(CK_BBOOL*)aTokPub->pValue != CK_TRUE || + aTokPriv == NULL || aTokPriv->pValue == NULL || + aTokPriv->ulValueLen != sizeof(CK_BBOOL) || + *(CK_BBOOL*)aTokPriv->pValue != CK_TRUE) { + return CKR_TEMPLATE_INCONSISTENT; + } + /* Parameters come from the key template attributes (v3.3 HSS + * profile), accepted from either arm. */ + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, + CKA_HSS_LEVELS, &aLevels); + if (aLevels == NULL) + FindAttributeType(pPrivateKeyTemplate, + ulPrivateKeyAttributeCount, CKA_HSS_LEVELS, &aLevels); + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, + CKA_HSS_LMS_TYPE, &aLms); + if (aLms == NULL) + FindAttributeType(pPrivateKeyTemplate, + ulPrivateKeyAttributeCount, CKA_HSS_LMS_TYPE, &aLms); + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, + CKA_HSS_LMOTS_TYPE, &aLmots); + if (aLmots == NULL) + FindAttributeType(pPrivateKeyTemplate, + ulPrivateKeyAttributeCount, CKA_HSS_LMOTS_TYPE, &aLmots); + if (aLevels == NULL || aLms == NULL || aLmots == NULL) + return CKR_TEMPLATE_INCOMPLETE; + if (aLevels->pValue == NULL || + aLevels->ulValueLen != sizeof(CK_ULONG) || + aLms->pValue == NULL || + aLms->ulValueLen != sizeof(CK_ULONG) || + aLmots->pValue == NULL || + aLmots->ulValueLen != sizeof(CK_ULONG)) { + return CKR_ATTRIBUTE_VALUE_INVALID; + } + hssLevels = *(CK_ULONG*)aLevels->pValue; + hssLms = *(CK_LMS_TYPE*)aLms->pValue; + hssLmots = *(CK_LMOTS_TYPE*)aLmots->pValue; + + *phPublicKey = *phPrivateKey = CK_INVALID_HANDLE; + rv = NewObject(session, CKK_HSS, CKO_PUBLIC_KEY, + pPublicKeyTemplate, ulPublicKeyAttributeCount, &pub); + if (rv == CKR_OK) { + rv = NewObject(session, CKK_HSS, CKO_PRIVATE_KEY, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, &priv); + } + if (rv == CKR_OK) { + ret = WP11_Hss_GenerateKeyPair(pub, priv, hssLevels, hssLms, + hssLmots, WP11_Session_GetSlot(session)); + if (ret == BAD_FUNC_ARG) + rv = CKR_MECHANISM_PARAM_INVALID; + else if (ret != 0) + rv = CKR_FUNCTION_FAILED; + } + break; + } +#endif #ifdef WOLFPKCS11_MLDSA case CKM_ML_DSA_KEY_PAIR_GEN: if (pMechanism->pParameter != NULL || diff --git a/src/internal.c b/src/internal.c index 87701f26..0fe40ad6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1553,6 +1553,12 @@ static int wolfPKCS11_Store_Name(int type, CK_ULONG id1, CK_ULONG id2, char* nam ret = XSNPRINTF(name, nameLen, "%s/wp11_hsskey_pub_%016lx_%016lx", str, id1, id2); break; +#ifdef WOLFPKCS11_LMS_PRIVATE + case WOLFPKCS11_STORE_HSSKEY_PRIV: + ret = XSNPRINTF(name, nameLen, "%s/wp11_hsskey_priv_%016lx_%016lx", + str, id1, id2); + break; +#endif #endif #ifdef WOLFPKCS11_XMSS case WOLFPKCS11_STORE_XMSSKEY_PUB: @@ -6350,12 +6356,20 @@ static int wp11_Object_Encode_HssKey(WP11_Object* object) /* Private key: a non-secret metadata shell (params + cached public * key + 8-byte stateId trailer). The secret one-time-signature state * is NEVER written to keyData — it lives only in the AES-GCM state - * file written by the wolfSSL write callback. */ + * file written by the wolfSSL write callback. + * + * The shell is immutable after keygen. Once materialized (built at + * keygen, or loaded from disk), reuse it: a reloaded signing key + * cannot re-export its public root (wolfSSL keeps a key in either + * signing or verify state), so rebuilding here would fail. */ int levels = 0, height = 0, winternitz = 0; byte pub[HSS_MAX_PUBLIC_KEY_LEN]; int hdrLen; word32 needed; + if (object->keyData != NULL && object->keyDataLen > 0) + return 0; + pubLen = sizeof(pub); ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, &winternitz); @@ -6447,10 +6461,12 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) ret = wc_LmsKey_SetContext(object->data.lmsKey, object); if (ret == 0) ret = wc_LmsKey_Reload(object->data.lmsKey); - /* Reload does not restore key->pub; import the cached shell pub so - * ExportPubRaw/Sign stay consistent with the stored record. */ - if (ret == 0) - ret = wc_LmsKey_ImportPubRaw(object->data.lmsKey, pub, pubLen); + /* Do NOT ImportPubRaw here: wolfSSL keeps an LmsKey in either signing + * (reloaded) or verify (pub) state, not both, and rejects a pub import + * after Reload with BAD_STATE_E. The public root is not needed for + * signing, and the cached shell (kept in keyData) preserves it for + * re-storage. */ + (void)pub; if (ret == 0) { object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; object->encoded = 0; @@ -6527,6 +6543,22 @@ int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, if (object == NULL || data == NULL || len == NULL) return BAD_FUNC_ARG; +#ifdef WOLFPKCS11_LMS_PRIVATE + /* Keygen (C_GenerateKeyPair) creates both objects with no CKA_VALUE and + * populates them in WP11_Hss_GenerateKeyPair, so a missing value is a + * deferred keygen object, not an error. A private key must never be + * imported WITH a value: caller-supplied one-time-signature state is a + * forgery vector. */ + if (object->objClass == CKO_PRIVATE_KEY) { + if (data[3] != NULL) + return BAD_FUNC_ARG; + return 0; + } + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + if (data[3] == NULL) + return 0; +#else if (object->objClass != CKO_PUBLIC_KEY) return BAD_FUNC_ARG; /* A public key must carry its value. There is no keygen path in a @@ -6535,6 +6567,7 @@ int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, * reject it instead of creating an unusable object. */ if (data[3] == NULL) return BAD_FUNC_ARG; +#endif if (object->onToken) WP11_Lock_LockRW(object->lock); @@ -6784,6 +6817,19 @@ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, return ret; } +/* Signature length for an HSS private key (parameter-set dependent). Returns + * 0 on error. */ +word32 WP11_Hss_SigLen(WP11_Object* key) +{ + word32 sigLen = 0; + + if (key == NULL || key->data.lmsKey == NULL) + return 0; + if (wc_LmsKey_GetSigLen(key->data.lmsKey, &sigLen) != 0) + return 0; + return sigLen; +} + /* Remaining one-time keys for an HSS private key. Uses the wp11-layer * statefulSigCount (persisted in the GCM-authenticated state header) rather * than reaching into wolfSSL LmsKey internals. total = 2^(levels*height); @@ -6932,12 +6978,34 @@ static int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, } break; } - case CKA_HSS_KEYS_REMAINING: - /* Remaining-signature count is a private-key/state property; not - * available in a verify-only build. Return NOT_AVAILABLE_E like the - * default arm; C_GetAttributeValue then sets ulValueLen. */ + case CKA_HSS_KEYS_REMAINING: { + /* Remaining-signature count is a private-key/state property. In a + * verify-only build (or for a public key) it is unavailable. */ +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + word32 remaining = 0; + CK_ULONG v; + if (WP11_Hss_SigsLeft(object, &remaining) != 0) { + ret = NOT_AVAILABLE_E; + break; + } + v = (CK_ULONG)remaining; + if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + break; + } +#endif ret = NOT_AVAILABLE_E; break; + } case CKA_VALUE: if (object->objClass == CKO_PUBLIC_KEY) { word32 pubLen = 0; diff --git a/src/slot.c b/src/slot.c index 3789cae7..6c97b9a6 100644 --- a/src/slot.c +++ b/src/slot.c @@ -384,6 +384,9 @@ static CK_MECHANISM_TYPE mechanismList[] = { #endif #ifdef WOLFPKCS11_LMS CKM_HSS, +#ifdef WOLFPKCS11_LMS_PRIVATE + CKM_HSS_KEY_PAIR_GEN, +#endif #endif #ifdef WOLFPKCS11_XMSS CKM_XMSS, @@ -648,13 +651,23 @@ static CK_MECHANISM_INFO mldsaMechInfo = { }; #endif #ifdef WOLFPKCS11_LMS -/* Info on the HSS (LMS) verify mechanism. PKCS#11 does not define a key-size - * semantics for stateful hash-based signatures (bits vs bytes), so report the - * "not applicable" 0..0 envelope. Verify-only: no CKF_SIGN. */ +/* Info on the HSS (LMS) sign/verify mechanism. PKCS#11 does not define a + * key-size semantics for stateful hash-based signatures (bits vs bytes), so + * report the "not applicable" 0..0 envelope. CKF_SIGN is added only when + * keygen+signing is built in. */ static CK_MECHANISM_INFO hssMechInfo = { 0, 0, CKF_VERIFY +#ifdef WOLFPKCS11_LMS_PRIVATE + | CKF_SIGN +#endif +}; +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Info on the HSS key-pair generation mechanism. */ +static CK_MECHANISM_INFO hssKgMechInfo = { + 0, 0, CKF_GENERATE_KEY_PAIR }; #endif +#endif #ifdef WOLFPKCS11_XMSS /* Info on the XMSS / XMSS^MT verify mechanisms. 0..0 envelope, verify-only. */ static CK_MECHANISM_INFO xmssMechInfo = { @@ -1023,6 +1036,11 @@ CK_RV C_GetMechanismInfo(CK_SLOT_ID slotID, CK_MECHANISM_TYPE type, case CKM_HSS: XMEMCPY(pInfo, &hssMechInfo, sizeof(CK_MECHANISM_INFO)); break; +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS_KEY_PAIR_GEN: + XMEMCPY(pInfo, &hssKgMechInfo, sizeof(CK_MECHANISM_INFO)); + break; +#endif #endif #ifdef WOLFPKCS11_XMSS case CKM_XMSS: diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 6c67a3a8..7844a769 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -653,6 +653,7 @@ WP11_LOCAL int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, WP11_LOCAL int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, word32* sigLen, WP11_Object* priv); +WP11_LOCAL word32 WP11_Hss_SigLen(WP11_Object* key); WP11_LOCAL int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining); #endif #endif From cbf433d83987135c6789bd2e862cc316a6f09a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 10:24:42 +0200 Subject: [PATCH 03/16] LMS/HSS private keys: fix reload devId no-op and harden the sign path Correctness fixes for the EXPERIMENTAL --enable-lms-private sign/keygen path: - Reload devId no-op: init the stateful HSS LmsKey with INVALID_DEVID in both keygen and the reload path. With a real devId under WOLF_CRYPTO_CB, wc_LmsKey_Reload intentionally no-ops (a device-backed key's state lives in the device), so the reloaded key would carry no private state - a crash and an OTS-index rollback on the next signature after a restart. LMS state is software-managed, so it must use INVALID_DEVID. - C_Sign(CKM_HSS): clear the operation flag on every failure path so the signing operation is terminated per PKCS#11. - Build: check the LMS/SHA-256 dependency before the LMS_PRIVATE->LMS force-enable so LMS is never left enabled without SHA-256 (CMake + autotools). - Durability: guard the state-file fsync as POSIX-only (MSVC has no fsync). - Remove the encrypted state file orphaned when C_GenerateKeyPair fails after keygen has already written it. - Drop the dead WOLFPKCS11_STATEFUL_SIG_ANY macro and a redundant nested #ifdef. --- CMakeLists.txt | 13 +++++---- configure.ac | 15 ++++++---- src/crypto.c | 23 +++++++++++---- src/internal.c | 65 +++++++++++++++++++++++++++++++++---------- wolfpkcs11/internal.h | 12 ++++---- 5 files changed, 90 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c7a866f4..47cd933f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -539,6 +539,14 @@ add_option("WOLFPKCS11_LMS_PRIVATE" "no" "yes;no" ) +# LMS/HSS is SHA-256 based. Check the dependency BEFORE the force-enable below +# so a missing SHA-256 disables LMS_PRIVATE without leaving LMS enabled on its +# own (the force-enable would otherwise turn LMS on for a broken build). +if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_SHA256) + message(STATUS "LMS/HSS requires SHA-256 support (disabled); disabling WOLFPKCS11_LMS_PRIVATE") + override_cache(WOLFPKCS11_LMS_PRIVATE "no") +endif() + if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_LMS) message(STATUS "WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS — enabling WOLFPKCS11_LMS automatically") override_cache(WOLFPKCS11_LMS "yes") @@ -553,11 +561,6 @@ if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_LMS) list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS") endif() -if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_SHA256) - message(STATUS "LMS/HSS requires SHA-256 support (disabled); disabling WOLFPKCS11_LMS_PRIVATE") - override_cache(WOLFPKCS11_LMS_PRIVATE "no") -endif() - if(WOLFPKCS11_LMS_PRIVATE) list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS_PRIVATE") endif() diff --git a/configure.ac b/configure.ac index 9a4877b8..ac0242b1 100644 --- a/configure.ac +++ b/configure.ac @@ -606,6 +606,15 @@ AC_ARG_ENABLE([lms-private], [ ENABLED_LMS_PRIVATE=no ] ) +# LMS/HSS is SHA-256 based. Check the dependency BEFORE the --enable-lms +# implication below so a missing SHA-256 disables lms-private without leaving +# --enable-lms turned on for a broken build. +if test "$ENABLED_SHA256" = "no" && test "$ENABLED_LMS_PRIVATE" = "yes" +then + echo "LMS/HSS requires SHA-256 support (disabled), disabling LMS/HSS keygen+sign" + ENABLED_LMS_PRIVATE=no +fi + if test "$ENABLED_LMS_PRIVATE" = "yes" && test "$ENABLED_LMS" = "no" then AC_MSG_NOTICE([--enable-lms-private implies --enable-lms; enabling]) @@ -621,12 +630,6 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS" fi -if test "$ENABLED_SHA256" = "no" && test "$ENABLED_LMS_PRIVATE" = "yes" -then - echo "LMS/HSS requires SHA-256 support (disabled), disabling LMS/HSS keygen+sign" - ENABLED_LMS_PRIVATE=no -fi - if test "$ENABLED_LMS_PRIVATE" = "yes" then AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS_PRIVATE" diff --git a/src/crypto.c b/src/crypto.c index f0305371..25d9f363 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -5457,9 +5457,13 @@ CK_RV C_Sign(CK_SESSION_HANDLE hSession, CK_BYTE_PTR pData, return CKR_OPERATION_NOT_INITIALIZED; sigLen = WP11_Hss_SigLen(obj); - if (sigLen == 0) + if (sigLen == 0) { + /* Genuine failure - terminate the active signing operation. */ + WP11_Session_SetOpInitialized(session, 0); return CKR_FUNCTION_FAILED; + } if (pSignature == NULL) { + /* Length query - operation remains active per PKCS#11. */ *pulSignatureLen = sigLen; return CKR_OK; } @@ -5468,10 +5472,14 @@ CK_RV C_Sign(CK_SESSION_HANDLE hSession, CK_BYTE_PTR pData, ret = WP11_Hss_Sign(pData, (word32)ulDataLen, pSignature, &sigLen, obj); - if (ret == NOT_AVAILABLE_E) - return CKR_DEVICE_ERROR; /* poisoned; reload required */ - if (ret != 0) - return CKR_FUNCTION_FAILED; + if (ret == NOT_AVAILABLE_E) { + /* Poisoned state; terminate the operation, reload required. */ + WP11_Session_SetOpInitialized(session, 0); + return CKR_DEVICE_ERROR; + } + /* Other failures fall through to the shared cleanup below, which + * clears the op flag and maps ret < 0 to CKR_FUNCTION_FAILED + * (matching the RSA/ECC/ML-DSA arms). */ *pulSignatureLen = sigLen; break; #endif @@ -8233,6 +8241,11 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, if (rv != CKR_OK && priv != NULL) { if (*phPrivateKey != CK_INVALID_HANDLE) WP11_Session_RemoveObject(session, priv); +#ifdef WOLFPKCS11_LMS_PRIVATE + /* An HSS keygen writes a durable state file before this point; if a + * later step failed, remove it so it is not orphaned on disk. */ + WP11_Hss_RemoveOrphanState(priv, WP11_Session_GetSlot(session)); +#endif WP11_Object_Free(priv); } diff --git a/src/internal.c b/src/internal.c index 0fe40ad6..baf31ce5 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1874,18 +1874,24 @@ static int wolfPKCS11_Store_CloseAndReport(void* store) if (ctx != NULL) { int commitRet = 0; int fsyncFailed = 0; +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(_WIN32) && !defined(_MSC_VER) + int fd; +#endif if (ctx->file != XBADFILE && ctx->file != NULL) { #ifdef WOLFPKCS11_LMS_PRIVATE - /* Durable mode: flush file data to disk before close so the - * rename in CommitTemp commits a fully-persisted file. */ + /* Durable mode: flush file data to disk before close so the rename + * in CommitTemp commits a fully-persisted file. The explicit fsync + * is POSIX-only; on Windows durability comes from the atomic rename + * (MoveFileExA with MOVEFILE_WRITE_THROUGH) in CommitTemp, and MSVC + * has no fsync()/fileno(). */ if (ctx->durable && ctx->is_write) { (void)XFFLUSH(ctx->file); - { - int fd = fileno(ctx->file); - if (fd < 0 || fsync(fd) != 0) - fsyncFailed = 1; - } +#if !defined(_WIN32) && !defined(_MSC_VER) + fd = fileno(ctx->file); + if (fd < 0 || fsync(fd) != 0) + fsyncFailed = 1; +#endif } #endif XFCLOSE(ctx->file); @@ -5504,16 +5510,15 @@ static int wp11_Object_Store_MlKemKey(WP11_Object* object, int tokenId, * [u32 ctLen][ciphertext (privSz + 16-byte GCM tag)] */ -/* Big-endian u32 reader. Always built — used by verify-only shell parsing. */ +/* Big-endian byte helpers for encoding/decoding the AAD-bound state header + * and the shell trailer (compiled in sign builds only - the whole framework + * is gated by WOLFPKCS11_STATEFUL_SIG_PRIVATE above). */ static word32 wp11_Stateful_ReadU32(const byte* p) { return ((word32)p[0] << 24) | ((word32)p[1] << 16) | ((word32)p[2] << 8) | (word32)p[3]; } -#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE -/* The remaining byte helpers are only needed for sign-capable schemes - * (encoding/decoding the AAD-bound state header and the shell trailer). */ static word64 wp11_Stateful_ReadU64(const byte* p) { return ((word64)p[0] << 56) | ((word64)p[1] << 48) @@ -6034,8 +6039,7 @@ static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, return 0; } -#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE (inner) */ -#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE (outer) */ +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE */ #ifdef WOLFPKCS11_LMS /* =========================================================================== * LMS/HSS (RFC 8554): verification and public-key import. @@ -6444,8 +6448,16 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) if (object->statefulStateId == 0) ret = BAD_FUNC_ARG; } + /* INVALID_DEVID unconditionally (not object->devId): HSS stateful + * signing is software-only by design - HSM/device-backed LMS is out of + * scope. This must hold even when the slot has a real devId (a hardware- + * offload build), so it is pinned here rather than inheriting the slot + * default. A real devId would route to the crypto callback and make + * wc_LmsKey_Reload treat the key as HSM-backed and skip the read + * callback (a no-op), leaving the reloaded key with no private state - + * any later Sign would then reuse leaf index 0 or crash. */ if (ret == 0) - ret = wc_LmsKey_Init(object->data.lmsKey, NULL, object->devId); + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); if (ret == 0) ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, height, winternitz); @@ -6720,7 +6732,14 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, priv->onToken = 1; priv->statefulSigCount = 0; - ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, priv->devId); + /* INVALID_DEVID unconditionally (not priv->devId): HSS stateful signing is + * software-only by design - HSM/device-backed LMS is out of scope - so this + * is pinned even when the slot has a real devId (a hardware-offload build), + * mirroring the reload path in wp11_Object_Decode_HssKey. A real devId would + * route MakeKey/Sign through the crypto callback and, critically, make a + * later wc_LmsKey_Reload a no-op, so the persisted state could never be + * restored. */ + ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, INVALID_DEVID); if (ret == 0) ret = wc_LmsKey_SetParameters(priv->data.lmsKey, nLevels, height, winternitz); @@ -6772,6 +6791,22 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, return ret; } +/* Remove the durable signature-state file backing an HSS private key that was + * generated but never committed to the token - e.g. a later C_GenerateKeyPair + * step (AddObject/SetInitialStates/store) failed after keygen already wrote the + * genesis state to disk. Without this the encrypted state file is orphaned. + * No-op unless priv is a CKK_HSS private key with a live state id; safe to call + * on any object and more than once. */ +void WP11_Hss_RemoveOrphanState(WP11_Object* priv, WP11_Slot* slot) +{ + if (priv != NULL && slot != NULL && priv->type == CKK_HSS && + priv->objClass == CKO_PRIVATE_KEY && priv->statefulStateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, + priv->statefulStateId); + priv->statefulStateId = 0; + } +} + /* Sign a raw message with an HSS private key. State is advanced and persisted * (via the write CB inside wc_LmsKey_Sign) BEFORE the signature is returned. * On any failure the in-memory state is poisoned (STATE_VALID cleared) to diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 7844a769..bd668380 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -146,13 +146,10 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #error WOLFPKCS11_LMS_PRIVATE is incompatible with WOLFPKCS11_TPM_STORE (encrypted state blobs exceed TPM NV capacity). #endif -/* Shared stateful hash-based signature framework selectors. _ANY covers the - * shell/byte utilities compiled in both verify-only and sign builds; _PRIVATE - * gates the encrypted durable-state infrastructure (keygen + signing). Future - * XMSS private-key support reuses the same framework. */ -#if defined(WOLFPKCS11_LMS) -#define WOLFPKCS11_STATEFUL_SIG_ANY -#endif +/* Stateful hash-based signature framework selector. _PRIVATE gates the + * encrypted durable-state infrastructure (keygen + signing); it is compiled + * only in sign-capable builds. Future XMSS private-key support reuses the same + * framework. */ #if defined(WOLFPKCS11_LMS_PRIVATE) #define WOLFPKCS11_STATEFUL_SIG_PRIVATE #endif @@ -655,6 +652,7 @@ WP11_LOCAL int WP11_Hss_Sign(unsigned char* data, word32 dataLen, WP11_Object* priv); WP11_LOCAL word32 WP11_Hss_SigLen(WP11_Object* key); WP11_LOCAL int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining); +WP11_LOCAL void WP11_Hss_RemoveOrphanState(WP11_Object* priv, WP11_Slot* slot); #endif #endif From 4162e9acc300cfce71e8d0fdf40aec876f36e69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 10:24:42 +0200 Subject: [PATCH 04/16] Default slot devId to INVALID_DEVID (software) when no device is configured wp11_Slot_Init left slot->devId at the 0 from the XMEMSET, but 0 is a valid crypto-callback device slot, so every key inherited a 0 devId and was routed through wolfSSL's crypto-callback dispatch even with no device registered. Default to INVALID_DEVID and let the TPM/MAXQ/STM32 paths override it for real hardware. Also thread the object devId through the ML-DSA decode (MldsaKeyTryDecode) instead of its own hard-coded INVALID_DEVID, matching the ML-DSA import/copy paths and re-enabling ML-DSA crypto-callback offload. Behaviour change: a no-device build that registered a crypto callback at devId 0 and relied on the default must now set CKA_WOLFSSL_DEVID explicitly. --- src/internal.c | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/internal.c b/src/internal.c index baf31ce5..939cbade 100644 --- a/src/internal.c +++ b/src/internal.c @@ -4639,13 +4639,16 @@ static int wp11_Object_Store_EccKey(WP11_Object* object, int tokenId, int objId) #ifdef WOLFPKCS11_MLDSA static int MldsaKeyTryDecode(wc_MlDsaKey* key, byte level, byte* data, - word32 len, CK_OBJECT_CLASS class) + word32 len, CK_OBJECT_CLASS class, int devId) { int ret = 0; word32 idx = 0; - /* Init key */ - ret = wc_MlDsaKey_Init(key, NULL, INVALID_DEVID); + /* Init key with the object's devId (INVALID_DEVID for no-device builds via + * wp11_Slot_Init's default) so ML-DSA offloads to a configured + * crypto-callback device and falls through to software otherwise. Mirrors + * the import/copy paths, which already use the object devId. */ + ret = wc_MlDsaKey_Init(key, NULL, devId); if (ret == 0) { /* Set level */ @@ -4699,14 +4702,16 @@ static int wp11_Object_Decode_MldsaKey(WP11_Object* object) if (ret == 0) { /* Decode ML-DSA private key. */ ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_44, - der, len, object->objClass); + der, len, object->objClass, object->devId); if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_65, - der, len, object->objClass); + der, len, object->objClass, + object->devId); } if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_87, - der, len, object->objClass); + der, len, object->objClass, + object->devId); } wc_ForceZero(der, len); } @@ -4717,16 +4722,16 @@ static int wp11_Object_Decode_MldsaKey(WP11_Object* object) /* Decode ML-DSA public key. */ ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_44, object->keyData, object->keyDataLen, - object->objClass); + object->objClass, object->devId); if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_65, object->keyData, object->keyDataLen, - object->objClass); + object->objClass, object->devId); } if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_87, object->keyData, object->keyDataLen, - object->objClass); + object->objClass, object->devId); } } object->encoded = (ret != 0); @@ -8908,6 +8913,12 @@ static int wp11_Slot_Init(WP11_Slot* slot, int id) XMEMSET(slot, 0, sizeof(*slot)); slot->id = id; + /* Default to pure software. Must be INVALID_DEVID, not the 0 left by the + * XMEMSET above: 0 is a valid crypto-callback device slot, so a 0 devId + * routes every key through the crypto-callback dispatch even when no device + * is registered. The hardware-offload paths below override this with a real + * device id. */ + slot->devId = INVALID_DEVID; slot->token.state = WP11_TOKEN_STATE_UNKNOWN; slot->token.tokenFlags = 0; From c71598430bc38c001276c23a5272c0eea0ba7650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 09:36:42 +0200 Subject: [PATCH 05/16] tests: make verify-only HSS checks aware of the sign-capable build test_hss_verify_only_no_keygen, test_hss_mech_info, and test_hss_empty_template_rejected asserted verify-only behaviour (keygen rejected, no CKF_SIGN, empty public-key template rejected) that inverts once --enable-lms-private is built. Branch these on WOLFPKCS11_LMS_PRIVATE and the specific mechanism/key type, since XMSS stays verify-only in this build. --- tests/pkcs11v3test.c | 46 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tests/pkcs11v3test.c b/tests/pkcs11v3test.c index b88ff5fa..d4d76dd5 100644 --- a/tests/pkcs11v3test.c +++ b/tests/pkcs11v3test.c @@ -3046,8 +3046,25 @@ static CK_RV hbs_keygen_rejected(CK_SESSION_HANDLE session, mech.ulParameterLen = 0; ret = funcList->C_GenerateKeyPair(session, &mech, pubTmpl, 1, privTmpl, 1, &pub, &priv); - CHECK_CKR_FAIL(ret, CKR_MECHANISM_INVALID, - "HBS keygen rejected in verify-only build"); +#ifdef WOLFPKCS11_LMS_PRIVATE + if (kgMech == CKM_HSS_KEY_PAIR_GEN) { + /* Sign-capable build: HSS keygen is supported. This minimal template + * omits CKA_TOKEN (and the scheme attributes), so keygen rejects it as + * an inconsistent template, not an invalid mechanism. */ + CHECK_CKR_FAIL(ret, CKR_TEMPLATE_INCONSISTENT, + "HSS keygen with incomplete template rejected"); + } + else +#endif + { + CHECK_CKR_FAIL(ret, CKR_MECHANISM_INVALID, + "HBS keygen rejected (no sign support for this scheme)"); + } + /* Should not have produced objects, but clean up if a regression did. */ + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); return ret; } @@ -3118,6 +3135,16 @@ static CK_RV hbs_mech_info(CK_MECHANISM_TYPE mechType) ret = CKR_GENERAL_ERROR; CHECK_CKR(ret, "HBS mech info missing CKF_VERIFY"); } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (mechType == CKM_HSS) { + /* Sign-capable build: CKM_HSS advertises signing too. */ + if (ret == CKR_OK && (info.flags & CKF_SIGN) == 0) { + ret = CKR_GENERAL_ERROR; + CHECK_CKR(ret, "HSS mech info missing CKF_SIGN (sign build)"); + } + } + else +#endif if (ret == CKR_OK && (info.flags & CKF_SIGN) != 0) { ret = CKR_GENERAL_ERROR; CHECK_CKR(ret, "HBS mech info unexpectedly claims CKF_SIGN"); @@ -3140,8 +3167,19 @@ static CK_RV hbs_empty_template_rejected(CK_SESSION_HANDLE session, ret = funcList->C_CreateObject(session, tmpl, sizeof(tmpl) / sizeof(*tmpl), &obj); - CHECK_CKR_FAIL(ret, CKR_ATTRIBUTE_VALUE_INVALID, - "HBS empty-template public key rejected"); +#ifdef WOLFPKCS11_LMS_PRIVATE + if (keyType == CKK_HSS) { + /* Sign-capable build: an HSS public key with no CKA_VALUE is a valid + * deferred-keygen placeholder (C_GenerateKeyPair populates it), so it + * is accepted rather than rejected. */ + CHECK_CKR(ret, "HSS empty-template public key (deferred keygen)"); + } + else +#endif + { + CHECK_CKR_FAIL(ret, CKR_ATTRIBUTE_VALUE_INVALID, + "HBS empty-template public key rejected"); + } if (obj != CK_INVALID_HANDLE) funcList->C_DestroyObject(session, obj); return ret; From b1cff156eecf502fa5a7bc4d4746d2ecb9fbc1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 09:36:42 +0200 Subject: [PATCH 06/16] tests: add LMS/HSS private-key functional and concurrency tests hbs_private_test.c (gated on WOLFPKCS11_LMS_PRIVATE): keygen -> sign -> verify, durable-state reload with no OTS-index rollback, keygen template validation, CKA_HSS_KEYS_REMAINING on a private key, value-carrying import rejection, key exhaustion -> poison -> CKR_DEVICE_ERROR, C_DestroyObject state-file cleanup, on-disk state tamper detection, a parameter matrix (L1/L2 x W1-W8 x H5/H10, fast subset by default / full via WOLFPKCS11_HSS_FULL_MATRIX), and a multi-level subtree-rollover reload test. hss_concurrency_test.c: N threads sign one shared token HSS key concurrently and assert every parsed RFC 8554 leaf index is unique - a hard check against one-time-signature reuse under concurrency. --- tests/hbs_private_test.c | 1236 ++++++++++++++++++++++++++++++++++ tests/hss_concurrency_test.c | 525 +++++++++++++++ tests/include.am | 14 + 3 files changed, 1775 insertions(+) create mode 100644 tests/hbs_private_test.c create mode 100644 tests/hss_concurrency_test.c diff --git a/tests/hbs_private_test.c b/tests/hbs_private_test.c new file mode 100644 index 00000000..11e83eaf --- /dev/null +++ b/tests/hbs_private_test.c @@ -0,0 +1,1236 @@ +/* hbs_private_test.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 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. + * + * wolfPKCS11 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 + * + * Functional test for the EXPERIMENTAL sign-capable LMS/HSS build + * (--enable-lms-private / WOLFPKCS11_LMS_PRIVATE). Exercises the paths that + * the verify-only tests cannot reach: + * + * - keygen -> sign (two-call length query) -> verify + * - durable state survives a C_Finalize/C_Initialize reload with no + * one-time-signature (OTS) index rollback + * - keygen template validation (CKA_TOKEN, missing/oversized attrs, bad + * mechanism parameter, invalid LMS/LMOTS type) + * - CKA_HSS_KEYS_REMAINING on a private key (size query, value, decrement, + * small-buffer error) + * - rejection of a private-key import that carries CKA_VALUE (forged OTS + * state injection) + * - key exhaustion poisons the key (subsequent C_Sign -> CKR_DEVICE_ERROR) + * - C_DestroyObject removes the encrypted state file + * - a tampered on-disk state file is detected on reload (no silent signing) + * + * The scheme used throughout is HSS L1 / LMS SHA256_M32_H5 / LMOTS + * SHA256_N32_W4, i.e. a single 32-leaf tree, so exhaustion is reachable in 32 + * signatures. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include + +#ifndef WOLFPKCS11_USER_SETTINGS + #include +#endif +#include + +#ifndef HAVE_PKCS11_STATIC +#include +#endif + +#include +#include + +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) + +#ifndef _WIN32 +#include +#endif + +#ifndef WOLFPKCS11_DLL_FILENAME + #ifdef __MACH__ + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.dylib" + #else + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.so" + #endif +#endif + +#define CHECK_CKR(rv, msg) \ + do { \ + if ((rv) != CKR_OK) { \ + fprintf(stderr, "\n%s:%d - %s: %lx - FAIL\n", \ + __FILE__, __LINE__, msg, (unsigned long)(rv)); \ + } \ + } \ + while (0) + +/* Assert that an operation failed with exactly the expected return code. */ +#define CHECK_CKR_FAIL(rv, expected, msg) \ + do { \ + if ((rv) != (expected)) { \ + fprintf(stderr, "\n%s:%d - %s: got %lx, expected %lx - FAIL\n",\ + __FILE__, __LINE__, msg, (unsigned long)(rv), \ + (unsigned long)(expected)); \ + } \ + } \ + while (0) + +#ifndef HAVE_PKCS11_STATIC +static void* dlib; +#endif +static CK_FUNCTION_LIST* funcList; +static CK_SLOT_ID slot = 0; +static const char* tokenName = "wolfpkcs11"; +static byte* soPin = (byte*)"password123456"; +static int soPinLen = 14; +static byte* userPin = (byte*)"wolfpkcs11-test"; +static int userPinLen = 15; +static const char* storeDir = "./store/hbs_priv"; + +static CK_BBOOL ckTrue = CK_TRUE; +static CK_BBOOL ckFalse = CK_FALSE; + +/* Scheme under test: HSS L1 / H5 / W4 - a single 32-leaf tree. */ +static CK_ULONG hssLevels = 1; +static CK_ULONG hssLmsType = CKL_LMS_SHA256_M32_H5; +static CK_ULONG hssLmotsType = CKL_LMOTS_SHA256_N32_W4; +#define HSS_H5_SIGS 32 + +static const CK_BYTE testMsg[] = "wolfPKCS11 HSS private-key sign test vector"; +#define TEST_MSG_LEN ((CK_ULONG)(sizeof(testMsg) - 1)) + +static CK_RV pkcs11_init(void) +{ + CK_RV ret; + CK_C_INITIALIZE_ARGS args; + CK_SLOT_ID slotList[16]; + CK_ULONG slotCount = sizeof(slotList) / sizeof(slotList[0]); +#ifndef HAVE_PKCS11_STATIC + CK_C_GetFunctionList func; + + dlib = dlopen(WOLFPKCS11_DLL_FILENAME, RTLD_NOW | RTLD_LOCAL); + if (dlib == NULL) { + fprintf(stderr, "dlopen error: %s\n", dlerror()); + return CKR_GENERAL_ERROR; + } + func = (CK_C_GetFunctionList)dlsym(dlib, "C_GetFunctionList"); + if (func == NULL) { + fprintf(stderr, "Failed to get function list function\n"); + dlclose(dlib); + return CKR_GENERAL_ERROR; + } + ret = func(&funcList); + if (ret != CKR_OK) { + fprintf(stderr, "Failed to get function list: %lx\n", + (unsigned long)ret); + dlclose(dlib); + return ret; + } +#else + ret = C_GetFunctionList(&funcList); + if (ret != CKR_OK) { + fprintf(stderr, "Failed to get function list: %lx\n", + (unsigned long)ret); + return ret; + } +#endif + + XMEMSET(&args, 0, sizeof(args)); + args.flags = CKF_OS_LOCKING_OK; + ret = funcList->C_Initialize(&args); + CHECK_CKR(ret, "Initialize"); + + if (ret == CKR_OK) { + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + CHECK_CKR(ret, "Get Slot List"); + } + if (ret == CKR_OK) { + if (slotCount > 0) + slot = slotList[0]; + else { + fprintf(stderr, "No slots available\n"); + ret = CKR_GENERAL_ERROR; + } + } + return ret; +} + +static void pkcs11_final(void) +{ + funcList->C_Finalize(NULL); +#ifndef HAVE_PKCS11_STATIC + if (dlib != NULL) { + dlclose(dlib); + dlib = NULL; + } +#endif +} + +static CK_RV pkcs11_init_token(void) +{ + CK_RV ret; + unsigned char label[32]; + + XMEMSET(label, ' ', sizeof(label)); + XMEMCPY(label, tokenName, strlen(tokenName)); + ret = funcList->C_InitToken(slot, soPin, soPinLen, label); + CHECK_CKR(ret, "Init Token"); + return ret; +} + +static CK_RV pkcs11_set_user_pin(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, &session); + CHECK_CKR(ret, "Open Session for PIN setup"); + if (ret == CKR_OK) { + ret = funcList->C_Login(session, CKU_SO, soPin, soPinLen); + CHECK_CKR(ret, "Login as SO"); + if (ret == CKR_OK) { + ret = funcList->C_InitPIN(session, userPin, userPinLen); + CHECK_CKR(ret, "Init User PIN"); + } + funcList->C_Logout(session); + funcList->C_CloseSession(session); + } + return ret; +} + +static CK_RV pkcs11_open_session(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); + CHECK_CKR(ret, "Open Session"); + if (ret == CKR_OK) { + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + CHECK_CKR(ret, "Login"); + } + return ret; +} + +static void close_session(CK_SESSION_HANDLE session) +{ + if (session != CK_INVALID_HANDLE) { + funcList->C_Logout(session); + funcList->C_CloseSession(session); + } +} + +/* Remove every wp11_* store artifact so each test starts from a clean token. */ +static void cleanup_store(void) +{ +#ifndef _WIN32 + DIR* d; + struct dirent* e; + char path[512]; + + d = opendir(storeDir); + if (d == NULL) + return; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, "wp11_", 5) == 0) { + snprintf(path, sizeof(path), "%s/%s", storeDir, e->d_name); + (void)remove(path); + } + } + closedir(d); +#endif +} + +/* Fresh library + token + user PIN + logged-in R/W session. */ +static CK_RV fresh_token(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + + cleanup_store(); + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = pkcs11_init_token(); + if (ret == CKR_OK) + ret = pkcs11_set_user_pin(); + if (ret == CKR_OK) + ret = pkcs11_open_session(session); + return ret; +} + +/* Re-open the existing on-disk token (reload path) without wiping the store. */ +static CK_RV reopen_token(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = pkcs11_open_session(session); + return ret; +} + +/* Like reopen_token but does not log a login failure as a FAIL: a tampered + * token is expected to fail the login-time reload, so the caller inspects the + * return code instead of treating it as an error. */ +static CK_RV reopen_token_quiet(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); + if (ret == CKR_OK) + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + return ret; +} + +/* Generate a token-resident HSS keypair with the given scheme parameters. */ +static CK_RV gen_hss_key(CK_SESSION_HANDLE session, CK_ULONG levels, + CK_ULONG lmsType, CK_ULONG lmotsType, CK_BYTE* id, CK_ULONG idLen, + CK_OBJECT_HANDLE* pub, CK_OBJECT_HANDLE* priv) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_ATTRIBUTE pubTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, id, idLen }, + { CKA_HSS_LEVELS, &levels, sizeof(levels) }, + { CKA_HSS_LMS_TYPE, &lmsType, sizeof(lmsType) }, + { CKA_HSS_LMOTS_TYPE, &lmotsType, sizeof(lmotsType) }, + }; + CK_ATTRIBUTE privTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, id, idLen }, + }; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl) / sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl) / sizeof(*privTmpl), pub, priv); + return ret; +} + +/* Find a single token object of the given class by CKA_ID. */ +static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, + CK_BYTE* id, CK_ULONG idLen, CK_OBJECT_HANDLE* obj) +{ + CK_RV ret; + CK_ULONG count = 0; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &cls, sizeof(cls) }, + { CKA_ID, id, idLen }, + }; + + ret = funcList->C_FindObjectsInit(session, tmpl, + sizeof(tmpl) / sizeof(*tmpl)); + CHECK_CKR(ret, "FindObjectsInit"); + if (ret == CKR_OK) { + ret = funcList->C_FindObjects(session, obj, 1, &count); + CHECK_CKR(ret, "FindObjects"); + funcList->C_FindObjectsFinal(session); + } + if (ret == CKR_OK && count != 1) { + fprintf(stderr, "\nExpected 1 object, found %lu - FAIL\n", + (unsigned long)count); + ret = CKR_GENERAL_ERROR; + } + return ret; +} + +/* Sign testMsg with the two-call length-query protocol; caller frees *sigOut. */ +static CK_RV hss_sign(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE priv, + CK_BYTE** sigOut, CK_ULONG* sigLenOut) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_ULONG sigLen = 0; + CK_BYTE* sig = NULL; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_SignInit(session, &mech, priv); + if (ret == CKR_OK) { + /* First call: NULL buffer -> length query, operation stays active. */ + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, NULL, + &sigLen); + } + if (ret == CKR_OK && sigLen == 0) + ret = CKR_GENERAL_ERROR; + if (ret == CKR_OK) { + sig = (CK_BYTE*)XMALLOC(sigLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (sig == NULL) + ret = CKR_HOST_MEMORY; + } + if (ret == CKR_OK) + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (ret == CKR_OK) { + *sigOut = sig; + *sigLenOut = sigLen; + } + else if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + return ret; +} + +static CK_RV hss_verify(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE pub, + CK_BYTE* sig, CK_ULONG sigLen) +{ + CK_RV ret; + CK_MECHANISM mech; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_VerifyInit(session, &mech, pub); + if (ret == CKR_OK) + ret = funcList->C_Verify(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + sigLen); + return ret; +} + +static CK_RV get_keys_remaining(CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE priv, CK_ULONG* remaining) +{ + CK_ATTRIBUTE a; + + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = remaining; + a.ulValueLen = sizeof(*remaining); + return funcList->C_GetAttributeValue(session, priv, &a, 1); +} + +/* ---- HIGH-1: keygen -> sign (two-call) -> verify ---- */ +static CK_RV test_keygen_sign_verify(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-ksv"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + + printf(" keygen -> sign -> verify...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair(CKM_HSS_KEY_PAIR_GEN)"); + } + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "C_Sign(CKM_HSS)"); + } + if (ret == CKR_OK) { + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "C_Verify(CKM_HSS) of generated signature"); + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- HIGH-2: durable state survives reload with no OTS index rollback ---- */ +static CK_RV test_reload_no_rollback(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-reload"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + CK_ULONG remainingBefore = 0, remainingAfterReload = 0, remainingFinal = 0; + int i; + + printf(" sign N, reload, confirm no rollback...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + /* Sign three times before the reload. */ + for (i = 0; ret == CKR_OK && i < 3; i++) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "pre-reload C_Sign"); + if (ret == CKR_OK) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingBefore); + CHECK_CKR(ret, "keys-remaining before reload"); + } + /* Force a reload from disk. */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) { + printf(" --- reloading token from disk ---\n"); + ret = reopen_token(&session); + } + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingAfterReload); + CHECK_CKR(ret, "keys-remaining after reload"); + } + /* The counter must not have rolled back across the reload. */ + if (ret == CKR_OK && remainingAfterReload != remainingBefore) { + fprintf(stderr, "\nOTS index rolled back on reload: before=%lu " + "after=%lu - FAIL\n", (unsigned long)remainingBefore, + (unsigned long)remainingAfterReload); + ret = CKR_GENERAL_ERROR; + } + /* A post-reload signature must continue decrementing and still verify. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "post-reload C_Sign"); + } + if (ret == CKR_OK) { + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "post-reload C_Verify"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingFinal); + CHECK_CKR(ret, "keys-remaining after post-reload sign"); + } + if (ret == CKR_OK && remainingFinal != remainingAfterReload - 1) { + fprintf(stderr, "\nCounter did not advance after reload: %lu -> %lu " + "- FAIL\n", (unsigned long)remainingAfterReload, + (unsigned long)remainingFinal); + ret = CKR_GENERAL_ERROR; + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- HIGH-3: keygen template validation ---- */ +static CK_RV gen_expect(CK_SESSION_HANDLE session, CK_ATTRIBUTE* pubTmpl, + CK_ULONG pubCount, CK_ATTRIBUTE* privTmpl, CK_ULONG privCount, + CK_VOID_PTR param, CK_ULONG paramLen, CK_RV expected, const char* msg) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = param; + mech.ulParameterLen = paramLen; + + ret = funcList->C_GenerateKeyPair(session, &mech, pubTmpl, pubCount, + privTmpl, privCount, &pub, &priv); + CHECK_CKR_FAIL(ret, expected, msg); + if (ret == CKR_OK) { + /* Should not have succeeded; clean up so the test stays isolated. */ + funcList->C_DestroyObject(session, pub); + funcList->C_DestroyObject(session, priv); + return CKR_GENERAL_ERROR; + } + return (ret == expected) ? CKR_OK : ret; +} + +static CK_RV test_keygen_negative_templates(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_ULONG dummyParam = 0; + CK_ULONG badType = 0x00000099UL; + CK_ULONG shortLevels = 1; + CK_BYTE id[] = "hss-neg"; + + /* Fully valid templates, mutated per-case below. */ + CK_ATTRIBUTE pubOk[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + CK_ATTRIBUTE privOk[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, id, sizeof(id) - 1 }, + }; + /* CKA_TOKEN=FALSE on the private arm. */ + CK_ATTRIBUTE privTokFalse[] = { + { CKA_TOKEN, &ckFalse, sizeof(ckFalse) }, + { CKA_ID, id, sizeof(id) - 1 }, + }; + /* Missing CKA_HSS_LMS_TYPE / LMOTS_TYPE. */ + CK_ATTRIBUTE pubIncomplete[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + }; + /* Wrong-sized CKA_HSS_LEVELS. */ + CK_ATTRIBUTE pubBadSize[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &shortLevels, 1 }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + /* Invalid LMS type value. */ + CK_ATTRIBUTE pubBadType[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &badType, sizeof(badType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + + printf(" keygen template validation...\n"); + ret = fresh_token(&session); + + /* Non-NULL mechanism parameter -> CKR_MECHANISM_PARAM_INVALID. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubOk, 4, privOk, 2, &dummyParam, + sizeof(dummyParam), CKR_MECHANISM_PARAM_INVALID, + "non-NULL mechanism parameter"); + } + /* CKA_TOKEN=FALSE -> CKR_TEMPLATE_INCONSISTENT. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubOk, 4, privTokFalse, 2, NULL, 0, + CKR_TEMPLATE_INCONSISTENT, "CKA_TOKEN=FALSE on private key"); + } + /* Missing scheme attributes -> CKR_TEMPLATE_INCOMPLETE. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubIncomplete, 2, privOk, 2, NULL, 0, + CKR_TEMPLATE_INCOMPLETE, "missing CKA_HSS_LMS/LMOTS_TYPE"); + } + /* Wrong-sized attribute -> CKR_ATTRIBUTE_VALUE_INVALID. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubBadSize, 4, privOk, 2, NULL, 0, + CKR_ATTRIBUTE_VALUE_INVALID, "wrong-sized CKA_HSS_LEVELS"); + } + /* Invalid LMS type -> CKR_MECHANISM_PARAM_INVALID. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubBadType, 4, privOk, 2, NULL, 0, + CKR_MECHANISM_PARAM_INVALID, "invalid CKA_HSS_LMS_TYPE"); + } + + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-8: CKA_HSS_KEYS_REMAINING on a private key ---- */ +static CK_RV test_keys_remaining_private(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-remain"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + CK_ULONG remaining = 0, remainingAfter = 0; + CK_ATTRIBUTE a; + CK_BYTE small = 0; + + printf(" CKA_HSS_KEYS_REMAINING on private key...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + /* Size query: NULL buffer must report sizeof(CK_ULONG). */ + if (ret == CKR_OK) { + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = NULL; + a.ulValueLen = 0; + ret = funcList->C_GetAttributeValue(session, priv, &a, 1); + CHECK_CKR(ret, "keys-remaining size query"); + if (ret == CKR_OK && a.ulValueLen != sizeof(CK_ULONG)) { + fprintf(stderr, "\nkeys-remaining size query returned %lu - FAIL\n", + (unsigned long)a.ulValueLen); + ret = CKR_GENERAL_ERROR; + } + } + /* Value must equal the full 2^(levels*height) tree size before signing. */ + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remaining); + CHECK_CKR(ret, "keys-remaining value"); + } + if (ret == CKR_OK && remaining != HSS_H5_SIGS) { + fprintf(stderr, "\nInitial keys-remaining %lu, expected %d - FAIL\n", + (unsigned long)remaining, HSS_H5_SIGS); + ret = CKR_GENERAL_ERROR; + } + /* Too-small buffer must be reported as CKR_BUFFER_TOO_SMALL. */ + if (ret == CKR_OK) { + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = &small; + a.ulValueLen = sizeof(small); + ret = funcList->C_GetAttributeValue(session, priv, &a, 1); + CHECK_CKR_FAIL(ret, CKR_BUFFER_TOO_SMALL, "keys-remaining small buffer"); + ret = (ret == CKR_BUFFER_TOO_SMALL) ? CKR_OK : CKR_GENERAL_ERROR; + } + /* The count decrements by one after a signature. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "C_Sign"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingAfter); + CHECK_CKR(ret, "keys-remaining after sign"); + } + if (ret == CKR_OK && remainingAfter != remaining - 1) { + fprintf(stderr, "\nkeys-remaining did not decrement: %lu -> %lu " + "- FAIL\n", (unsigned long)remaining, (unsigned long)remainingAfter); + ret = CKR_GENERAL_ERROR; + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-9: private-key import carrying CKA_VALUE is rejected ---- */ +static CK_RV test_import_value_rejected(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE obj = CK_INVALID_HANDLE; + CK_OBJECT_CLASS privClass = CKO_PRIVATE_KEY; + CK_KEY_TYPE keyType = CKK_HSS; + CK_BYTE fakeValue[64]; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &privClass, sizeof(privClass) }, + { CKA_KEY_TYPE, &keyType, sizeof(keyType) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + { CKA_VALUE, fakeValue, sizeof(fakeValue) }, + }; + + printf(" private-key import with CKA_VALUE is rejected...\n"); + XMEMSET(fakeValue, 0xAB, sizeof(fakeValue)); + ret = fresh_token(&session); + if (ret == CKR_OK) { + CK_RV cret = funcList->C_CreateObject(session, tmpl, + sizeof(tmpl) / sizeof(*tmpl), &obj); + if (cret == CKR_OK) { + fprintf(stderr, "\nValue-carrying HSS private-key import accepted " + "- FAIL\n"); + funcList->C_DestroyObject(session, obj); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" rejected with %lx (expected)\n", (unsigned long)cret); + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-7: exhaustion poisons the key -> CKR_DEVICE_ERROR ---- */ +static CK_RV test_exhaustion_poison(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-exhaust"; + CK_MECHANISM mech; + CK_BYTE sig[4096]; + CK_ULONG sigLen; + CK_ULONG initialRemaining = 0; + int successes = 0; + int i; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + printf(" key exhaustion poisons the key...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &initialRemaining); + CHECK_CKR(ret, "initial keys-remaining"); + } + /* Sign until the tree is exhausted. Each failed C_Sign must have cleared + * the operation (MEDIUM-4), so the next C_SignInit succeeds. */ + for (i = 0; ret == CKR_OK && i < HSS_H5_SIGS + 4; i++) { + CK_RV sret = funcList->C_SignInit(session, &mech, priv); + if (sret != CKR_OK) { + fprintf(stderr, "\nC_SignInit failed at iter %d: %lx - FAIL\n", + i, (unsigned long)sret); + ret = CKR_GENERAL_ERROR; + break; + } + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (sret == CKR_OK) { + successes++; + } + else { + /* First failure: the tree is spent. The very next attempt must be + * reported as a poisoned key (CKR_DEVICE_ERROR). */ + printf(" exhausted after %d signatures (first failure %lx)\n", + successes, (unsigned long)sret); + sret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR(sret, "C_SignInit after exhaustion"); + if (sret == CKR_OK) { + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, + TEST_MSG_LEN, sig, &sigLen); + CHECK_CKR_FAIL(sret, CKR_DEVICE_ERROR, + "poisoned-key C_Sign"); + if (sret != CKR_DEVICE_ERROR) + ret = CKR_GENERAL_ERROR; + } + else { + ret = CKR_GENERAL_ERROR; + } + break; + } + } + if (ret == CKR_OK && successes != (int)initialRemaining) { + fprintf(stderr, "\nSigned %d times, keys-remaining said %lu - FAIL\n", + successes, (unsigned long)initialRemaining); + ret = CKR_GENERAL_ERROR; + } + close_session(session); + pkcs11_final(); + return ret; +} + +#ifndef _WIN32 +/* Locate the single encrypted HSS state file in the store directory. */ +static CK_RV find_state_file(char* path, size_t pathLen) +{ + DIR* d; + struct dirent* e; + CK_RV ret = CKR_OBJECT_HANDLE_INVALID; + const char* prefix = "wp11_hsskey_priv_state_"; + + d = opendir(storeDir); + if (d == NULL) + return CKR_GENERAL_ERROR; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, prefix, strlen(prefix)) == 0) { + snprintf(path, pathLen, "%s/%s", storeDir, e->d_name); + ret = CKR_OK; + break; + } + } + closedir(d); + return ret; +} + +/* ---- MEDIUM-10: C_DestroyObject removes the encrypted state file ---- */ +static CK_RV test_destroy_removes_state(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-destroy"; + char statePath[512]; + FILE* f; + + printf(" C_DestroyObject removes the state file...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + if (ret == CKR_OK) { + ret = find_state_file(statePath, sizeof(statePath)); + CHECK_CKR(ret, "locate state file after keygen"); + } + if (ret == CKR_OK) { + ret = funcList->C_DestroyObject(session, priv); + CHECK_CKR(ret, "C_DestroyObject(private key)"); + } + if (ret == CKR_OK) { + f = fopen(statePath, "rb"); + if (f != NULL) { + fclose(f); + fprintf(stderr, "\nState file still present after destroy: %s " + "- FAIL\n", statePath); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" state file removed\n"); + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-11: a tampered on-disk state file is detected on reload ---- */ +static CK_RV test_state_tamper_detected(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-tamper"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + char statePath[512]; + FILE* f; + long fsize; + int flipByte; + + printf(" tampered state file is detected on reload...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + /* Sign once so a real advanced state is on disk before we tamper it. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "C_Sign before tamper"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = find_state_file(statePath, sizeof(statePath)); + /* Force everything to disk, then flip a ciphertext byte near the end. */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) { + f = fopen(statePath, "rb+"); + if (f == NULL) + ret = CKR_GENERAL_ERROR; + else { + CK_BYTE b = 0; + fseek(f, 0, SEEK_END); + fsize = ftell(f); + flipByte = (int)(fsize > 4 ? fsize - 4 : 0); + fseek(f, flipByte, SEEK_SET); + if (fread(&b, 1, 1, f) == 1) { + b ^= 0xFF; + fseek(f, flipByte, SEEK_SET); + fwrite(&b, 1, 1, f); + } + else { + ret = CKR_GENERAL_ERROR; + } + fclose(f); + } + } + /* Reload and attempt to use the key. The AES-GCM authentication failure + * must surface as an error and never produce a signature from + * unauthenticated state. Detection may happen at C_Login (the reload runs + * during login-time decode) or, if login proceeds, at C_Sign. Either is a + * safe outcome; only a produced signature is a failure. */ + if (ret == CKR_OK) { + CK_RV rret; + + printf(" --- reloading tampered token ---\n"); + rret = reopen_token_quiet(&session); + if (rret != CKR_OK) { + printf(" tamper detected at login: %lx\n", + (unsigned long)rret); + } + else if (find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv) != CKR_OK) { + printf(" tampered key not usable after login\n"); + } + else { + CK_RV sret = hss_sign(session, priv, &sig, &sigLen); + if (sret == CKR_OK) { + fprintf(stderr, "\nSigned with tampered state - FAIL\n"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" signing correctly refused: %lx\n", + (unsigned long)sret); + } + } + } + close_session(session); + pkcs11_final(); + return ret; +} +#endif /* !_WIN32 */ + +/* ---- Phase 2: parameter matrix ---- */ +typedef struct { + const char* name; + CK_ULONG levels; + CK_ULONG lms; + CK_ULONG lmots; + CK_ULONG capacity; /* 2^(levels*height) */ + int fast; /* 1 = part of the default fast subset */ +} HssParamSet; + +static const HssParamSet paramSets[] = { + { "L1/H5/W1", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W1, 32, 0 }, + { "L1/H5/W2", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W2, 32, 0 }, + { "L1/H5/W4", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W4, 32, 1 }, + { "L1/H5/W8", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W8, 32, 0 }, + { "L1/H10/W4", 1, CKL_LMS_SHA256_M32_H10, CKL_LMOTS_SHA256_N32_W4, 1024, 0 }, + { "L2/H5/W4", 2, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W4, 1024, 1 }, +}; + +/* keygen -> sign x2 -> verify -> reload -> confirm counter preserved -> sign + * -> verify, for one parameter set. Each set gets a fresh token. */ +static CK_RV run_param_set(const HssParamSet* ps) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-param"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, remaining = 0, remaining2 = 0; + int i; + + printf(" [%s] keygen/sign/verify/reload...\n", ps->name); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, ps->levels, ps->lms, ps->lmots, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "param-set keygen"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remaining); + CHECK_CKR(ret, "param-set keys-remaining"); + } + if (ret == CKR_OK && remaining != ps->capacity) { + fprintf(stderr, "\n[%s] capacity %lu, expected %lu - FAIL\n", ps->name, + (unsigned long)remaining, (unsigned long)ps->capacity); + ret = CKR_GENERAL_ERROR; + } + for (i = 0; ret == CKR_OK && i < 2; i++) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "param-set pre-reload sign/verify"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + /* Reload and confirm the counter survived (capacity - 2 signs). */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &remaining2); + if (ret == CKR_OK && remaining2 != ps->capacity - 2) { + fprintf(stderr, "\n[%s] post-reload remaining %lu, expected %lu " + "- FAIL\n", ps->name, (unsigned long)remaining2, + (unsigned long)(ps->capacity - 2)); + ret = CKR_GENERAL_ERROR; + } + if (ret == CKR_OK) + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "param-set post-reload sign/verify"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +static CK_RV test_param_matrix(void) +{ + CK_RV ret = CKR_OK; + int full = 0, i, n; + +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_HSS_FULL_MATRIX") != NULL) + full = 1; +#endif + n = (int)(sizeof(paramSets) / sizeof(paramSets[0])); + printf(" parameter matrix (%s)...\n", full ? "full" : "fast subset"); + for (i = 0; ret == CKR_OK && i < n; i++) { + if (!full && !paramSets[i].fast) + continue; + ret = run_param_set(¶mSets[i]); + } + if (ret == CKR_OK && !full) { + printf(" (set WOLFPKCS11_HSS_FULL_MATRIX=1 for W1/W2/W8 and H10)\n"); + } + return ret; +} + +/* Multi-level HSS (L2/H5): the lower subtree holds 2^5 = 32 one-time keys. + * Signing past that boundary exhausts the lower tree and advances the parent to + * a fresh subtree - the most error-prone HSS state transition. Confirm signing + * across the boundary verifies on both sides and that a reload straddling the + * boundary resumes correctly. */ +static CK_RV test_multilevel_rollover(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-rollover"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, remaining = 0; + const int crossAt = 32; /* lower-subtree size, 2^5 */ + const int total = 36; /* a few signatures past the boundary */ + int i; + + printf(" multi-level (L2/H5) subtree rollover...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, 2, CKL_LMS_SHA256_M32_H5, + CKL_LMOTS_SHA256_N32_W4, id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "L2 keygen"); + } + for (i = 0; ret == CKR_OK && i < total; i++) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "rollover sign/verify"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + /* Reload right after crossing into the second subtree. */ + if (ret == CKR_OK && i == crossAt) { + printf(" --- reload straddling the subtree boundary ---\n"); + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, + &pub); + CHECK_CKR(ret, "reload across boundary"); + } + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &remaining); + if (ret == CKR_OK && remaining != (CK_ULONG)(1024 - total)) { + fprintf(stderr, "\nrollover: remaining %lu, expected %d - FAIL\n", + (unsigned long)remaining, 1024 - total); + ret = CKR_GENERAL_ERROR; + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +static CK_RV run_all(void) +{ + CK_RV ret; + + ret = test_keygen_sign_verify(); + if (ret == CKR_OK) + ret = test_reload_no_rollback(); + if (ret == CKR_OK) + ret = test_keygen_negative_templates(); + if (ret == CKR_OK) + ret = test_keys_remaining_private(); + if (ret == CKR_OK) + ret = test_import_value_rejected(); + if (ret == CKR_OK) + ret = test_exhaustion_poison(); + if (ret == CKR_OK) + ret = test_param_matrix(); + if (ret == CKR_OK) + ret = test_multilevel_rollover(); +#ifndef _WIN32 + if (ret == CKR_OK) + ret = test_destroy_removes_state(); + if (ret == CKR_OK) + ret = test_state_tamper_detected(); +#endif + return ret; +} +#endif /* WOLFPKCS11_LMS_PRIVATE && !WOLFPKCS11_NO_STORE */ + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) + { + CK_RV ret; + +#ifndef WOLFPKCS11_NO_ENV + if (!XGETENV("WOLFPKCS11_TOKEN_PATH")) + XSETENV("WOLFPKCS11_TOKEN_PATH", storeDir, 1); + else + storeDir = XGETENV("WOLFPKCS11_TOKEN_PATH"); +#endif + + printf("wolfPKCS11 LMS/HSS private-key (sign) test\n"); + printf("=========================================\n\n"); + + ret = run_all(); + + cleanup_store(); + if (ret == CKR_OK) { + printf("\nAll tests passed!\n"); + return 0; + } + printf("\nTest failed with error: %lx\n", (unsigned long)ret); + return 1; + } +#else + printf("LMS/HSS sign support (--enable-lms-private) not compiled in!\n"); + return 77; +#endif +} diff --git a/tests/hss_concurrency_test.c b/tests/hss_concurrency_test.c new file mode 100644 index 00000000..9bde95e8 --- /dev/null +++ b/tests/hss_concurrency_test.c @@ -0,0 +1,525 @@ +/* hss_concurrency_test.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 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. + * + * wolfPKCS11 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 + * + * Concurrency test for the EXPERIMENTAL sign-capable LMS/HSS build + * (--enable-lms-private). The one catastrophic failure mode of a stateful + * hash-based scheme is two signers using the same one-time-signature (OTS) + * leaf index. This test drives many threads signing concurrently with a single + * shared token-resident HSS private key, extracts the leaf index q from every + * RFC 8554 signature, and asserts: + * + * - every one of the N*M signatures carries a distinct leaf index, + * - the indices are exactly {0, 1, ..., N*M-1} (no reuse AND no gaps), + * - every signature verifies against the public key, + * - CKA_HSS_KEYS_REMAINING decreased by exactly N*M. + * + * Best run under ThreadSanitizer. + * + * The scheme is HSS L1 (single level) so the leaf index is the big-endian u32 + * immediately after the 4-byte HSS Nspk field: sig = u32(Nspk=0) || lms_sig, + * and lms_sig = u32(q) || ... (RFC 8554 sec. 3.3, 5.4). + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include + +#ifndef WOLFPKCS11_USER_SETTINGS + #include +#endif +#include + +#ifndef HAVE_PKCS11_STATIC +#include +#endif + +#include +#include + +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) && \ + !defined(SINGLE_THREADED) && !defined(_WIN32) + +#include +#include +#include + +#ifndef WOLFPKCS11_DLL_FILENAME + #ifdef __MACH__ + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.dylib" + #else + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.so" + #endif +#endif + +/* H10 = 1024-leaf tree; NUM_THREADS*SIGS_PER_THREAD stays well under it so the + * test exercises contention without hitting the exhaustion boundary. */ +#define NUM_THREADS 4 +#define SIGS_PER_THREAD 64 +#define TOTAL_SIGS (NUM_THREADS * SIGS_PER_THREAD) +#define MAX_SIG_LEN 8192 + +#define CHECK_CKR(rv, msg) \ + do { \ + if ((rv) != CKR_OK) { \ + fprintf(stderr, "\n%s:%d - %s: %lx - FAIL\n", \ + __FILE__, __LINE__, msg, (unsigned long)(rv)); \ + } \ + } \ + while (0) + +#ifndef HAVE_PKCS11_STATIC +static void* dlib; +#endif +static CK_FUNCTION_LIST* funcList; +static CK_SLOT_ID slot = 0; +static const char* tokenName = "wolfpkcs11"; +static byte* soPin = (byte*)"password123456"; +static int soPinLen = 14; +static byte* userPin = (byte*)"wolfpkcs11-test"; +static int userPinLen = 15; +static const char* storeDir = "./store/hss_conc"; + +static CK_BBOOL ckTrue = CK_TRUE; +static CK_ULONG hssLevels = 1; +static CK_ULONG hssLmsType = CKL_LMS_SHA256_M32_H10; +static CK_ULONG hssLmotsType = CKL_LMOTS_SHA256_N32_W4; +static CK_BYTE keyId[] = "hss-concurrency"; +static const CK_BYTE testMsg[] = "wolfPKCS11 HSS concurrency test message"; +#define TEST_MSG_LEN ((CK_ULONG)(sizeof(testMsg) - 1)) + +/* Per-thread context: the leaf indices this thread observed. */ +typedef struct { + int threadId; + word32 leaf[SIGS_PER_THREAD]; + int signedCount; + CK_RV err; +} WorkerCtx; + +static CK_RV pkcs11_init(void) +{ + CK_RV ret; + CK_C_INITIALIZE_ARGS args; + CK_SLOT_ID slotList[16]; + CK_ULONG slotCount = sizeof(slotList) / sizeof(slotList[0]); +#ifndef HAVE_PKCS11_STATIC + CK_C_GetFunctionList func; + + dlib = dlopen(WOLFPKCS11_DLL_FILENAME, RTLD_NOW | RTLD_LOCAL); + if (dlib == NULL) { + fprintf(stderr, "dlopen error: %s\n", dlerror()); + return CKR_GENERAL_ERROR; + } + func = (CK_C_GetFunctionList)dlsym(dlib, "C_GetFunctionList"); + if (func == NULL) { + dlclose(dlib); + return CKR_GENERAL_ERROR; + } + ret = func(&funcList); + if (ret != CKR_OK) { + dlclose(dlib); + return ret; + } +#else + ret = C_GetFunctionList(&funcList); + if (ret != CKR_OK) + return ret; +#endif + + XMEMSET(&args, 0, sizeof(args)); + args.flags = CKF_OS_LOCKING_OK; + ret = funcList->C_Initialize(&args); + CHECK_CKR(ret, "Initialize"); + if (ret == CKR_OK) { + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + CHECK_CKR(ret, "Get Slot List"); + } + if (ret == CKR_OK) { + if (slotCount > 0) + slot = slotList[0]; + else + ret = CKR_GENERAL_ERROR; + } + return ret; +} + +static void pkcs11_final(void) +{ + funcList->C_Finalize(NULL); +#ifndef HAVE_PKCS11_STATIC + if (dlib != NULL) { + dlclose(dlib); + dlib = NULL; + } +#endif +} + +static CK_RV pkcs11_init_token(void) +{ + CK_RV ret; + unsigned char label[32]; + CK_SESSION_HANDLE session; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + XMEMSET(label, ' ', sizeof(label)); + XMEMCPY(label, tokenName, strlen(tokenName)); + ret = funcList->C_InitToken(slot, soPin, soPinLen, label); + CHECK_CKR(ret, "Init Token"); + if (ret == CKR_OK) { + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, &session); + CHECK_CKR(ret, "Open Session for PIN setup"); + } + if (ret == CKR_OK) { + ret = funcList->C_Login(session, CKU_SO, soPin, soPinLen); + CHECK_CKR(ret, "Login SO"); + if (ret == CKR_OK) { + ret = funcList->C_InitPIN(session, userPin, userPinLen); + CHECK_CKR(ret, "Init User PIN"); + } + funcList->C_Logout(session); + funcList->C_CloseSession(session); + } + return ret; +} + +/* Open a session and ensure the token is logged in as the user. Login is + * application-wide, so a second concurrent login returns + * CKR_USER_ALREADY_LOGGED_IN, which is success for our purposes. */ +static CK_RV open_logged_in_session(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); + if (ret != CKR_OK) + return ret; + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + if (ret == CKR_USER_ALREADY_LOGGED_IN) + ret = CKR_OK; + return ret; +} + +static CK_RV gen_hss_key(CK_SESSION_HANDLE session) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_ATTRIBUTE pubTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, keyId, sizeof(keyId) - 1 }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + CK_ATTRIBUTE privTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, keyId, sizeof(keyId) - 1 }, + }; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl) / sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl) / sizeof(*privTmpl), &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair(HSS L1/H10)"); + return ret; +} + +static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, + CK_OBJECT_HANDLE* obj) +{ + CK_RV ret; + CK_ULONG count = 0; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &cls, sizeof(cls) }, + { CKA_ID, keyId, sizeof(keyId) - 1 }, + }; + + ret = funcList->C_FindObjectsInit(session, tmpl, + sizeof(tmpl) / sizeof(*tmpl)); + if (ret == CKR_OK) { + ret = funcList->C_FindObjects(session, obj, 1, &count); + funcList->C_FindObjectsFinal(session); + } + if (ret == CKR_OK && count != 1) + ret = CKR_GENERAL_ERROR; + return ret; +} + +/* Big-endian u32 read. */ +static word32 read_be32(const CK_BYTE* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) | + ((word32)p[2] << 8) | (word32)p[3]; +} + +/* Extract the LMS leaf index q from an HSS L1 signature. Returns 0 on success + * and sets *q; non-zero if the signature is too short or not single-level. */ +static int hss_leaf_index(const CK_BYTE* sig, CK_ULONG sigLen, word32* q) +{ + word32 nspk; + + if (sigLen < 8) + return -1; + nspk = read_be32(sig); /* HSS Nspk = L-1; must be 0 for L1 */ + if (nspk != 0) + return -1; + *q = read_be32(sig + 4); /* first field of the LMS signature */ + return 0; +} + +static void* worker(void* arg) +{ + WorkerCtx* ctx = (WorkerCtx*)arg; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE priv = CK_INVALID_HANDLE, pub = CK_INVALID_HANDLE; + CK_MECHANISM mech; + CK_BYTE sig[MAX_SIG_LEN]; + CK_ULONG sigLen; + CK_RV ret; + int i; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = open_logged_in_session(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, &pub); + + for (i = 0; ret == CKR_OK && i < SIGS_PER_THREAD; i++) { + ret = funcList->C_SignInit(session, &mech, priv); + if (ret != CKR_OK) + break; + sigLen = sizeof(sig); + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (ret != CKR_OK) + break; + /* Verify and record the leaf index. */ + ret = funcList->C_VerifyInit(session, &mech, pub); + if (ret == CKR_OK) { + ret = funcList->C_Verify(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, + sig, sigLen); + } + if (ret != CKR_OK) + break; + if (hss_leaf_index(sig, sigLen, &ctx->leaf[i]) != 0) { + ret = CKR_GENERAL_ERROR; + break; + } + ctx->signedCount++; + } + + if (session != CK_INVALID_HANDLE) + funcList->C_CloseSession(session); + ctx->err = ret; + return NULL; +} + +static CK_RV get_keys_remaining(CK_SESSION_HANDLE session, CK_ULONG* remaining) +{ + CK_RV ret; + CK_OBJECT_HANDLE priv = CK_INVALID_HANDLE; + CK_ATTRIBUTE a; + + ret = find_by_id(session, CKO_PRIVATE_KEY, &priv); + if (ret == CKR_OK) { + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = remaining; + a.ulValueLen = sizeof(*remaining); + ret = funcList->C_GetAttributeValue(session, priv, &a, 1); + } + return ret; +} + +static int cmp_word32(const void* a, const void* b) +{ + word32 x = *(const word32*)a; + word32 y = *(const word32*)b; + + if (x < y) + return -1; + if (x > y) + return 1; + return 0; +} + +static void cleanup_store(void) +{ + DIR* d; + struct dirent* e; + char path[512]; + + d = opendir(storeDir); + if (d == NULL) + return; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, "wp11_", 5) == 0) { + snprintf(path, sizeof(path), "%s/%s", storeDir, e->d_name); + (void)remove(path); + } + } + closedir(d); +} + +static CK_RV run_test(void) +{ + CK_RV ret; + CK_SESSION_HANDLE keeper = CK_INVALID_HANDLE; + pthread_t threads[NUM_THREADS]; + WorkerCtx ctx[NUM_THREADS]; + word32 allLeaves[TOTAL_SIGS]; + CK_ULONG remainBefore = 0, remainAfter = 0; + int i, j, n = 0; + int spawned = 0; + + XMEMSET(ctx, 0, sizeof(ctx)); + + cleanup_store(); + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = pkcs11_init_token(); + /* Keeper session keeps the token logged in for the workers. */ + if (ret == CKR_OK) + ret = open_logged_in_session(&keeper); + if (ret == CKR_OK) + ret = gen_hss_key(keeper); + if (ret == CKR_OK) + ret = get_keys_remaining(keeper, &remainBefore); + + /* Fan out. */ + for (i = 0; ret == CKR_OK && i < NUM_THREADS; i++) { + ctx[i].threadId = i; + ctx[i].err = CKR_OK; + if (pthread_create(&threads[i], NULL, worker, &ctx[i]) != 0) { + ret = CKR_GENERAL_ERROR; + break; + } + spawned++; + } + for (i = 0; i < spawned; i++) + pthread_join(threads[i], NULL); + + /* Collect results. */ + for (i = 0; ret == CKR_OK && i < NUM_THREADS; i++) { + if (ctx[i].err != CKR_OK) { + fprintf(stderr, "\nthread %d failed: %lx - FAIL\n", i, + (unsigned long)ctx[i].err); + ret = ctx[i].err; + } + else if (ctx[i].signedCount != SIGS_PER_THREAD) { + fprintf(stderr, "\nthread %d signed %d/%d - FAIL\n", i, + ctx[i].signedCount, SIGS_PER_THREAD); + ret = CKR_GENERAL_ERROR; + } + else { + for (j = 0; j < SIGS_PER_THREAD; j++) + allLeaves[n++] = ctx[i].leaf[j]; + } + } + + /* The N*M signatures must use leaf indices {0, 1, ..., N*M-1} exactly - + * any duplicate is an OTS reuse, any gap is a lost index. */ + if (ret == CKR_OK) { + qsort(allLeaves, TOTAL_SIGS, sizeof(word32), cmp_word32); + for (i = 0; i < TOTAL_SIGS; i++) { + if (allLeaves[i] != (word32)i) { + if (i > 0 && allLeaves[i] == allLeaves[i - 1]) { + fprintf(stderr, "\nOTS INDEX REUSED: leaf %lu appears " + "twice - FAIL\n", (unsigned long)allLeaves[i]); + } + else { + fprintf(stderr, "\nleaf index sequence broken at %d: got " + "%lu - FAIL\n", i, (unsigned long)allLeaves[i]); + } + ret = CKR_GENERAL_ERROR; + break; + } + } + } + + /* Keys-remaining must have dropped by exactly TOTAL_SIGS. */ + if (ret == CKR_OK) + ret = get_keys_remaining(keeper, &remainAfter); + if (ret == CKR_OK && remainAfter != remainBefore - TOTAL_SIGS) { + fprintf(stderr, "\nkeys-remaining %lu -> %lu, expected drop of %d " + "- FAIL\n", (unsigned long)remainBefore, + (unsigned long)remainAfter, TOTAL_SIGS); + ret = CKR_GENERAL_ERROR; + } + if (ret == CKR_OK) { + printf(" %d threads x %d signatures = %d unique leaf indices, all " + "verified\n", NUM_THREADS, SIGS_PER_THREAD, TOTAL_SIGS); + } + + if (keeper != CK_INVALID_HANDLE) { + funcList->C_Logout(keeper); + funcList->C_CloseSession(keeper); + } + pkcs11_final(); + return ret; +} +#endif /* WOLFPKCS11_LMS_PRIVATE && !NO_STORE && !SINGLE_THREADED && !_WIN32 */ + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) && \ + !defined(SINGLE_THREADED) && !defined(_WIN32) + { + CK_RV ret; + +#ifndef WOLFPKCS11_NO_ENV + if (!XGETENV("WOLFPKCS11_TOKEN_PATH")) + XSETENV("WOLFPKCS11_TOKEN_PATH", storeDir, 1); + else + storeDir = XGETENV("WOLFPKCS11_TOKEN_PATH"); +#endif + + printf("wolfPKCS11 HSS concurrent-signing (OTS reuse) test\n"); + printf("=================================================\n\n"); + + ret = run_test(); + + cleanup_store(); + if (ret == CKR_OK) { + printf("\nAll tests passed!\n"); + return 0; + } + printf("\nTest failed with error: %lx\n", (unsigned long)ret); + return 1; + } +#else + printf("LMS/HSS sign, store, and threads required - skipping.\n"); + return 77; +#endif +} diff --git a/tests/include.am b/tests/include.am index 5af7cbba..7d9164ac 100644 --- a/tests/include.am +++ b/tests/include.am @@ -250,6 +250,16 @@ noinst_PROGRAMS += tests/hbs_persistence_test tests_hbs_persistence_test_SOURCES = tests/hbs_persistence_test.c tests_hbs_persistence_test_LDADD = +check_PROGRAMS += tests/hbs_private_test +noinst_PROGRAMS += tests/hbs_private_test +tests_hbs_private_test_SOURCES = tests/hbs_private_test.c +tests_hbs_private_test_LDADD = + +check_PROGRAMS += tests/hss_concurrency_test +noinst_PROGRAMS += tests/hss_concurrency_test +tests_hss_concurrency_test_SOURCES = tests/hss_concurrency_test.c +tests_hss_concurrency_test_LDADD = + if BUILD_STATIC tests_pkcs11test_LDADD += src/libwolfpkcs11.la tests_pkcs11mtt_LDADD += src/libwolfpkcs11.la @@ -301,6 +311,8 @@ tests_logout_token_key_zero_test_LDADD += src/libwolfpkcs11.la tests_tpm_decode_bounds_test_LDADD += src/libwolfpkcs11.la tests_concurrent_destroy_object_test_LDADD += src/libwolfpkcs11.la tests_hbs_persistence_test_LDADD += src/libwolfpkcs11.la +tests_hbs_private_test_LDADD += src/libwolfpkcs11.la +tests_hss_concurrency_test_LDADD += src/libwolfpkcs11.la else tests_object_id_uniqueness_test_LDADD += src/libwolfpkcs11.la tests_empty_pin_store_test_LDADD += src/libwolfpkcs11.la @@ -344,6 +356,8 @@ tests_logout_token_key_zero_test_LDADD += src/libwolfpkcs11.la tests_tpm_decode_bounds_test_LDADD += src/libwolfpkcs11.la tests_concurrent_destroy_object_test_LDADD += src/libwolfpkcs11.la tests_hbs_persistence_test_LDADD += src/libwolfpkcs11.la +tests_hbs_private_test_LDADD += src/libwolfpkcs11.la +tests_hss_concurrency_test_LDADD += src/libwolfpkcs11.la endif EXTRA_DIST += tests/unit.h \ From e0353a43ea40657a3a1ac64abd91a0870b2ca087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 09:36:42 +0200 Subject: [PATCH 07/16] ci: add --enable-lms-private build and test jobs Parameterize build-workflow.yml with wolfssl_lms/wolfssl_xmss inputs (default verify-only) so a job can request a sign-capable wolfSSL LMS build, and add lms_private and lms_private_static jobs to unit-test.yml. --- .github/workflows/build-workflow.yml | 10 +++++++++- .github/workflows/unit-test.yml | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index 9741730b..ffa5bb40 100644 --- a/.github/workflows/build-workflow.yml +++ b/.github/workflows/build-workflow.yml @@ -11,6 +11,14 @@ on: required: false type: string default: 'make check' + wolfssl_lms: + required: false + type: string + default: 'verify-only' + wolfssl_xmss: + required: false + type: string + default: 'verify-only' jobs: build: @@ -36,7 +44,7 @@ jobs: working-directory: ./wolfssl run: | ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 \ - --enable-mldsa --enable-lms=verify-only --enable-xmss=verify-only C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa --enable-lms=${{inputs.wolfssl_lms}} --enable-xmss=${{inputs.wolfssl_xmss}} C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" - name: wolfssl make install working-directory: ./wolfssl run: make diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8cfcb303..b699744a 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -126,6 +126,19 @@ jobs: uses: ./.github/workflows/build-workflow.yml with: config: --enable-lms --enable-xmss --disable-shared + # EXPERIMENTAL LMS/HSS keygen + signing. Needs a sign-capable wolfSSL LMS + # build (full --enable-lms, not verify-only); --enable-lms-private implies + # --enable-lms on the wolfPKCS11 side. + lms_private: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private + wolfssl_lms: 'yes' + lms_private_static: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private --disable-shared + wolfssl_lms: 'yes' debug: uses: ./.github/workflows/build-workflow.yml with: From 28f865a954ffecf02936e7fc64923c3857f34ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 09:36:42 +0200 Subject: [PATCH 08/16] docs: document --enable-lms-private and WOLFPKCS11_STATEFUL_RELAX_FSYNC Add a README section and CMake options rows for the EXPERIMENTAL LMS/HSS keygen+signing feature, covering the stateful/OTS-reuse caveat and the non-production WOLFPKCS11_STATEFUL_RELAX_FSYNC environment variable. --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index c7908a49..df76acec 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,33 @@ As ML-KEM is a feature of PKCS#11 version 3.2, support for that is required, too. Hence, to enable all in wolfPKCS11, add `--enable-pkcs11v32 --enable-mlkem` during the configure step. +### Optional: LMS/HSS Hash-Based Signatures (RFC 8554) + +Verification and public-key import for LMS/HSS are enabled with `--enable-lms` +(or `WOLFPKCS11_LMS=yes` in CMake) and require a wolfSSL build with LMS support +(`--enable-lms`, `WOLFSSL_HAVE_LMS`). LMS/HSS is a feature of PKCS#11 version +3.2, so that support is enabled automatically. + +Key generation and signing are **EXPERIMENTAL** and off by default. Enable them +with `--enable-lms-private` (or `WOLFPKCS11_LMS_PRIVATE=yes` in CMake), which +implies `--enable-lms` and requires a *sign-capable* wolfSSL LMS build (full +`--enable-lms`, not `--enable-lms=verify-only`). + +LMS/HSS is a **stateful** signature scheme: every one-time-signature (OTS) leaf +index must be used at most once, and reusing an index catastrophically breaks +security. wolfPKCS11 therefore: + +* Requires stateful private keys to be token-resident (`CKA_TOKEN=TRUE`); a + session-only key that vanished on a crash could release a signature whose OTS + index was never persisted. +* Durably persists the advanced key state (fsync plus atomic rename of an + encrypted, authenticated state file) *before* each signature is released, so a + crash or power loss cannot roll the OTS index backwards. + +Because the signature state is durable and per-key, do not clone, snapshot, or +restore the token storage directory of a machine that signs with LMS/HSS keys - +rolling the state files back to an earlier point would reuse OTS indices. + ### Build options and defines #### Define WOLFPKCS11_TPM_STORE @@ -267,6 +294,8 @@ cmake -DCMAKE_PREFIX_PATH=/path/to/wolfssl/install .. | `WOLFPKCS11_PKCS11_V3_2` | `no` | PKCS#11 v3.2 support | | `WOLFPKCS11_MLDSA` | `no` | ML-DSA support | | `WOLFPKCS11_MLKEM` | `no` | ML-KEM support | +| `WOLFPKCS11_LMS` | `no` | LMS/HSS verification and public-key import | +| `WOLFPKCS11_LMS_PRIVATE` | `no` | LMS/HSS keygen + signing (EXPERIMENTAL, stateful private keys) | | `WOLFPKCS11_EXAMPLES` | `yes` | Build examples | | `WOLFPKCS11_TESTS` | `yes` | Build and register tests | | `WOLFPKCS11_COVERAGE` | `no` | Code coverage support | @@ -294,6 +323,15 @@ POSIX or `%APPDIR%\wolfPKCS11` on Windows), and finally the optional Set to any value to stop storage of token data. +### WOLFPKCS11_STATEFUL_RELAX_FSYNC + +**Non-production only.** When set to `1`, wolfPKCS11 skips the `fsync()` calls +(file and parent directory) that make LMS/HSS signature-state writes durable. +This speeds up test runs on throwaway storage but is **dangerous**: after a +crash or power loss the on-disk state can lag the signatures already released, +leading to **OTS-key reuse**. Leave it unset (the default always fsyncs) in any +real deployment. Only meaningful when built with `--enable-lms-private`. + ## Release Notes From f781df32207e7d52c989f761300f9f1bbf1a66e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 09:46:59 +0200 Subject: [PATCH 09/16] tests: add LMS/HSS fault-injection and durability tests Extend hbs_private_test.c (POSIX only): - test_write_failure_poison: make the store directory unwritable mid-sign so the durable state write fails. The sign is refused, the key is poisoned (subsequent C_Sign -> CKR_DEVICE_ERROR), and a reload recovers to the last durable index - never advancing past the failed write. - test_crash_durability: a forked child reloads the token, signs once, and _exit()s with no C_Finalize, simulating a crash right after a signature. The parent confirms the per-sign fsync made the advance durable and the consumed one-time index is never reused - durability does not depend on clean shutdown. - test_corruption_matrix: truncated file, flipped magic, flipped scheme-param, and flipped GCM tag are each detected fail-closed at login-time reload, never yielding a signature from unauthenticated state. --- tests/hbs_private_test.c | 302 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/tests/hbs_private_test.c b/tests/hbs_private_test.c index 11e83eaf..259d0ddd 100644 --- a/tests/hbs_private_test.c +++ b/tests/hbs_private_test.c @@ -66,6 +66,9 @@ #ifndef _WIN32 #include +#include +#include +#include #endif #ifndef WOLFPKCS11_DLL_FILENAME @@ -998,6 +1001,299 @@ static CK_RV test_state_tamper_detected(void) pkcs11_final(); return ret; } + +/* ---- Phase 3: state-write failure poisons the key; reload never advances + * past the last durable index. ---- */ +static CK_RV test_write_failure_poison(void) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-wfail"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, before = 0, after = 0; + + printf(" state-write failure poisons the key...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + /* One good signature establishes a durable baseline index on disk. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "baseline sign"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &before); + /* Make the store directory unwritable so the durable state write fails. */ + if (ret == CKR_OK && chmod(storeDir, 0500) != 0) + ret = CKR_GENERAL_ERROR; + if (ret == CKR_OK) { + sret = hss_sign(session, priv, &sig, &sigLen); + if (sret == CKR_OK) { + fprintf(stderr, "\nsigned despite an unwritable store - FAIL\n"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + ret = CKR_GENERAL_ERROR; + } + else { + printf(" write-failed sign refused: %lx\n", (unsigned long)sret); + } + } + (void)chmod(storeDir, 0700); /* restore, even on failure */ + /* The key is poisoned until reloaded: the next sign is CKR_DEVICE_ERROR. */ + if (ret == CKR_OK) { + sret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR_FAIL(sret, CKR_DEVICE_ERROR, "poisoned-key sign"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + if (sret != CKR_DEVICE_ERROR) + ret = CKR_GENERAL_ERROR; + } + /* Reload must recover to the last durable index - the failed write must not + * have advanced it. */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &after); + if (ret == CKR_OK && after != before) { + fprintf(stderr, "\nreload advanced past a failed write: %lu -> %lu " + "- FAIL\n", (unsigned long)before, (unsigned long)after); + ret = CKR_GENERAL_ERROR; + } + /* And it still signs correctly from the recovered state. */ + if (ret == CKR_OK) + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "post-recovery sign/verify"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- Phase 3: per-sign durability survives an unclean exit. A forked child + * signs and _exit()s with NO C_Finalize; the parent confirms the state advanced + * durably and the consumed index is never reused. ---- */ +static CK_RV test_crash_durability(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-crash"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, before = 0, after = 0; + pid_t pid; + int status = 0; + + printf(" per-sign durability survives an unclean exit (fork)...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &before); + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret != CKR_OK) + return ret; + + pid = fork(); + if (pid == 0) { + /* Child: fresh process. Reload, sign once (durable), then hard-exit + * with no C_Finalize, simulating a crash right after a signature. */ + CK_SESSION_HANDLE cs = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE cp = CK_INVALID_HANDLE; + CK_BYTE* csig = NULL; + CK_ULONG cslen = 0; + CK_RV cret; + + cret = reopen_token(&cs); + if (cret == CKR_OK) + cret = find_by_id(cs, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &cp); + if (cret == CKR_OK) + cret = hss_sign(cs, cp, &csig, &cslen); + _exit(cret == CKR_OK ? 0 : 1); + } + if (pid < 0) + return CKR_GENERAL_ERROR; + if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || + WEXITSTATUS(status) != 0) { + fprintf(stderr, "\nchild sign failed (status %d) - FAIL\n", status); + return CKR_GENERAL_ERROR; + } + + /* Parent: the child's signature must have durably advanced the state even + * though it never finalized, and the recovered index is exactly one past + * the parent's - never reused. */ + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &after); + if (ret == CKR_OK && after != before - 1) { + fprintf(stderr, "\ncrash durability: remaining %lu -> %lu, expected a " + "drop of 1 - FAIL\n", (unsigned long)before, (unsigned long)after); + ret = CKR_GENERAL_ERROR; + } + if (ret == CKR_OK) + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "post-crash sign/verify"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- Phase 3: on-disk state-file corruption matrix. ---- */ +#define CORRUPT_TRUNCATE 0 +#define CORRUPT_MAGIC 1 +#define CORRUPT_PARAM 2 +#define CORRUPT_TAG 3 + +static CK_RV corrupt_state_and_check(const char* label, int type) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-corrupt"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + char statePath[512]; + FILE* f; + long fsize = 0, off; + CK_BYTE b = 0; + + printf(" [%s]...\n", label); + ret = fresh_token(&session); + if (ret == CKR_OK) + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = find_state_file(statePath, sizeof(statePath)); + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret != CKR_OK) + return ret; + + if (type == CORRUPT_TRUNCATE) { + f = fopen(statePath, "rb"); + if (f == NULL) + return CKR_GENERAL_ERROR; + fseek(f, 0, SEEK_END); + fsize = ftell(f); + fclose(f); + if (truncate(statePath, fsize > 8 ? fsize - 8 : 0) != 0) + return CKR_GENERAL_ERROR; + } + else { + f = fopen(statePath, "rb+"); + if (f == NULL) + return CKR_GENERAL_ERROR; + fseek(f, 0, SEEK_END); + fsize = ftell(f); + if (type == CORRUPT_MAGIC) + off = 0; /* header magic */ + else if (type == CORRUPT_PARAM) + off = 12; /* scheme-param bytes (AAD-bound) */ + else + off = fsize - 1; /* CORRUPT_TAG: last GCM tag byte */ + fseek(f, off, SEEK_SET); + if (fread(&b, 1, 1, f) == 1) { + b ^= 0xFF; + fseek(f, off, SEEK_SET); + fwrite(&b, 1, 1, f); + } + else { + fclose(f); + return CKR_GENERAL_ERROR; + } + fclose(f); + } + + /* Reload and attempt to use the key: it must be refused - detection may + * surface at login-time reload or at C_Sign - never a produced signature. */ + ret = reopen_token_quiet(&session); + if (ret != CKR_OK) { + printf(" detected at login: %lx\n", (unsigned long)ret); + ret = CKR_OK; + } + else if (find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv) + != CKR_OK) { + printf(" key not usable after corruption\n"); + } + else { + sret = hss_sign(session, priv, &sig, &sigLen); + if (sret == CKR_OK) { + fprintf(stderr, "\n[%s] signed with corrupted state - FAIL\n", + label); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" signing refused: %lx\n", (unsigned long)sret); + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +static CK_RV test_corruption_matrix(void) +{ + CK_RV ret; + + printf(" on-disk state corruption matrix...\n"); + ret = corrupt_state_and_check("truncated", CORRUPT_TRUNCATE); + if (ret == CKR_OK) + ret = corrupt_state_and_check("bad magic", CORRUPT_MAGIC); + if (ret == CKR_OK) + ret = corrupt_state_and_check("bad scheme param", CORRUPT_PARAM); + if (ret == CKR_OK) + ret = corrupt_state_and_check("flipped GCM tag", CORRUPT_TAG); + return ret; +} #endif /* !_WIN32 */ /* ---- Phase 2: parameter matrix ---- */ @@ -1196,6 +1492,12 @@ static CK_RV run_all(void) ret = test_destroy_removes_state(); if (ret == CKR_OK) ret = test_state_tamper_detected(); + if (ret == CKR_OK) + ret = test_write_failure_poison(); + if (ret == CKR_OK) + ret = test_crash_durability(); + if (ret == CKR_OK) + ret = test_corruption_matrix(); #endif return ret; } From a1bebdc0044a803abaa4403460473afc6273e1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 10:03:24 +0200 Subject: [PATCH 10/16] LMS/HSS private keys: make the durable state flush work on Windows The per-sign state-file flush was POSIX-only (fsync). On Windows the durability contract was silently reduced to MoveFileExA's MOVEFILE_WRITE_THROUGH, which makes only the rename durable, not the file data buffered before close. Flush the data explicitly with _commit(_fileno(...)) on Windows/MSVC (fsync stays on POSIX). is already included for the store paths. --- src/internal.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/internal.c b/src/internal.c index 939cbade..abacc8e0 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1880,14 +1880,17 @@ static int wolfPKCS11_Store_CloseAndReport(void* store) if (ctx->file != XBADFILE && ctx->file != NULL) { #ifdef WOLFPKCS11_LMS_PRIVATE - /* Durable mode: flush file data to disk before close so the rename - * in CommitTemp commits a fully-persisted file. The explicit fsync - * is POSIX-only; on Windows durability comes from the atomic rename - * (MoveFileExA with MOVEFILE_WRITE_THROUGH) in CommitTemp, and MSVC - * has no fsync()/fileno(). */ + /* Durable mode: flush the file data to disk before close so the + * rename in CommitTemp commits a fully-persisted file. POSIX uses + * fsync(); Windows/MSVC uses _commit() (FlushFileBuffers under the + * hood) because MoveFileExA's MOVEFILE_WRITE_THROUGH makes only the + * rename durable, not the file data buffered before close. */ if (ctx->durable && ctx->is_write) { (void)XFFLUSH(ctx->file); -#if !defined(_WIN32) && !defined(_MSC_VER) +#if defined(_WIN32) || defined(_MSC_VER) + if (_commit(_fileno(ctx->file)) != 0) + fsyncFailed = 1; +#else fd = fileno(ctx->file); if (fd < 0 || fsync(fd) != 0) fsyncFailed = 1; From 93e88106eee3d7692486c69a77c9c4c2d12b80ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 10:03:24 +0200 Subject: [PATCH 11/16] ci: cover --enable-lms-private under CMake and the sanitizers Add -DWOLFPKCS11_LMS_PRIVATE to the CMake build (the CMake wolfSSL already builds sign-capable LMS), and add an "LMS/HSS private (sign)" entry to the sanitizer matrix with --enable-lms on the sanitizer wolfSSL build, so the keygen/sign/state-file paths run under ASan and UBSan. --- .github/workflows/cmake.yml | 2 +- .github/workflows/sanitizer-tests.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e8c73717..fb433a8d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -65,7 +65,7 @@ jobs: -DWOLFPKCS11_AESECB:BOOL=yes -DWOLFPKCS11_AESCTS:BOOL=yes -DWOLFPKCS11_AESCMAC:BOOL=yes \ -DWOLFPKCS11_PBKDF2:BOOL=yes -DWOLFPKCS11_SHA3:BOOL=yes -DWOLFPKCS11_PKCS11_V3_0:BOOL=yes \ -DWOLFPKCS11_PKCS11_V3_2:BOOL=yes -DWOLFPKCS11_MLDSA:BOOL=yes -DWOLFPKCS11_MLKEM:BOOL=yes \ - -DWOLFPKCS11_LMS:BOOL=yes -DWOLFPKCS11_XMSS:BOOL=yes \ + -DWOLFPKCS11_LMS:BOOL=yes -DWOLFPKCS11_LMS_PRIVATE:BOOL=yes -DWOLFPKCS11_XMSS:BOOL=yes \ -DCMAKE_MODULE_PATH="$GITHUB_WORKSPACE/install/${CMAKE_INSTALL_LIBDIR}" \ .. cmake --build . diff --git a/.github/workflows/sanitizer-tests.yml b/.github/workflows/sanitizer-tests.yml index 3eb3d00f..6aa5354d 100644 --- a/.github/workflows/sanitizer-tests.yml +++ b/.github/workflows/sanitizer-tests.yml @@ -28,6 +28,8 @@ jobs: configure_flags: "--enable-nss --enable-tpm" - name: "PKCS#11 V3.2 PQC Build" configure_flags: "--enable-pkcs11v32 --enable-mldsa --enable-mlkem" + - name: "LMS/HSS private (sign)" + configure_flags: "--enable-lms-private" steps: #pull wolfPKCS11 @@ -76,7 +78,7 @@ jobs: ;; esac ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 --enable-debug \ - --enable-mldsa C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa --enable-lms C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" - name: wolfssl make working-directory: ./wolfssl run: | From b30c04523b8f55977bc9b7b48ee86616c45bd647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 10:05:29 +0200 Subject: [PATCH 12/16] docs: document the LMS/HSS stateful private-key security model Add a "Security model for stateful private keys" section: the AES-GCM AAD-bound state-file format and per-key nonce, what is detected fail-closed (in-place tamper/corruption, non-durable writes), and - importantly - what a software-only stateful scheme cannot protect against (offline state rollback via backup/snapshot/restore, WOLFPKCS11_STATEFUL_RELAX_FSYNC, and multi-process sharing of one token directory), plus a note on constant-time considerations. --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index df76acec..92173dc3 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,48 @@ Because the signature state is durable and per-key, do not clone, snapshot, or restore the token storage directory of a machine that signs with LMS/HSS keys - rolling the state files back to an earlier point would reuse OTS indices. +#### Security model for stateful private keys + +Each HSS private key has a durable, per-key state file separate from the key +object. Its layout is an AES-GCM ciphertext whose Additional Authenticated Data +(AAD) binds the header magic, format version, scheme parameters (levels, LMS +height, LMOTS Winternitz), and the running signature counter. A fresh random +nonce is generated for every write. The private key material is encrypted with +the token key (derived from the user PIN) and the decrypted buffers are cleared +with `wc_ForceZero` on every path. + +What this **detects and refuses** (fail-closed): + +* In-place tampering or corruption of a state file - a flipped byte, a truncated + file, a wrong magic/version, or altered scheme parameters fails the AES-GCM + authentication (or the header check) at reload time, and the key will not + sign. +* A signature-state write that does not become durable (fsync/rename failure) - + the key is poisoned in memory, the in-memory counter is reverted, and no + signature is released; a reload recovers to the last durable index and never + advances past the failed write. + +What this **cannot** protect against (inherent to any software-only stateful +scheme - handle operationally): + +* **Offline state rollback.** Anyone who can copy the token directory and later + restore it - a filesystem backup/restore, a VM or container snapshot, `cp`, + or block-level replication - resets the OTS counter and causes one-time-key + **reuse**, which breaks the signatures irrecoverably. Only an HSM/TPM with a + hardware monotonic counter can prevent this, and that path is out of scope + here. Never restore or duplicate a signing machine's token directory. +* **`WOLFPKCS11_STATEFUL_RELAX_FSYNC=1`.** This disables the durability fsyncs; + a crash or power loss can then leave the on-disk state behind the signatures + already released, reusing OTS keys. Non-production only. +* **Concurrent use of one token directory from multiple processes.** State + durability and mutual exclusion are enforced within a single library instance; + two processes signing against the same on-disk token can reuse indices. Use + one signing process per token directory. + +HSS signing is not secret-input timing-sensitive the way an RSA/ECC private +scalar is (the message and the tree position are not confidential), so no +constant-time treatment beyond the existing token-key handling is required. + ### Build options and defines #### Define WOLFPKCS11_TPM_STORE From b205bd2b0506b0a6882116abd7296cd9d622cedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 10:42:18 +0200 Subject: [PATCH 13/16] ci: add a small-memory WC_LMS variant job for LMS/HSS private keys Add a wolfssl_extra input to build-workflow and an lms_private_small job that builds wolfSSL with --enable-lms=small (WOLFSSL_WC_LMS_SMALL), which sizes the private-data buffers differently and so exercises the state-file engine's dynamic sizing. Pass --disable-crypttests because wolfSSL's own unit test fails to link in the small LMS configuration. Validated locally: the full parameter matrix and the concurrency test pass against a small-LMS wolfSSL. --- .github/workflows/build-workflow.yml | 6 +++++- .github/workflows/unit-test.yml | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index ffa5bb40..535ebf2a 100644 --- a/.github/workflows/build-workflow.yml +++ b/.github/workflows/build-workflow.yml @@ -19,6 +19,10 @@ on: required: false type: string default: 'verify-only' + wolfssl_extra: + required: false + type: string + default: '' jobs: build: @@ -44,7 +48,7 @@ jobs: working-directory: ./wolfssl run: | ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 \ - --enable-mldsa --enable-lms=${{inputs.wolfssl_lms}} --enable-xmss=${{inputs.wolfssl_xmss}} C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa --enable-lms=${{inputs.wolfssl_lms}} --enable-xmss=${{inputs.wolfssl_xmss}} ${{inputs.wolfssl_extra}} C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" - name: wolfssl make install working-directory: ./wolfssl run: make diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b699744a..a173cf84 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -139,6 +139,15 @@ jobs: with: config: --enable-lms-private --disable-shared wolfssl_lms: 'yes' + # Small-memory WC_LMS variant (WOLFSSL_WC_LMS_SMALL): different private-data + # sizing, so it exercises the state-file engine's dynamic sizing. Skip + # wolfSSL's own crypttests, which fail to link in this LMS config. + lms_private_small: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private + wolfssl_lms: 'small' + wolfssl_extra: '--disable-crypttests' debug: uses: ./.github/workflows/build-workflow.yml with: From ddbd35abf04763917fd6b0f0855030443e32ef52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 24 Jul 2026 11:05:41 +0200 Subject: [PATCH 14/16] LMS/HSS: avoid a redundant wc_LmsKey_Free on the keygen failure path WP11_Hss_GenerateKeyPair's failure branch freed priv->data.lmsKey but, unlike the reload path in wp11_Object_Decode_HssKey, did not set priv->encoded = 1. The C_GenerateKeyPair cleanup then calls WP11_Object_Free, whose HSS arm frees the key again when encoded is 0 - a redundant wc_LmsKey_Free (idempotent and benign with the WC_LMS backend used here, but a latent double-free against a backend whose free is not idempotent). Set encoded = 1 to match the reload path. --- src/internal.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/internal.c b/src/internal.c index abacc8e0..61a6ca7a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -6788,6 +6788,10 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, } else { wc_LmsKey_Free(priv->data.lmsKey); + /* Mark encoded so the C_GenerateKeyPair cleanup (WP11_Object_Free) does + * not free the wolfSSL key a second time; mirrors the failure path in + * wp11_Object_Decode_HssKey. */ + priv->encoded = 1; if (stateWritten && priv->statefulStateId != 0) { (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, priv->statefulStateId); From 57d5388f5fe96b68e7575c76615c8dea30bc1a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 28 Jul 2026 16:52:15 +0200 Subject: [PATCH 15/16] Move the hash-based signature code into its own source file internal.c was already ~19k lines and the LMS/HSS private-key work added another 1.5k to it, which made the change hard to review. Nothing could be moved out before, because internal.c defines struct WP11_Object, WP11_Session, WP11_Token and WP11_Slot in the .c file - wolfpkcs11/internal.h is installed and deliberately keeps them opaque for crypto.c, slot.c, wolfpkcs11.c and the test programs. src/wp11_types.h now carries those layouts, the internal macros and WP11_LOCAL prototypes for the internal.c helpers a sibling source file needs: Rng_New, Rng_Free, the wp11_storage_* wrappers, the store durability pair and the state-file open/remove. It is listed in noinst_HEADERS, so it is never installed and the opaque interface still holds outside src/. src/hbs.c and src/hbs.h hold LMS/HSS, XMSS and the scheme-neutral framework for stateful private keys. The durable state file stays in internal.c with the rest of the store code, since it works on WP11_FileStoreCtx and the store helpers; hbs.c owns only what goes into the file, that is the AAD-bound header, the AES-GCM sealing, the signature counter and the per-scheme wrappers. The per-key-type switches in internal.c dispatch through the entry points declared in hbs.h. internal.c drops from 1525 added lines against master to 298. Review fixes folded into the same change: - Gate the durability plumbing and the AES-GCM-with-AAD helpers on WOLFPKCS11_STATEFUL_SIG_PRIVATE instead of WOLFPKCS11_LMS_PRIVATE, so a later XMSS private-key implementation reuses them unchanged. Stop gating WP11_FLAG_STATEFUL_STATE_VALID at all. - Add wolfPKCS11_StoreDirName() and use it for the three copies of directory name extraction in Store_OpenSz, StoreCommitTemp and StateFile_Open. - Parse the HSS templates in C_GenerateKeyPair through four helpers rather than inline in the switch arm. - Replace the local enum reserving 72 bytes of state-file path and the 4 + 4 + 4 + 4 + 4 + 4 shell header length with named constants. - Document the WP11_Hss_SigsLeft cross-check against wc_LmsKey_SigsLeft and keep the pessimistic answer when the counter and wolfSSL disagree. - Have the CKA_HSS_KEYS_REMAINING attribute case and the CKK_HSS unstore case set their default first and override it, instead of relying on a break inside an #ifdef to skip the fallthrough. - Explain why wolfPKCS11_Store_Close keeps its void return: it is the store backend interface in the installed wolfpkcs11/store.h that custom store builds reimplement. - Drop the EXPERIMENTAL framing from the tests and the CI comments. It stays in configure.ac, CMakeLists.txt and the README. - Use ASCII hyphens in the comments this branch added. Fix a CMake bug found while checking the build matrix: the semicolon in the WOLFPKCS11_LMS_PRIVATE help string split the arguments to add_option()'s set(... CACHE BOOL ...) call, leaving the cache variable unset, so cmake -DWOLFPKCS11_LMS_PRIVATE=yes failed to configure. Verified with the autotools matrix (default, --enable-lms, with --enable-xmss, --enable-lms-private, with --enable-xmss), the CMake build, make dist, and make check at 51 pass / 2 skip / 0 fail. hbs_private_test, hss_concurrency_test and pkcs11v3test are clean under ASan and UBSan. --- .github/workflows/unit-test.yml | 2 +- CMakeLists.txt | 3 +- README.md | 9 +- src/crypto.c | 158 +- src/hbs.c | 1962 +++++++++++++++++++++ src/hbs.h | 71 + src/include.am | 7 +- src/internal.c | 2849 +++---------------------------- src/wp11_types.h | 626 +++++++ tests/hbs_private_test.c | 2 +- tests/hss_concurrency_test.c | 2 +- wolfpkcs11/internal.h | 2 - 12 files changed, 2988 insertions(+), 2705 deletions(-) create mode 100644 src/hbs.c create mode 100644 src/hbs.h create mode 100644 src/wp11_types.h diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index a173cf84..8130db33 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -126,7 +126,7 @@ jobs: uses: ./.github/workflows/build-workflow.yml with: config: --enable-lms --enable-xmss --disable-shared - # EXPERIMENTAL LMS/HSS keygen + signing. Needs a sign-capable wolfSSL LMS + # LMS/HSS keygen + signing. Needs a sign-capable wolfSSL LMS # build (full --enable-lms, not verify-only); --enable-lms-private implies # --enable-lms on the wolfPKCS11 side. lms_private: diff --git a/CMakeLists.txt b/CMakeLists.txt index 47cd933f..deffadc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -535,7 +535,7 @@ endif() # LMS/HSS keygen + signing (EXPERIMENTAL, stateful private keys; default: disabled) add_option("WOLFPKCS11_LMS_PRIVATE" - "Enable wolfPKCS11 LMS/HSS keygen and signing (EXPERIMENTAL, stateful private keys; default: disabled)" + "Enable wolfPKCS11 LMS/HSS keygen and signing (EXPERIMENTAL, stateful private keys, default: disabled)" "no" "yes;no" ) @@ -631,6 +631,7 @@ set(LIB_SOURCES src/wolfpkcs11.c src/slot.c src/crypto.c + src/hbs.c ) if(BUILD_SHARED_LIBS) message(STATUS "BUILD_SHARED_LIBS enabled: ${LIB_SOURCES}") diff --git a/README.md b/README.md index 92173dc3..d89d84a5 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,12 @@ scheme - handle operationally): **reuse**, which breaks the signatures irrecoverably. Only an HSM/TPM with a hardware monotonic counter can prevent this, and that path is out of scope here. Never restore or duplicate a signing machine's token directory. -* **`WOLFPKCS11_STATEFUL_RELAX_FSYNC=1`.** This disables the durability fsyncs; - a crash or power loss can then leave the on-disk state behind the signatures - already released, reusing OTS keys. Non-production only. +* **`WOLFPKCS11_STATEFUL_RELAX_FSYNC=1`.** An opt-in environment variable that + trades durability for speed when running tests on throwaway storage, at the + cost of the guarantee above: a crash or power loss can then leave the on-disk + state behind the signatures already released, reusing OTS keys. It is never + set by default; leave it unset in any real deployment. See + [WOLFPKCS11_STATEFUL_RELAX_FSYNC](#wolfpkcs11_stateful_relax_fsync). * **Concurrent use of one token directory from multiple processes.** State durability and mutual exclusion are enforced within a single library instance; two processes signing against the same on-disk token can reuse indices. Use diff --git a/src/crypto.c b/src/crypto.c index 25d9f363..799f3ec4 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -5473,7 +5473,12 @@ CK_RV C_Sign(CK_SESSION_HANDLE hSession, CK_BYTE_PTR pData, ret = WP11_Hss_Sign(pData, (word32)ulDataLen, pSignature, &sigLen, obj); if (ret == NOT_AVAILABLE_E) { - /* Poisoned state; terminate the operation, reload required. */ + /* The key's signature state was poisoned by an earlier + * failure, so its in-memory leaf index can no longer be + * trusted. Every further sign fails this way until the object + * is re-read from the token (C_Logout/C_Login, or restarting + * the application), which restores the state from the durable + * state file. Terminate the operation. */ WP11_Session_SetOpInitialized(session, 0); return CKR_DEVICE_ERROR; } @@ -7824,6 +7829,97 @@ CK_RV C_GenerateKey(CK_SESSION_HANDLE hSession, * type of operation. * CKR_OK on success. */ +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Look up an attribute in the public-key template, falling back to the + * private-key one. The v3.3 HSS profile lets the caller supply the key + * parameters on either arm of C_GenerateKeyPair. */ +static CK_ATTRIBUTE_PTR HssFindGenAttr(CK_ATTRIBUTE_PTR pPublicKeyTemplate, + CK_ULONG ulPublicKeyAttributeCount, + CK_ATTRIBUTE_PTR pPrivateKeyTemplate, + CK_ULONG ulPrivateKeyAttributeCount, + CK_ATTRIBUTE_TYPE type) +{ + CK_ATTRIBUTE_PTR attr = NULL; + + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, type, + &attr); + if (attr == NULL) { + FindAttributeType(pPrivateKeyTemplate, ulPrivateKeyAttributeCount, + type, &attr); + } + return attr; +} + +/* True when the template holds CKA_TOKEN with the value CK_TRUE. */ +static int HssTemplateIsToken(CK_ATTRIBUTE_PTR pTemplate, CK_ULONG ulCount) +{ + CK_ATTRIBUTE_PTR attr = NULL; + + FindAttributeType(pTemplate, ulCount, CKA_TOKEN, &attr); + return attr != NULL && attr->pValue != NULL && + attr->ulValueLen == sizeof(CK_BBOOL) && + *(CK_BBOOL*)attr->pValue == CK_TRUE; +} + +/* Stateful keys must be token-resident: a session-only key that vanishes on a + * crash could release a signature whose one-time index was never persisted. + * Both arms of the key pair must ask for CKA_TOKEN=TRUE explicitly. */ +static CK_RV HssKeyPairGenCheckToken(CK_ATTRIBUTE_PTR pPublicKeyTemplate, + CK_ULONG ulPublicKeyAttributeCount, + CK_ATTRIBUTE_PTR pPrivateKeyTemplate, + CK_ULONG ulPrivateKeyAttributeCount) +{ + if (!HssTemplateIsToken(pPublicKeyTemplate, ulPublicKeyAttributeCount) || + !HssTemplateIsToken(pPrivateKeyTemplate, ulPrivateKeyAttributeCount)) { + return CKR_TEMPLATE_INCONSISTENT; + } + return CKR_OK; +} + +/* Collect the HSS parameter set (CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE and + * CKA_HSS_LMOTS_TYPE) from the key templates. All three are required; each is + * a CK_ULONG-sized value. + * + * @return CKR_TEMPLATE_INCOMPLETE when an attribute is absent. + * @return CKR_ATTRIBUTE_VALUE_INVALID when one has no value or a bad length. + * @return CKR_OK on success. + */ +static CK_RV HssKeyPairGenParams(CK_ATTRIBUTE_PTR pPublicKeyTemplate, + CK_ULONG ulPublicKeyAttributeCount, + CK_ATTRIBUTE_PTR pPrivateKeyTemplate, + CK_ULONG ulPrivateKeyAttributeCount, + CK_ULONG* levels, CK_LMS_TYPE* lmsType, + CK_LMOTS_TYPE* lmotsType) +{ + CK_ATTRIBUTE_PTR aLevels; + CK_ATTRIBUTE_PTR aLms; + CK_ATTRIBUTE_PTR aLmots; + + aLevels = HssFindGenAttr(pPublicKeyTemplate, ulPublicKeyAttributeCount, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_HSS_LEVELS); + aLms = HssFindGenAttr(pPublicKeyTemplate, ulPublicKeyAttributeCount, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_HSS_LMS_TYPE); + aLmots = HssFindGenAttr(pPublicKeyTemplate, ulPublicKeyAttributeCount, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_HSS_LMOTS_TYPE); + + if (aLevels == NULL || aLms == NULL || aLmots == NULL) + return CKR_TEMPLATE_INCOMPLETE; + if (aLevels->pValue == NULL || + aLevels->ulValueLen != sizeof(CK_ULONG) || + aLms->pValue == NULL || + aLms->ulValueLen != sizeof(CK_ULONG) || + aLmots->pValue == NULL || + aLmots->ulValueLen != sizeof(CK_ULONG)) { + return CKR_ATTRIBUTE_VALUE_INVALID; + } + + *levels = *(CK_ULONG*)aLevels->pValue; + *lmsType = *(CK_LMS_TYPE*)aLms->pValue; + *lmotsType = *(CK_LMOTS_TYPE*)aLmots->pValue; + return CKR_OK; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMechanism, CK_ATTRIBUTE_PTR pPublicKeyTemplate, @@ -7865,7 +7961,7 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, return rv; } /* Only require R/W session for token objects. Each template must be - * inspected independently — a public template with CKA_TOKEN=FALSE must + * inspected independently - a public template with CKA_TOKEN=FALSE must * not mask a private template requesting CKA_TOKEN=TRUE. */ if (!WP11_Session_IsRW(session)) { CK_ATTRIBUTE_PTR tpls[2] = { pPublicKeyTemplate, pPrivateKeyTemplate }; @@ -7970,8 +8066,6 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, #endif #ifdef WOLFPKCS11_LMS_PRIVATE case CKM_HSS_KEY_PAIR_GEN: { - CK_ATTRIBUTE_PTR aLevels = NULL, aLms = NULL, aLmots = NULL; - CK_ATTRIBUTE_PTR aTokPub = NULL, aTokPriv = NULL; CK_ULONG hssLevels; CK_LMS_TYPE hssLms; CK_LMOTS_TYPE hssLmots; @@ -7981,51 +8075,17 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, pMechanism->ulParameterLen != 0) { return CKR_MECHANISM_PARAM_INVALID; } - /* Stateful keys must be token-resident: a session-only key that - * vanishes on a crash could release a signature whose one-time - * index was never persisted. Require CKA_TOKEN=TRUE on both arms. */ - FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, - CKA_TOKEN, &aTokPub); - FindAttributeType(pPrivateKeyTemplate, ulPrivateKeyAttributeCount, - CKA_TOKEN, &aTokPriv); - if (aTokPub == NULL || aTokPub->pValue == NULL || - aTokPub->ulValueLen != sizeof(CK_BBOOL) || - *(CK_BBOOL*)aTokPub->pValue != CK_TRUE || - aTokPriv == NULL || aTokPriv->pValue == NULL || - aTokPriv->ulValueLen != sizeof(CK_BBOOL) || - *(CK_BBOOL*)aTokPriv->pValue != CK_TRUE) { - return CKR_TEMPLATE_INCONSISTENT; - } - /* Parameters come from the key template attributes (v3.3 HSS - * profile), accepted from either arm. */ - FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, - CKA_HSS_LEVELS, &aLevels); - if (aLevels == NULL) - FindAttributeType(pPrivateKeyTemplate, - ulPrivateKeyAttributeCount, CKA_HSS_LEVELS, &aLevels); - FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, - CKA_HSS_LMS_TYPE, &aLms); - if (aLms == NULL) - FindAttributeType(pPrivateKeyTemplate, - ulPrivateKeyAttributeCount, CKA_HSS_LMS_TYPE, &aLms); - FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, - CKA_HSS_LMOTS_TYPE, &aLmots); - if (aLmots == NULL) - FindAttributeType(pPrivateKeyTemplate, - ulPrivateKeyAttributeCount, CKA_HSS_LMOTS_TYPE, &aLmots); - if (aLevels == NULL || aLms == NULL || aLmots == NULL) - return CKR_TEMPLATE_INCOMPLETE; - if (aLevels->pValue == NULL || - aLevels->ulValueLen != sizeof(CK_ULONG) || - aLms->pValue == NULL || - aLms->ulValueLen != sizeof(CK_ULONG) || - aLmots->pValue == NULL || - aLmots->ulValueLen != sizeof(CK_ULONG)) { - return CKR_ATTRIBUTE_VALUE_INVALID; - } - hssLevels = *(CK_ULONG*)aLevels->pValue; - hssLms = *(CK_LMS_TYPE*)aLms->pValue; - hssLmots = *(CK_LMOTS_TYPE*)aLmots->pValue; + rv = HssKeyPairGenCheckToken(pPublicKeyTemplate, + ulPublicKeyAttributeCount, pPrivateKeyTemplate, + ulPrivateKeyAttributeCount); + if (rv != CKR_OK) + return rv; + + rv = HssKeyPairGenParams(pPublicKeyTemplate, + ulPublicKeyAttributeCount, pPrivateKeyTemplate, + ulPrivateKeyAttributeCount, &hssLevels, &hssLms, &hssLmots); + if (rv != CKR_OK) + return rv; *phPublicKey = *phPrivateKey = CK_INVALID_HANDLE; rv = NewObject(session, CKK_HSS, CKO_PUBLIC_KEY, diff --git a/src/hbs.c b/src/hbs.c new file mode 100644 index 00000000..ba9444e0 --- /dev/null +++ b/src/hbs.c @@ -0,0 +1,1962 @@ +/* hbs.c + * + * Copyright (C) 2006-2023 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 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. + * + * wolfPKCS11 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 + */ + +/* Hash-based signature schemes: LMS/HSS (RFC 8554) and XMSS/XMSS^MT + * (RFC 8391), plus the scheme-neutral framework for stateful private keys. + * + * The durable state file itself is storage, so it lives with the rest of the + * store code in internal.c (wp11_Stateful_StateFile_Open / _Remove); this file + * owns what goes into it - the AAD-bound header, the AES-GCM sealing, the + * signature counter and the per-scheme thin wrappers. + */ + +#include "wp11_types.h" +#include "hbs.h" + +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* AES-GCM encrypt with caller-supplied AAD. The tag is appended to the + * ciphertext (out[len .. len+AES_BLOCK_SIZE-1]). Used for the state files so + * the file header (magic, version, parameters, sig count) is bound to the + * ciphertext via authenticated additional data; any tampering with those + * bytes is detected at decrypt time. */ +static int wp11_EncryptDataAAD(byte* out, const byte* data, int len, + const byte* key, int keySz, const byte* iv, int ivSz, + const byte* aad, int aadSz, int devId) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, devId); + if (ret == 0) + ret = wc_AesGcmSetKey(&aes, key, keySz); + if (ret == 0) { + ret = wc_AesGcmEncrypt(&aes, out, data, len, iv, ivSz, out + len, + AES_BLOCK_SIZE, aad, aadSz); + } + wc_AesFree(&aes); + + return ret; +} + +static int wp11_DecryptDataAAD(byte* out, const byte* data, int len, + const byte* key, int keySz, const byte* iv, int ivSz, + const byte* aad, int aadSz, int devId) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, devId); + if (ret == 0) + ret = wc_AesGcmSetKey(&aes, key, keySz); + if (ret == 0) { + ret = wc_AesGcmDecrypt(&aes, out, data, len, iv, ivSz, data + len, + AES_BLOCK_SIZE, aad, aadSz); + } + wc_AesFree(&aes); + + return ret; +} + +/* =========================================================================== + * Stateful hash-based signature framework + * =========================================================================== + * + * Shared infrastructure for stateful hash-based signature schemes. The first + * concrete user is LMS/HSS (RFC 8554, this file's WOLFPKCS11_LMS_PRIVATE + * block below). Future schemes (XMSS, XMSS^MT - RFC 8391) plug into the same + * framework via thin wrappers. + * + * Contract every stateful-sig scheme MUST honour: + * + * 1. Token-resident only. Session-only stateful keys are unsafe (a process + * crash between sign and cleanup can release a signature whose OTS + * index was never persisted). Reject CKA_TOKEN=FALSE in C_GenerateKeyPair + * with CKR_TEMPLATE_INCONSISTENT. + * + * 2. State file keyed by a per-key 64-bit nonce, NOT by the object's + * position in the token list. The nonce is generated at keygen time + * and stored in the shell file's last 8 bytes. WP11_Object holds a + * copy in `statefulStateId`. Token-list reordering (Add/Remove, + * Slot_Store) must NEVER move or delete the state file. + * + * 3. State persistence is synchronous and durable. The wolfSSL write + * callback writes the encrypted state file with fsync + atomic rename + * + parent-directory fsync (unless WOLFPKCS11_STATEFUL_RELAX_FSYNC=1) + * BEFORE returning success - so wolfSSL never releases a signature + * whose state advance has not reached stable storage. + * + * 4. State file uses AES-GCM (slot-key) with the AAD-bound header below. + * Tampering with parameters or the persisted sigCount fails decrypt. + * + * 5. SigCount tracked in the wp11 layer (`statefulSigCount`), persisted + * inside the AAD-bound header, and used to compute KEYS_REMAINING - + * no reads from wolfSSL key internals (priv_raw layout is not public + * API and varies between backends). + * + * 6. Poison-on-failure. Any state-write failure or sign error clears + * WP11_FLAG_STATEFUL_STATE_VALID; subsequent signs return + * CKR_DEVICE_ERROR until the object is reloaded from disk + * (wolfSSL Reload restores in-memory state from the durable file). + * + * 7. Locking: all post-keygen state-mutating paths run under the token + * lock (priv->lock points at &token->lock for token-resident keys). + * EXCEPTION: during the keygen function itself, priv is thread-local + * (no other thread has the handle yet, and priv->lock has not been + * assigned by WP11_Session_AddObject), so the write callback fires + * unlocked. This is safe only because no concurrent reader can find + * priv before keygen returns. + * + * On-disk layouts: + * + * Shell file (per scheme; non-secret metadata): + * [u32 magic][u32 version][scheme-specific params + pub][u64 stateId] + * The trailer (stateId) is always the last 8 bytes - that's how + * wp11_Stateful_PeekStateIdFromShell finds it without parsing. + * + * State file (AES-GCM, AAD-bound): + * AAD: [u32 magic][u32 version][scheme params (P bytes)][u64 sigCount] + * [u32 ivLen][iv (12 bytes)] + * [u32 ctLen][ciphertext (privSz + 16-byte GCM tag)] + */ + +/* Big-endian byte helpers for encoding/decoding the AAD-bound state header + * and the shell trailer (compiled in sign builds only - the whole framework + * is gated by WOLFPKCS11_STATEFUL_SIG_PRIVATE above). */ +static word32 wp11_Stateful_ReadU32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; +} + +static word64 wp11_Stateful_ReadU64(const byte* p) +{ + return ((word64)p[0] << 56) | ((word64)p[1] << 48) + | ((word64)p[2] << 40) | ((word64)p[3] << 32) + | ((word64)p[4] << 24) | ((word64)p[5] << 16) + | ((word64)p[6] << 8) | (word64)p[7]; +} + +static void wp11_Stateful_WriteU32(byte* p, word32 v) +{ + p[0] = (byte)(v >> 24); + p[1] = (byte)(v >> 16); + p[2] = (byte)(v >> 8); + p[3] = (byte)v; +} + +static void wp11_Stateful_WriteU64(byte* p, word64 v) +{ + p[0] = (byte)(v >> 56); + p[1] = (byte)(v >> 48); + p[2] = (byte)(v >> 40); + p[3] = (byte)(v >> 32); + p[4] = (byte)(v >> 24); + p[5] = (byte)(v >> 16); + p[6] = (byte)(v >> 8); + p[7] = (byte)v; +} + +/* IV length for the AES-GCM-encrypted state file. The full 96-bit GCM + * standard IV; same for every scheme. */ +#define WP11_STATEFUL_IV_LEN 12 + +/* Cached env-var read once at first use: when 1, the state-write callback + * skips fsync() (file and directory). DANGEROUS: documented as non-production + * only. Default is to always fsync. The variable applies to ALL stateful + * hash-based signature schemes (LMS/HSS today; XMSS in the future) - there + * is no per-scheme knob, since the durability requirement is identical. */ +static int wp11_StatefulRelaxFsync = -1; + +static int wp11_StatefulShouldFsync(void) +{ + if (wp11_StatefulRelaxFsync < 0) { +#ifndef WOLFPKCS11_NO_ENV + const char* v = XGETENV("WOLFPKCS11_STATEFUL_RELAX_FSYNC"); + wp11_StatefulRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; +#else + wp11_StatefulRelaxFsync = 0; +#endif + if (wp11_StatefulRelaxFsync) { + /* Single, prominent stderr line - the README documents this is + * non-production. Operators who reach this path must see it. */ + fprintf(stderr, + "wolfPKCS11: WARNING: WOLFPKCS11_STATEFUL_RELAX_FSYNC=1 is " + "set. Stateful hash-signature state writes will skip fsync; " + "a power loss can roll back the leaf-index advance and cause " + "OTS-key REUSE. Do NOT use this setting in production.\n"); + } + } + return wp11_StatefulRelaxFsync ? 0 : 1; +} + +/* Sanity caps for inputs to the generic state-blob helpers. The wolfSSL + * state buffer for any practical scheme is well below 1 MiB; the scheme + * parameter region is small (HSS = 12 bytes, XMSS = 4-12 bytes). Reject + * absurd inputs up front so the size-arithmetic below cannot overflow. */ +#define WP11_STATEFUL_PRIV_MAX (1U << 20) /* 1 MiB */ +#define WP11_STATEFUL_PARAMS_MAX 256U + +/* Persist the encrypted state file durably. Caller passes the scheme's + * (schemeFilePrefix, magic, version) and a packed scheme-params buffer + * that is bound into the GCM AAD. On a successful return the state file + * at (slot_id, statefulStateId) is fsync'd and renamed atomically. + * + * AAD-bound layout (caught by AES-GCM tag): + * [u32 magic][u32 version][u32 schemeParamLen][schemeParamBytes] + * [u64 sigCount] + * Including schemeParamLen in the AAD diagnoses thin-wrapper version skew + * cleanly (a length mismatch surfaces as a header-mismatch BAD_FUNC_ARG + * before decrypt rather than as opaque AES-GCM tag failure). */ +static int wp11_Stateful_WriteStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* schemeParamBytes, word32 schemeParamLen, + const byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* hdr = NULL; + word32 hdrLen; + byte* ct = NULL; + word32 ctLen; + int tokenId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + /* Stateful keys are token-resident only - reject any attempt to + * persist state for a session-only object. The HSS write CB enforces + * this too, but the framework helper enforces it for all schemes. */ + if (!o->onToken) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (schemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + /* Defensive caps: prevent size-arithmetic overflow. */ + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; + + ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) + return MEMORY_E; + + wp11_Stateful_WriteU32(hdr + 0, magic); + wp11_Stateful_WriteU32(hdr + 4, version); + wp11_Stateful_WriteU32(hdr + 8, schemeParamLen); + if (schemeParamLen > 0) + XMEMCPY(hdr + 12, schemeParamBytes, schemeParamLen); + wp11_Stateful_WriteU64(hdr + 12 + schemeParamLen, o->statefulSigCount); + + /* Fresh GCM nonce per write (NEVER reuse object->iv). */ + ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_STATEFUL_IV_LEN); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return MEMORY_E; + } + + ret = wp11_EncryptDataAAD(ct, priv, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); + if (ret != 0) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag - + * unused for file storage, but kept for parity with TPM_STORE. */ + (void)(hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen); + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 0, &storage); + if (ret == 0) { + if (wp11_StatefulShouldFsync()) + wolfPKCS11_Store_SetDurable(storage, 1); + ret = wp11_storage_write(storage, hdr, (int)hdrLen); + if (ret == 0) { + byte ivLenBuf[4]; + wp11_Stateful_WriteU32(ivLenBuf, WP11_STATEFUL_IV_LEN); + ret = wp11_storage_write(storage, ivLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, iv, WP11_STATEFUL_IV_LEN); + if (ret == 0) { + byte ctLenBuf[4]; + wp11_Stateful_WriteU32(ctLenBuf, ctLen); + ret = wp11_storage_write(storage, ctLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, ct, (int)ctLen); + /* Capture the close-and-commit result. A void close would mask + * a failed rename/fsync; that would let wolfSSL release a signature + * whose state advance never reached durable storage. */ + { + int closeRet = wolfPKCS11_Store_CloseAndReport(storage); + if (ret == 0) + ret = closeRet; + } + } + + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +/* Read and decrypt the state file. The full header is verified as AAD by + * the AES-GCM tag; tampering with magic, version, scheme params, scheme- + * params length, or the persisted sigCount fails decrypt. The expected + * scheme params are passed by the caller (typically: re-pack from the + * in-memory wolfSSL key); a mismatch returns BAD_FUNC_ARG before decrypt. + * The decrypted sigCount is restored into o->statefulSigCount. */ +static int wp11_Stateful_ReadStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* expectedSchemeParamBytes, word32 schemeParamLen, + byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte* hdr = NULL; + word32 hdrLen; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* ct = NULL; + word32 m, v, persistedParamLen, ivLen, ctLen; + word64 sigCount = 0; + int tokenId; + + if (o == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (expectedSchemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; + + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) { + wc_ForceZero(priv, privSz); + return MEMORY_E; + } + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 1, &storage); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_ForceZero(priv, privSz); + return ret; + } + + ret = wp11_storage_read(storage, hdr, (int)hdrLen); + if (ret == 0) { + m = wp11_Stateful_ReadU32(hdr); + v = wp11_Stateful_ReadU32(hdr + 4); + persistedParamLen = wp11_Stateful_ReadU32(hdr + 8); + sigCount = wp11_Stateful_ReadU64(hdr + 12 + schemeParamLen); + if (m != magic || v != version) { + ret = BAD_FUNC_ARG; + } + else if (persistedParamLen != schemeParamLen) { + /* Scheme-params length mismatch - diagnoses thin-wrapper + * version skew before AES-GCM tag failure. */ + ret = BAD_FUNC_ARG; + } + else if (schemeParamLen > 0 && + XMEMCMP(hdr + 12, expectedSchemeParamBytes, schemeParamLen) + != 0) { + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ivLen = wp11_Stateful_ReadU32(buf); + if (ivLen != WP11_STATEFUL_IV_LEN) + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) + ret = wp11_storage_read(storage, iv, WP11_STATEFUL_IV_LEN); + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ctLen = wp11_Stateful_ReadU32(buf); + if (ctLen < AES_BLOCK_SIZE || + ctLen - AES_BLOCK_SIZE != privSz) { + ret = BAD_FUNC_ARG; + } + } + } + if (ret == 0) { + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) + ret = MEMORY_E; + } + if (ret == 0) + ret = wp11_storage_read(storage, ct, (int)ctLen); + if (ret == 0) { + ret = wp11_DecryptDataAAD(priv, ct, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); + /* AES-GCM authentication failure manifests here. */ + } + wp11_storage_close(storage); + if (ct != NULL) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ret == 0) { + /* AES-GCM verified the header; sigCount cannot have been rolled + * back without detection. Restore into the in-memory object. */ + o->statefulSigCount = sigCount; + } + if (ret != 0) + wc_ForceZero(priv, privSz); + return ret; +} + +/* Recover the per-key stateId from the on-disk shell file. The shell + * format is otherwise scheme-specific, but by contract the LAST 8 bytes + * are always the u64 stateId. Used by Unstore to find the state file + * before deleting it, when the in-memory object hasn't been decoded yet. + * + * Validates the shell's magic + version against caller-supplied values + * before returning the trailer. Without this check, a truncated, replaced, + * or wrong-scheme file at the same (tokenId, objId) would yield a garbage + * stateId that the caller then passes to Store_Remove - silently no-op'ing + * or deleting the wrong file while the real state file orphans on disk. + * + * Returns BAD_FUNC_ARG if magic or version don't match (e.g., wrong scheme, + * file truncated below the header size, or the file is something else). */ +static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, + int storeTypeShell, word32 expectedShellMagic, word32 expectedShellVersion, + word64* outStateId) +{ + int ret; + void* storage = NULL; + unsigned char* buf = NULL; + int bufLen = 0; + + if (outStateId == NULL) + return BAD_FUNC_ARG; + *outStateId = 0; + + ret = wp11_storage_open_readonly(storeTypeShell, + (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); + if (ret != 0) + return ret; + ret = wp11_storage_read_alloc_array(storage, &buf, &bufLen); + wp11_storage_close(storage); + if (ret != 0) + return ret; + + /* Need at least [u32 magic][u32 version] + [u64 stateId trailer]. */ + if (bufLen < 16) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + if (wp11_Stateful_ReadU32(buf) != expectedShellMagic || + wp11_Stateful_ReadU32(buf + 4) != expectedShellVersion) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + *outStateId = wp11_Stateful_ReadU64(buf + (bufLen - 8)); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return 0; +} +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE */ + +#ifdef WOLFPKCS11_LMS +/* =========================================================================== + * LMS/HSS (RFC 8554): verification and public-key import. + * + * Verify-only: this build imports raw HSS public keys and verifies + * signatures. There is no private-key/keygen/sign or persisted signature + * state here. The public key is self-describing, so wolfSSL derives the + * parameter set from the key bytes on import; the CKA_HSS_LEVELS / + * CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes are optional and, when + * supplied, are validated against the imported key. + * ========================================================================= */ + +/* Map tree height -> RFC 8554 LMS typecode. Returns 0 on unknown height. */ +static CK_LMS_TYPE wp11_HssHeightToLmsType(int h) +{ + switch (h) { + case 5: return CKL_LMS_SHA256_M32_H5; + case 10: return CKL_LMS_SHA256_M32_H10; + case 15: return CKL_LMS_SHA256_M32_H15; + case 20: return CKL_LMS_SHA256_M32_H20; + case 25: return CKL_LMS_SHA256_M32_H25; + default: return 0; + } +} + +/* Map Winternitz -> LMOTS typecode. Returns 0 on unknown. */ +static CK_LMOTS_TYPE wp11_HssWToLmotsType(int w) +{ + switch (w) { + case 1: return CKL_LMOTS_SHA256_N32_W1; + case 2: return CKL_LMOTS_SHA256_N32_W2; + case 4: return CKL_LMOTS_SHA256_N32_W4; + case 8: return CKL_LMOTS_SHA256_N32_W8; + default: return 0; + } +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Forward parameter maps (RFC 8554 typecode -> height / Winternitz), used by + * keygen to translate the CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes. + * Return 0 on unknown. */ +static int wp11_HssLmsTypeToHeight(CK_LMS_TYPE t) +{ + switch (t) { + case CKL_LMS_SHA256_M32_H5: return 5; + case CKL_LMS_SHA256_M32_H10: return 10; + case CKL_LMS_SHA256_M32_H15: return 15; + case CKL_LMS_SHA256_M32_H20: return 20; + case CKL_LMS_SHA256_M32_H25: return 25; + default: return 0; + } +} + +static int wp11_HssLmotsTypeToW(CK_LMOTS_TYPE t) +{ + switch (t) { + case CKL_LMOTS_SHA256_N32_W1: return 1; + case CKL_LMOTS_SHA256_N32_W2: return 2; + case CKL_LMOTS_SHA256_N32_W4: return 4; + case CKL_LMOTS_SHA256_N32_W8: return 8; + default: return 0; + } +} + +/* On-disk magic + version for the HSS private-key shell file: non-secret + * metadata (parameters + cached public key) with the 8-byte stateId nonce as + * the last 8 bytes. Persisted at keygen so token load can call + * wc_LmsKey_SetParameters before wc_LmsKey_Reload and locate the state file. */ +#define WP11_HSS_SHELL_MAGIC 0x48535350UL /* "HSSP" */ +#define WP11_HSS_SHELL_VERSION 1U +/* Fixed part of the shell, ahead of the public key: magic, version, levels, + * height, winternitz and the public-key length, each a big-endian u32. */ +#define WP11_HSS_SHELL_HDR_LEN (6 * 4) +/* The stateId trailer that wp11_Stateful_PeekStateIdFromShell relies on being + * the last bytes of the shell. */ +#define WP11_HSS_SHELL_TRAILER_LEN 8 + +/* On-disk magic + version for the encrypted HSS state file. The header + * (levels/height/winternitz + persisted signature counter) is bound into the + * AES-GCM tag via AAD, so tampering is detected at decrypt time. */ +#define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ +#define WP11_HSS_STATE_VERSION 1U + +/* HSS scheme param region inside the AAD-bound state header: 3 u32s. */ +#define WP11_HSS_SCHEME_PARAM_LEN 12U + +/* Serialize the private-key shell header: + * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] + * [u32 pubLen][pub]. The 8-byte stateId trailer is appended by the caller. + * Returns bytes written, or BUFFER_E. */ +static int wp11_HssPackShellHeader(byte* out, word32 outLen, int levels, + int height, int winternitz, const byte* pub, word32 pubLen) +{ + word32 needed = WP11_HSS_SHELL_HDR_LEN + pubLen; + word32 idx = 0; + + if (out == NULL || outLen < needed) + return BUFFER_E; + wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_MAGIC); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_VERSION); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)levels); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)height); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)winternitz); idx += 4; + wp11_Stateful_WriteU32(out + idx, pubLen); idx += 4; + if (pub != NULL && pubLen > 0) + XMEMCPY(out + idx, pub, pubLen); + idx += pubLen; + return (int)idx; +} + +/* Parse the private-key shell header. On success *pub points into the input + * buffer (no allocation) and *pubLen is its length. */ +static int wp11_HssParseShellHeader(const byte* in, word32 inLen, + int* levels, int* height, int* winternitz, + const byte** pub, word32* pubLen) +{ + word32 idx = 0; + word32 magic, version, pl; + + if (in == NULL || inLen < WP11_HSS_SHELL_HDR_LEN) + return BAD_FUNC_ARG; + magic = wp11_Stateful_ReadU32(in + idx); idx += 4; + version = wp11_Stateful_ReadU32(in + idx); idx += 4; + if (magic != WP11_HSS_SHELL_MAGIC || version != WP11_HSS_SHELL_VERSION) + return BAD_FUNC_ARG; + *levels = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *height = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *winternitz = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + pl = wp11_Stateful_ReadU32(in + idx); idx += 4; + /* Cap pubLen against the max HSS public-key size so a malformed shell + * cannot force a giant ImportPubRaw. */ + if (pl > HSS_MAX_PUBLIC_KEY_LEN) + return BAD_FUNC_ARG; + if (idx + pl > inLen) + return BAD_FUNC_ARG; + *pub = in + idx; + *pubLen = pl; + return 0; +} + +/* Pack the HSS scheme params (levels|height|winternitz, big-endian u32s) into + * the AAD-bound region of the state header. */ +static void wp11_Hss_PackSchemeParams(byte out[WP11_HSS_SCHEME_PARAM_LEN], + int levels, int height, int winternitz) +{ + wp11_Stateful_WriteU32(out + 0, (word32)levels); + wp11_Stateful_WriteU32(out + 4, (word32)height); + wp11_Stateful_WriteU32(out + 8, (word32)winternitz); +} + +/* HSS state-file write, keyed on disk by (slot_id, statefulStateId). Thin + * wrapper over the generic durable state-blob writer. */ +static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, + word32 privSz) +{ + int levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_WriteStateBlob(o, "hsskey", + (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); +} + +/* HSS state-file read. Validates the in-memory key's params against the + * AAD-bound header before AES-GCM decrypt. */ +static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) +{ + int levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_ReadStateBlob(o, "hsskey", + (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); +} + +/* wolfSSL write callback: invoked by wc_LmsKey_MakeKey and wc_LmsKey_Sign + * after each state advance. Must return WC_LMS_RC_SAVED_TO_NV_MEMORY on + * success; any other value aborts the sign (wolfSSL zeroizes the signature). */ +static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + + if (o == NULL || priv == NULL || privSz == 0) + return WC_LMS_RC_BAD_ARG; + /* Stateful HSS keys are always token-resident in this build. */ + if (!o->onToken || o->statefulStateId == 0) + return WC_LMS_RC_WRITE_FAIL; + if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_WRITE_FAIL; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + + if (o == NULL) + return WC_LMS_RC_BAD_ARG; + if (wp11_Hss_ReadStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_READ_FAIL; + return WC_LMS_RC_READ_TO_MEMORY; +} + +/* Recover the statefulStateId from the on-disk HSS shell (last 8 bytes), + * validating the shell magic+version first. Used by orphan cleanup when the + * object has not been decoded into memory. */ +static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, + word64* outStateId) +{ + return wp11_Stateful_PeekStateIdFromShell(tokenId, objId, + WOLFPKCS11_STORE_HSSKEY_PRIV, (word32)WP11_HSS_SHELL_MAGIC, + WP11_HSS_SHELL_VERSION, outStateId); +} + +/* Delete the signature-state file backing a stored HSS private key, called + * when the object itself is removed from the token. The per-key nonce comes + * from the in-memory object when it has been decoded, otherwise from the + * on-disk shell. Best effort: a state file that cannot be located or removed + * must not stop the object from being destroyed. */ +void wp11_Hss_RemoveStoredState(WP11_Object* object, int tokenId, int objId) +{ + word64 stateId; + + if (object == NULL) + return; + + stateId = object->statefulStateId; + if (stateId == 0) + (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, &stateId); + if (stateId != 0) + (void)wp11_Stateful_StateFile_Remove("hsskey", tokenId, stateId); +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Import a raw HSS public key into the given wolfSSL LmsKey. wolfSSL derives + * the parameter set from the key bytes (RFC 8554 self-describing public key), + * so no parameters need to be set first. On failure the (partially + * initialized) key is NOT freed here; the caller owns it. */ +static int wp11_HssImportPub(LmsKey* key, int devId, const byte* pub, + word32 pubLen) +{ + int ret; + + ret = wc_LmsKey_Init(key, NULL, devId); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(key, pub, pubLen); + return ret; +} + +/* Validate caller-supplied HSS parameter attributes against the parameters + * wolfSSL derived from the just-imported public key. Per the PKCS#11 v3.3 HSS + * profile the public-key parameters are the individual CK_ULONG attributes + * CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE (top-level tree height encoding) and + * CKA_HSS_LMOTS_TYPE (Winternitz encoding). Each is optional; a supplied value + * that does not match the key is rejected with BAD_FUNC_ARG. levelsAttr, + * lmsAttr and lmotsAttr are the raw attribute buffers (NULL when absent) with + * lengths levelsLen/lmsLen/lmotsLen. */ +static int wp11_HssCheckParamAttrs(LmsKey* key, + const byte* levelsAttr, word32 levelsLen, + const byte* lmsAttr, word32 lmsLen, + const byte* lmotsAttr, word32 lmotsLen) +{ + int ret, levels = 0, height = 0, winternitz = 0; + CK_ULONG v; + + ret = wc_LmsKey_GetParameters(key, &levels, &height, &winternitz); + if (ret != 0) + return ret; + if (levelsAttr != NULL) { + if (levelsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, levelsAttr, sizeof(v)); + if (v != (CK_ULONG)levels) + return BAD_FUNC_ARG; + } + if (lmsAttr != NULL) { + if (lmsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, lmsAttr, sizeof(v)); + if (v != (CK_ULONG)wp11_HssHeightToLmsType(height)) + return BAD_FUNC_ARG; + } + if (lmotsAttr != NULL) { + if (lmotsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, lmotsAttr, sizeof(v)); + if (v != (CK_ULONG)wp11_HssWToLmotsType(winternitz)) + return BAD_FUNC_ARG; + } + return 0; +} + +/* Serialize the public key: keyData holds the raw RFC 8554 HSS public key. */ +int wp11_Object_Encode_HssKey(WP11_Object* object) +{ + int ret; + word32 pubLen = 0; + + if (object->objClass == CKO_PUBLIC_KEY) { + /* Public key: raw RFC 8554 public key, unencrypted (Step-1 format). */ + ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); + if (ret == 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, object->keyData, + &pubLen); + if (ret == 0) + object->keyDataLen = (int)pubLen; + } + /* On failure release the buffer so a later store does not skip + * re-encoding and persist an uninitialised buffer. Public key material + * only, so no zeroization needed. */ + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + /* Private key: a non-secret metadata shell (params + cached public + * key + 8-byte stateId trailer). The secret one-time-signature state + * is NEVER written to keyData - it lives only in the AES-GCM state + * file written by the wolfSSL write callback. + * + * The shell is immutable after keygen. Once materialized (built at + * keygen, or loaded from disk), reuse it: a reloaded signing key + * cannot re-export its public root (wolfSSL keeps a key in either + * signing or verify state), so rebuilding here would fail. */ + int levels = 0, height = 0, winternitz = 0; + byte pub[HSS_MAX_PUBLIC_KEY_LEN]; + int hdrLen; + word32 needed; + + if (object->keyData != NULL && object->keyDataLen > 0) + return 0; + + pubLen = sizeof(pub); + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, + &winternitz); + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, pub, &pubLen); + if (ret == 0 && object->statefulStateId == 0) + ret = BAD_FUNC_ARG; + if (ret == 0) { + needed = WP11_HSS_SHELL_HDR_LEN + pubLen + + WP11_HSS_SHELL_TRAILER_LEN; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(needed, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + hdrLen = wp11_HssPackShellHeader(object->keyData, needed, levels, + height, winternitz, pub, pubLen); + if (hdrLen < 0) { + ret = hdrLen; + } + else { + wp11_Stateful_WriteU64(object->keyData + hdrLen, + object->statefulStateId); + object->keyDataLen = hdrLen + 8; + } + } + wc_ForceZero(pub, sizeof(pub)); + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; + } +#endif + return NOT_AVAILABLE_E; +} + +/* Reconstruct the live wolfSSL key from keyData. Public keys import the raw + * key directly; private keys parse the shell, reload the durable signature + * state (via the read callback), and mark the state valid. */ +int wp11_Object_Decode_HssKey(WP11_Object* object) +{ + int ret; + + if (object->objClass == CKO_PUBLIC_KEY) { + /* Raw RFC 8554 public key; wolfSSL re-derives the parameter set. */ + ret = wp11_HssImportPub(object->data.lmsKey, object->devId, + object->keyData, (word32)object->keyDataLen); + object->encoded = (ret != 0) ? 1 : 0; + return ret; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + int levels = 0, height = 0, winternitz = 0; + const byte* pub = NULL; + word32 pubLen = 0; + word32 idx = 0; + + ret = wp11_HssParseShellHeader(object->keyData, + (word32)object->keyDataLen, &levels, &height, &winternitz, + &pub, &pubLen); + if (ret == 0) { + idx = WP11_HSS_SHELL_HDR_LEN + pubLen; /* trailer follows pub */ + if (idx + WP11_HSS_SHELL_TRAILER_LEN > (word32)object->keyDataLen) + ret = BAD_FUNC_ARG; + } + if (ret == 0) { + object->statefulStateId = + wp11_Stateful_ReadU64(object->keyData + idx); + if (object->statefulStateId == 0) + ret = BAD_FUNC_ARG; + } + /* INVALID_DEVID unconditionally (not object->devId): HSS stateful + * signing is software-only by design - HSM/device-backed LMS is out of + * scope. This must hold even when the slot has a real devId (a hardware- + * offload build), so it is pinned here rather than inheriting the slot + * default. A real devId would route to the crypto callback and make + * wc_LmsKey_Reload treat the key as HSM-backed and skip the read + * callback (a no-op), leaving the reloaded key with no private state - + * any later Sign would then reuse leaf index 0 or crash. */ + if (ret == 0) + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); + if (ret == 0) + ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, height, + winternitz); + /* Wire callbacks before Reload: Reload reads the encrypted state via + * the read callback; the write callback is needed for later signing. */ + if (ret == 0) + ret = wc_LmsKey_SetWriteCb(object->data.lmsKey, + wp11_Hss_WriteState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetReadCb(object->data.lmsKey, + wp11_Hss_ReadState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetContext(object->data.lmsKey, object); + if (ret == 0) + ret = wc_LmsKey_Reload(object->data.lmsKey); + /* Do NOT ImportPubRaw here: wolfSSL keeps an LmsKey in either signing + * (reloaded) or verify (pub) state, not both, and rejects a pub import + * after Reload with BAD_STATE_E. The public root is not needed for + * signing, and the cached shell (kept in keyData) preserves it for + * re-storage. */ + (void)pub; + if (ret == 0) { + object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + object->encoded = 0; + } + else { + /* Clear the validity flag so a later Sign cannot use a half-built + * key, and free the wolfSSL key. encoded=1 stops Object_Free from + * freeing the wolfSSL key a second time. */ + object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + wc_LmsKey_Free(object->data.lmsKey); + object->encoded = 1; + } + return ret; + } +#endif + return NOT_AVAILABLE_E; +} + +int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; +#endif + ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); + if (ret == 0) { + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret = 0; + void* storage = NULL; + int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; +#endif + if (object->keyData == NULL) + ret = wp11_Object_Encode_HssKey(object); + if (ret == 0) + ret = wp11_storage_open(storeType, tokenId, objId, + object->keyDataLen, &storage); + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +/* Import an HSS public key from a C_CreateObject template. Following the + * PKCS#11 v3.3 HSS profile the collected attributes are data[0]=CKA_HSS_LEVELS, + * data[1]=CKA_HSS_LMS_TYPE, data[2]=CKA_HSS_LMOTS_TYPE (all optional CK_ULONG) + * and data[3]=CKA_VALUE, the raw RFC 8554 public key. wolfSSL derives the + * parameter set from the key bytes; any supplied CKA_HSS_* attribute is + * validated against the derived parameters and a mismatch is rejected. + * Private-key import is rejected: caller-supplied stateful private material is + * a forgery vector and out of scope for verify-only support. */ +int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) +{ + int ret = 0; + LmsKey* newKey = NULL; + + if (object == NULL || data == NULL || len == NULL) + return BAD_FUNC_ARG; +#ifdef WOLFPKCS11_LMS_PRIVATE + /* Keygen (C_GenerateKeyPair) creates both objects with no CKA_VALUE and + * populates them in WP11_Hss_GenerateKeyPair, so a missing value is a + * deferred keygen object, not an error. A private key must never be + * imported WITH a value: caller-supplied one-time-signature state is a + * forgery vector. */ + if (object->objClass == CKO_PRIVATE_KEY) { + if (data[3] != NULL) + return BAD_FUNC_ARG; + return 0; + } + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + if (data[3] == NULL) + return 0; +#else + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + /* A public key must carry its value. There is no keygen path in a + * verify-only build that would populate it later, so an absent + * CKA_VALUE is an incomplete template rather than a deferred import; + * reject it instead of creating an unusable object. */ + if (data[3] == NULL) + return BAD_FUNC_ARG; +#endif + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + /* Import into a temporary key and swap it in only after it fully validates, + * so a failed re-import (C_SetAttributeValue with a new CKA_VALUE) leaves + * the existing key unchanged as PKCS#11 requires. */ + newKey = (LmsKey*)XMALLOC(sizeof(LmsKey), NULL, DYNAMIC_TYPE_KEY); + if (newKey == NULL) + ret = MEMORY_E; + if (ret == 0) { + XMEMSET(newKey, 0, sizeof(LmsKey)); + ret = wp11_HssImportPub(newKey, object->devId, data[3], + (word32)len[3]); + } + if (ret == 0) { + ret = wp11_HssCheckParamAttrs(newKey, data[0], (word32)len[0], + data[1], (word32)len[1], data[2], (word32)len[2]); + } + if (ret == 0) { + /* Replace the live key and drop the cached serialization so a later + * store re-encodes from the new key instead of persisting stale bytes. */ + if (object->data.lmsKey != NULL) { + if (!object->encoded) + wc_LmsKey_Free(object->data.lmsKey); + XFREE(object->data.lmsKey, NULL, DYNAMIC_TYPE_KEY); + } + object->data.lmsKey = newKey; + object->encoded = 0; + newKey = NULL; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + if (newKey != NULL) { + wc_LmsKey_Free(newKey); + XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); + } + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + return ret; +} + +/* Verify an HSS signature over a raw message. Stateless on the verifier. */ +int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, + word32 dataLen, int* stat, WP11_Object* pub) +{ + int ret; + word32 expSigLen = 0; + + if (pub == NULL || pub->data.lmsKey == NULL || stat == NULL) + return BAD_FUNC_ARG; + + /* Verification only reads the key: wc_LmsKey_Verify works from a locally + * allocated LmsState, so a shared RO lock is safe for concurrent verifies + * on the same token object. */ + if (pub->onToken) + WP11_Lock_LockRO(pub->lock); + + /* A signature whose length does not match the key's parameter set cannot + * be valid; report it as an invalid signature (stat = 0 -> + * CKR_SIGNATURE_INVALID) rather than letting wolfSSL's length check + * surface as a function failure. */ + if (wc_LmsKey_GetSigLen(pub->data.lmsKey, &expSigLen) == 0 && + sigLen != expSigLen) { + *stat = 0; + ret = 0; + } + else { + /* Unlike XMSS, whose wolfSSL verify rejects a zero-length message with + * BAD_FUNC_ARG, wc_LmsKey_Verify accepts an empty message, so HSS needs + * no dataLen == 0 special-case here (see WP11_Xmss_Verify). */ + ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, + (int)dataLen); + /* Only a genuine verification mismatch (SIG_VERIFY_E) maps to stat=0. + * Remaining argument/state errors surface as a function failure so + * they are not silently reported as an invalid signature. */ + if (ret == 0) { + *stat = 1; + } + else if (ret == SIG_VERIFY_E) { + *stat = 0; + ret = 0; + } + else { + *stat = 0; + } + } + + if (pub->onToken) + WP11_Lock_UnlockRO(pub->lock); + + return ret; +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Generate an HSS key pair. levels/lmsType/lmotsType come from the v3.3 + * keygen template attributes; wolfSSL requires uniform parameters across + * levels. The private key's one-time-signature state is durably persisted at + * genesis (via the write callback invoked inside wc_LmsKey_MakeKey) before + * this returns success. */ +int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + CK_ULONG levels, CK_LMS_TYPE lmsType, + CK_LMOTS_TYPE lmotsType, WP11_Slot* slot) +{ + int ret; + int nLevels, height, winternitz; + WC_RNG rng; + byte pubBuf[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubLen = sizeof(pubBuf); + int stateWritten = 0; + + if (pub == NULL || priv == NULL || slot == NULL || + pub->data.lmsKey == NULL || priv->data.lmsKey == NULL) { + return BAD_FUNC_ARG; + } + + /* Translate the v3.3 typecodes to wolfSSL (levels, height, winternitz), + * enforcing the wolfSSL level limit. */ + nLevels = (int)levels; + height = wp11_HssLmsTypeToHeight(lmsType); + winternitz = wp11_HssLmotsTypeToW(lmotsType); + if (nLevels < 1 || nLevels > LMS_MAX_LEVELS || height == 0 || + winternitz == 0) { + return BAD_FUNC_ARG; + } + + /* Generate the per-key 64-bit nonce keying the state file. Must be + * non-zero (0 is the "none" sentinel); retry on the vanishing chance of + * zero, else treat as an RNG failure. */ + { + byte nonceBuf[8]; + int tries; + for (tries = 0; tries < 4; tries++) { + ret = WP11_Slot_GenerateRandom(slot, nonceBuf, sizeof(nonceBuf)); + if (ret != 0) + return ret; + priv->statefulStateId = wp11_Stateful_ReadU64(nonceBuf); + if (priv->statefulStateId != 0) + break; + } + if (priv->statefulStateId == 0) + return RNG_FAILURE_E; + } + + /* Mark on-token so the write CB commits; AddObject formalizes the + * registration shortly. priv->lock is assigned later by Session_AddObject; + * during keygen no other thread holds a handle, so the CB fires unlocked. */ + priv->onToken = 1; + priv->statefulSigCount = 0; + + /* INVALID_DEVID unconditionally (not priv->devId): HSS stateful signing is + * software-only by design - HSM/device-backed LMS is out of scope - so this + * is pinned even when the slot has a real devId (a hardware-offload build), + * mirroring the reload path in wp11_Object_Decode_HssKey. A real devId would + * route MakeKey/Sign through the crypto callback and, critically, make a + * later wc_LmsKey_Reload a no-op, so the persisted state could never be + * restored. */ + ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, INVALID_DEVID); + if (ret == 0) + ret = wc_LmsKey_SetParameters(priv->data.lmsKey, nLevels, height, + winternitz); + /* Wire callbacks BEFORE MakeKey so genesis state is persisted durably. */ + if (ret == 0) + ret = wc_LmsKey_SetWriteCb(priv->data.lmsKey, wp11_Hss_WriteState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetReadCb(priv->data.lmsKey, wp11_Hss_ReadState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetContext(priv->data.lmsKey, priv); + + if (ret == 0) + ret = Rng_New(&slot->token.rng, &slot->token.rngLock, &rng); + if (ret == 0) { + /* MakeKey invokes the write CB to persist genesis state; on success + * the genesis state file is durable on disk. */ + ret = wc_LmsKey_MakeKey(priv->data.lmsKey, &rng); + Rng_Free(&rng); + if (ret == 0) + stateWritten = 1; + } + + /* Mirror parameters + public key into the public-key object. */ + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(priv->data.lmsKey, pubBuf, &pubLen); + if (ret == 0) + ret = wc_LmsKey_Init(pub->data.lmsKey, NULL, pub->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(pub->data.lmsKey, nLevels, height, + winternitz); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(pub->data.lmsKey, pubBuf, pubLen); + + if (ret == 0) { + priv->local = pub->local = 1; + priv->keyGenMech = pub->keyGenMech = CKM_HSS_KEY_PAIR_GEN; + priv->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + } + else { + wc_LmsKey_Free(priv->data.lmsKey); + /* Mark encoded so the C_GenerateKeyPair cleanup (WP11_Object_Free) does + * not free the wolfSSL key a second time; mirrors the failure path in + * wp11_Object_Decode_HssKey. */ + priv->encoded = 1; + if (stateWritten && priv->statefulStateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, + priv->statefulStateId); + } + priv->statefulStateId = 0; + } + + wc_ForceZero(pubBuf, sizeof(pubBuf)); + return ret; +} + +/* Remove the durable signature-state file backing an HSS private key that was + * generated but never committed to the token - e.g. a later C_GenerateKeyPair + * step (AddObject/SetInitialStates/store) failed after keygen already wrote the + * genesis state to disk. Without this the encrypted state file is orphaned. + * No-op unless priv is a CKK_HSS private key with a live state id; safe to call + * on any object and more than once. */ +void WP11_Hss_RemoveOrphanState(WP11_Object* priv, WP11_Slot* slot) +{ + if (priv != NULL && slot != NULL && priv->type == CKK_HSS && + priv->objClass == CKO_PRIVATE_KEY && priv->statefulStateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, + priv->statefulStateId); + priv->statefulStateId = 0; + } +} + +/* Sign a raw message with an HSS private key. State is advanced and persisted + * (via the write CB inside wc_LmsKey_Sign) BEFORE the signature is returned. + * On any failure the in-memory state is poisoned (STATE_VALID cleared) to + * force a reload from durable storage before the next attempt. Token-resident + * keys always have priv->lock == &token->lock. */ +int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, + word32* sigLen, WP11_Object* priv) +{ + int ret = 0; + word32 outLen = 0; + + if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || + sigLen == NULL || priv->lock == NULL) { + return BAD_FUNC_ARG; + } + + WP11_Lock_LockRW(priv->lock); + + if ((priv->opFlag & WP11_FLAG_STATEFUL_STATE_VALID) == 0) { + ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ + } + if (ret == 0) { + word64 prevCount = priv->statefulSigCount; + priv->statefulSigCount = prevCount + 1; + outLen = *sigLen; + ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, + (int)dataLen); + if (ret == 0) { + *sigLen = outLen; + } + else { + /* Write CB failed (durable state may lag the in-memory advance) + * or the key is exhausted. Poison until reloaded; zero any bytes + * wolfSSL wrote and revert the in-memory counter. */ + priv->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + if (outLen > 0) + XMEMSET(sig, 0, outLen); + priv->statefulSigCount = prevCount; + } + } + + WP11_Lock_UnlockRW(priv->lock); + return ret; +} + +/* Signature length for an HSS private key (parameter-set dependent). Returns + * 0 on error. */ +word32 WP11_Hss_SigLen(WP11_Object* key) +{ + word32 sigLen = 0; + + if (key == NULL || key->data.lmsKey == NULL) + return 0; + if (wc_LmsKey_GetSigLen(key->data.lmsKey, &sigLen) != 0) + return 0; + return sigLen; +} + +/* Remaining one-time keys for an HSS private key. The count comes from the + * wp11-layer statefulSigCount (persisted in the GCM-authenticated state + * header) rather than from wolfSSL LmsKey internals, whose layout is not + * public API: total = 2^(levels*height), saturating at UINT32_MAX for large + * parameter sets. + * + * wolfSSL's own view is used as a cross-check. wc_LmsKey_SigsLeft only reports + * whether any key is left, so the two can be compared but not reconciled; on + * disagreement the answer is always the pessimistic one, since over-reporting + * would invite a caller to attempt a signature that must not happen. */ +int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) +{ + int levels = 0, height = 0, winternitz = 0; + int totalH; + word64 used, total; + + if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) + return BAD_FUNC_ARG; + + *remaining = 0; + + /* wolfSSL says the key is spent: report 0 whatever the counter holds. */ + if (wc_LmsKey_SigsLeft(key->data.lmsKey) == 0) + return 0; + + if (wc_LmsKey_GetParameters(key->data.lmsKey, &levels, &height, + &winternitz) != 0) { + return BAD_FUNC_ARG; + } + totalH = levels * height; + if (totalH <= 0) + return BAD_FUNC_ARG; + if (totalH >= 64) { + *remaining = 0xFFFFFFFFU; + return 0; + } + total = ((word64)1) << totalH; + used = key->statefulSigCount; + /* The counter says spent while wolfSSL still reports keys left. The two + * views of the state have drifted, so keep the pessimistic 0. */ + if (used >= total) + return 0; + + if ((total - used) > 0xFFFFFFFFULL) + *remaining = 0xFFFFFFFFU; + else + *remaining = (word32)(total - used); + return 0; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Serve CKA_HSS_KEYS_REMAINING for a private key. Follows the usual attribute + * convention: data == NULL asks for the size only, a short buffer returns + * BUFFER_E with the required size in len. */ +static int wp11_HssGetKeysRemaining(WP11_Object* object, byte* data, + CK_ULONG* len) +{ + word32 remaining = 0; + CK_ULONG v; + + if (WP11_Hss_SigsLeft(object, &remaining) != 0) + return NOT_AVAILABLE_E; + + v = (CK_ULONG)remaining; + if (data == NULL) { + *len = sizeof(v); + return 0; + } + if (*len < sizeof(v)) { + *len = sizeof(v); + return BUFFER_E; + } + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + return 0; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Serve HSS-specific attributes from a public-key object. */ +int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len) +{ + int ret = 0; + int levels = 0, height = 0, winternitz = 0; + + if (object == NULL || object->data.lmsKey == NULL || len == NULL) + return BAD_FUNC_ARG; + + switch (type) { + case CKA_HSS_LEVELS: { + CK_ULONG v; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + v = (CK_ULONG)levels; + if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + } + break; + } + case CKA_HSS_LMS_TYPE: { + CK_LMS_TYPE v; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + v = wp11_HssHeightToLmsType(height); + if (v == 0) + ret = NOT_AVAILABLE_E; + else if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + } + break; + } + case CKA_HSS_LMOTS_TYPE: { + CK_LMOTS_TYPE v; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + v = wp11_HssWToLmotsType(winternitz); + if (v == 0) + ret = NOT_AVAILABLE_E; + else if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + } + break; + } + case CKA_HSS_LMS_TYPES: + case CKA_HSS_LMOTS_TYPES: { + /* wolfSSL uses uniform LMS/LMOTS parameters across all HSS levels, so + * every array entry is the top-level type. Per-level types (allowed + * by the PKCS#11 v3.3 profile) are not expressible by wolfSSL and are + * not supported here. */ + CK_ULONG total; + CK_ULONG fill; + int i; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + total = (CK_ULONG)(sizeof(CK_ULONG) * levels); + fill = (type == CKA_HSS_LMS_TYPES) + ? wp11_HssHeightToLmsType(height) + : wp11_HssWToLmotsType(winternitz); + if (fill == 0) + ret = NOT_AVAILABLE_E; + else if (data == NULL) + *len = total; + else if (*len < total) { + *len = total; + ret = BUFFER_E; + } + else { + for (i = 0; i < levels; i++) + XMEMCPY(data + i * sizeof(CK_ULONG), &fill, + sizeof(CK_ULONG)); + *len = total; + } + } + break; + } + case CKA_HSS_KEYS_REMAINING: + /* Remaining-signature count is a private-key/state property. In a + * verify-only build (or for a public key) it is unavailable. */ + ret = NOT_AVAILABLE_E; +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + ret = wp11_HssGetKeysRemaining(object, data, len); +#endif + break; + case CKA_VALUE: + if (object->objClass == CKO_PUBLIC_KEY) { + word32 pubLen = 0; + ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); + if (ret == 0) { + if (data == NULL) + *len = pubLen; + else if (*len < pubLen) { + *len = pubLen; + ret = BUFFER_E; + } + else { + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, + data, &pubLen); + if (ret == 0) + *len = pubLen; + } + } + } + else { + *len = CK_UNAVAILABLE_INFORMATION; + ret = NOT_AVAILABLE_E; + } + break; + default: + ret = NOT_AVAILABLE_E; + break; + } + return ret; +} + +/* Validate a supplied CKA_HSS_LMS_TYPES / CKA_HSS_LMOTS_TYPES array against the + * imported public key. wolfSSL uses uniform LMS/LMOTS parameters across all HSS + * levels, so a conforming array holds one CK_ULONG per level, each equal to the + * single top-level type served by HssObject_GetAttr. A wrong length or any + * differing entry is rejected so a malformed or mismatched template is not + * silently accepted; a matching array (as read back from this key) is accepted. + * Returns 0 when the array matches, BAD_FUNC_ARG otherwise. The caller holds the + * object lock. */ +int wp11_HssCheckTypesAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + const byte* data, CK_ULONG len) +{ + int ret; + int levels = 0; + int height = 0; + int winternitz = 0; + int i; + CK_ULONG expected; + CK_ULONG entry; + + if (object->data.lmsKey == NULL || data == NULL) + return BAD_FUNC_ARG; + + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + + expected = (type == CKA_HSS_LMS_TYPES) + ? (CK_ULONG)wp11_HssHeightToLmsType(height) + : (CK_ULONG)wp11_HssWToLmotsType(winternitz); + if (expected == 0) + return BAD_FUNC_ARG; + if (len != (CK_ULONG)(sizeof(CK_ULONG) * levels)) + return BAD_FUNC_ARG; + + for (i = 0; i < levels; i++) { + XMEMCPY(&entry, data + i * sizeof(CK_ULONG), sizeof(entry)); + if (entry != expected) + return BAD_FUNC_ARG; + } + + return 0; +} +#endif /* WOLFPKCS11_LMS */ + +#ifdef WOLFPKCS11_XMSS +/* =========================================================================== + * XMSS/XMSS^MT (RFC 8391): verification and public-key import. + * + * Verify-only. One wolfSSL XmssKey type serves both XMSS and XMSS^MT; the + * PKCS#11 key type (CKK_XMSS vs CKK_XMSSMT) selects which via the is_xmssmt + * flag. The raw public key carries the parameter-set OID in its leading + * bytes, so wc_XmssKey_ImportPubRaw_ex derives the parameter set on import. + * A CKA_PARAMETER_SET attribute is optional and, when supplied, is validated + * against the OID in the key. + * ========================================================================= */ + +/* Length of the parameter-set OID prefix in a raw RFC 8391 public key. wolfSSL + * only exposes XMSS_OID_LEN when built with WOLFSSL_HAVE_XMSS and it is absent + * from some released headers, so define our own to keep the build independent + * of the wolfSSL version. */ +#define WP11_XMSS_OID_LEN 4 + +/* Big-endian 32-bit read from a byte buffer (used to read the XMSS OID). */ +static word32 wp11_HbsReadU32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; +} + +/* Import a raw XMSS/XMSS^MT public key into the given wolfSSL XmssKey, deriving + * the parameter set from the embedded OID. is_xmssmt disambiguates the XMSS and + * XMSS^MT OID namespaces (which overlap) and comes from the PKCS#11 key type. + * When the caller supplies a CKA_PARAMETER_SET (params != NULL, the OID as a + * CK_ULONG) it must match the OID in the public key. On failure the key is NOT + * freed here; the caller owns it. */ +static int wp11_XmssImportPub(XmssKey* key, int is_xmssmt, int devId, + const byte* pub, word32 pubLen, const byte* params, word32 paramsLen) +{ + int ret; + + ret = wc_XmssKey_Init(key, NULL, devId); + if (ret == 0 && params != NULL) { + CK_ULONG userOid; + if (paramsLen != sizeof(CK_ULONG) || pubLen < WP11_XMSS_OID_LEN) { + ret = BAD_FUNC_ARG; + } + else { + XMEMCPY(&userOid, params, sizeof(userOid)); + if (userOid != (CK_ULONG)wp11_HbsReadU32(pub)) + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) + ret = wc_XmssKey_ImportPubRaw_ex(key, pub, pubLen, is_xmssmt); + return ret; +} + +/* Serialize the public key: keyData holds the raw RFC 8391 public key. */ +int wp11_Object_Encode_XmssKey(WP11_Object* object) +{ + int ret; + word32 pubLen = 0; + + if (object->objClass != CKO_PUBLIC_KEY) + return NOT_AVAILABLE_E; + + ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); + if (ret == 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, object->keyData, + &pubLen); + if (ret == 0) + object->keyDataLen = (int)pubLen; + } + /* On failure release the buffer so a later store does not skip re-encoding + * (keyData != NULL) and persist an uninitialised buffer with a stale + * length. This is public key material only, so no zeroization is needed. */ + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; +} + +int wp11_Object_Decode_XmssKey(WP11_Object* object) +{ + int ret; + + if (object->objClass != CKO_PUBLIC_KEY) + return NOT_AVAILABLE_E; + + /* Reload from the stored raw public key; wolfSSL re-derives the parameter + * set from the embedded OID (no separate parameter set is persisted). */ + ret = wp11_XmssImportPub(object->data.xmssKey, + (object->type == CKK_XMSSMT) ? 1 : 0, object->devId, object->keyData, + (word32)object->keyDataLen, NULL, 0); + /* Object owns the key on all paths; encoded stays 0 for the single free in + * WP11_Object_Free. See wp11_Object_Decode_HssKey. */ + object->encoded = 0; + return ret; +} + +int wp11_Object_Load_XmssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + + ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, + objId, &storage); + if (ret == 0) { + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +int wp11_Object_Store_XmssKey(WP11_Object* object, int tokenId, + int objId) +{ + int ret = 0; + void* storage = NULL; + + if (object->keyData == NULL) + ret = wp11_Object_Encode_XmssKey(object); + if (ret == 0) + ret = wp11_storage_open(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, objId, + object->keyDataLen, &storage); + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +/* Import an XMSS/XMSS^MT public key from a C_CreateObject template. data[1] is + * the raw RFC 8391 public key; data[0] (CKA_PARAMETER_SET, the OID as a + * CK_ULONG) is optional and, when present, is validated against the key. + * Private-key import is rejected. */ +int WP11_Object_SetXmssKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) +{ + int ret = 0; + XmssKey* newKey = NULL; + + if (object == NULL || data == NULL || len == NULL) + return BAD_FUNC_ARG; + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + /* A public key must carry its value; an absent CKA_VALUE is an + * incomplete template (no keygen path fills it later). Reject it. */ + if (data[1] == NULL) + return BAD_FUNC_ARG; + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + /* Import into a temporary key and swap it in only after it validates, so a + * failed re-import leaves the existing key unchanged (PKCS#11 requires a + * failed C_SetAttributeValue to be a no-op). */ + newKey = (XmssKey*)XMALLOC(sizeof(XmssKey), NULL, DYNAMIC_TYPE_KEY); + if (newKey == NULL) + ret = MEMORY_E; + if (ret == 0) { + XMEMSET(newKey, 0, sizeof(XmssKey)); + ret = wp11_XmssImportPub(newKey, (object->type == CKK_XMSSMT) ? 1 : 0, + object->devId, data[1], (word32)len[1], data[0], (word32)len[0]); + } + if (ret == 0) { + /* Replace the live key and drop the cached serialization so a later + * store re-encodes from the new key. */ + if (object->data.xmssKey != NULL) { + if (!object->encoded) + wc_XmssKey_Free(object->data.xmssKey); + XFREE(object->data.xmssKey, NULL, DYNAMIC_TYPE_KEY); + } + object->data.xmssKey = newKey; + object->encoded = 0; + newKey = NULL; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + if (newKey != NULL) { + wc_XmssKey_Free(newKey); + XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); + } + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + return ret; +} + +/* Verify an XMSS/XMSS^MT signature over a raw message. Stateless. */ +int WP11_Xmss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, + word32 dataLen, int* stat, WP11_Object* pub) +{ + int ret; + word32 expSigLen = 0; + + if (pub == NULL || pub->data.xmssKey == NULL || stat == NULL) + return BAD_FUNC_ARG; + + /* Verification only reads the key: wc_XmssKey_Verify works from a locally + * allocated XmssState, so a shared RO lock is safe for concurrent verifies + * on the same token object. */ + if (pub->onToken) + WP11_Lock_LockRO(pub->lock); + + /* A wrong-length signature cannot be valid for this key; report it as an + * invalid signature rather than a function failure. wolfSSL's XMSS verify + * also rejects a zero-length message with BAD_FUNC_ARG; treat that the same + * way (an invalid verification, not an internal failure). */ + if (wc_XmssKey_GetSigLen(pub->data.xmssKey, &expSigLen) == 0 && + sigLen != expSigLen) { + *stat = 0; + ret = 0; + } + else if (dataLen == 0) { + *stat = 0; + ret = 0; + } + else { + ret = wc_XmssKey_Verify(pub->data.xmssKey, sig, sigLen, data, + (int)dataLen); + /* Only SIG_VERIFY_E maps to stat=0; other argument/state errors + * surface as a function failure so they are not silently reported as + * an invalid signature. */ + if (ret == 0) { + *stat = 1; + } + else if (ret == SIG_VERIFY_E) { + *stat = 0; + ret = 0; + } + else { + *stat = 0; + } + } + + if (pub->onToken) + WP11_Lock_UnlockRO(pub->lock); + + return ret; +} + +/* Serve XMSS-specific attributes from a public-key object. */ +int XmssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len) +{ + int ret = 0; + + if (object == NULL || object->data.xmssKey == NULL || len == NULL) + return BAD_FUNC_ARG; + + switch (type) { + case CKA_VALUE: + if (object->objClass == CKO_PUBLIC_KEY) { + word32 pubLen = 0; + ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); + if (ret == 0) { + if (data == NULL) + *len = pubLen; + else if (*len < pubLen) { + *len = pubLen; + ret = BUFFER_E; + } + else { + ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, + data, &pubLen); + if (ret == 0) + *len = pubLen; + } + } + } + else { + *len = CK_UNAVAILABLE_INFORMATION; + ret = NOT_AVAILABLE_E; + } + break; + case CKA_PARAMETER_SET: { + /* The XMSS parameter set is identified by the OID in the leading + * bytes of the raw public key; return it as a CK_ULONG. */ + CK_ULONG v; + word32 pubLen = 0; + byte* raw; + + if (object->objClass != CKO_PUBLIC_KEY) { + *len = CK_UNAVAILABLE_INFORMATION; + ret = NOT_AVAILABLE_E; + break; + } + if (data == NULL) { + *len = sizeof(v); + break; + } + if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + break; + } + ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); + if (ret == 0 && pubLen < WP11_XMSS_OID_LEN) + ret = NOT_AVAILABLE_E; + if (ret == 0) { + raw = (byte*)XMALLOC(pubLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (raw == NULL) { + ret = MEMORY_E; + } + else { + ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, raw, + &pubLen); + if (ret == 0) { + v = (CK_ULONG)wp11_HbsReadU32(raw); + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + XFREE(raw, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } + break; + } + default: + ret = NOT_AVAILABLE_E; + break; + } + return ret; +} +#endif /* WOLFPKCS11_XMSS */ diff --git a/src/hbs.h b/src/hbs.h new file mode 100644 index 00000000..3cf8cd67 --- /dev/null +++ b/src/hbs.h @@ -0,0 +1,71 @@ +/* hbs.h + * + * Copyright (C) 2006-2023 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 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. + * + * wolfPKCS11 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 + */ + +/* Hash-based signature schemes (hbs.c), private to src/. + * + * The object encode/decode/load/store entry points below are dispatched from + * the per-key-type switches in internal.c. Everything the rest of the library + * calls - WP11_Hss_Verify, WP11_Hss_Sign, WP11_Object_SetHssKey and friends - + * is declared in wolfpkcs11/internal.h instead. + */ + +#ifndef WOLFPKCS11_HBS_H +#define WOLFPKCS11_HBS_H + +#include "wp11_types.h" + +#ifdef WOLFPKCS11_LMS + +WP11_LOCAL int wp11_Object_Encode_HssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Decode_HssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len); +WP11_LOCAL int wp11_HssCheckTypesAttr(WP11_Object* object, + CK_ATTRIBUTE_TYPE type, const byte* data, + CK_ULONG len); + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Delete the signature-state file of a stored HSS private key, for + * wp11_Object_Unstore. Works whether or not the object has been decoded. */ +WP11_LOCAL void wp11_Hss_RemoveStoredState(WP11_Object* object, int tokenId, + int objId); +#endif + +#endif /* WOLFPKCS11_LMS */ + +#ifdef WOLFPKCS11_XMSS + +WP11_LOCAL int wp11_Object_Encode_XmssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Decode_XmssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Load_XmssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int wp11_Object_Store_XmssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int XmssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len); + +#endif /* WOLFPKCS11_XMSS */ + +#endif /* WOLFPKCS11_HBS_H */ diff --git a/src/include.am b/src/include.am index f54ea410..44b241c8 100644 --- a/src/include.am +++ b/src/include.am @@ -8,7 +8,12 @@ src_libwolfpkcs11_la_SOURCES = \ src/internal.c \ src/wolfpkcs11.c \ src/slot.c \ - src/crypto.c + src/crypto.c \ + src/hbs.c + +noinst_HEADERS += \ + src/wp11_types.h \ + src/hbs.h src_libwolfpkcs11_la_CFLAGS = -DBUILDING_WOLFPKCS11 $(AM_CFLAGS) src_libwolfpkcs11_la_CPPFLAGS = -DBUILDING_WOLFPKCS11 $(AM_CPPFLAGS) diff --git a/src/internal.c b/src/internal.c index 61a6ca7a..571c69fa 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19,177 +19,8 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ -#ifdef HAVE_CONFIG_H - #include -#endif -#include "wolfpkcs11/pkcs11.h" - -#ifndef WOLFSSL_USER_SETTINGS -#include -#else -#include "user_settings.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef WOLFPKCS11_MLKEM -#include -#endif - -#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) -/* OS-specific includes for directory creation */ -#if defined(_WIN32) || defined(_MSC_VER) - #include - #include - #define MKDIR(path) _mkdir(path) -#else - #include - #define MKDIR(path) mkdir(path, 0700) -#endif -#endif - -#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) - #include - #include - #if defined(_WIN32) || defined(_MSC_VER) - #include - #include - #include - #include - #else - #include - #include - #include - #endif -#endif - -#include -#include - -#ifdef WOLFPKCS11_TPM - #include - #include - - #ifndef WOLFPKCS11_TPM_CUST_IO - #include - #ifndef TPM2_IOCB_CTX - #define TPM2_IOCB_CTX NULL - #endif - #endif -#endif - -#ifdef WOLFSSL_MAXQ10XX_CRYPTO -#include -#include -#define MAX_CERT_DATASIZE 2048 -#define MAX_SIG_DATASIZE 64 -#define ECC_KEYCOMPLEN 32 -#endif /* WOLFSSL_MAXQ10XX_CRYPTO */ - -#if defined(WC_RSA_BLINDING) && (!defined(HAVE_FIPS) || \ - (defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION > 2))) -#define WOLFPKCS11_NEED_RSA_RNG -#endif - -#if defined(WOLFPKCS11_TPM) && defined(WOLFSSL_MAXQ10XX_CRYPTO) - #error "wolfTPM and MAXQ10XX are incompatible with each other." -#endif - -/* Helper to get size of struct field */ -#define FIELD_SIZE(type, field) (sizeof(((type *)0)->field)) - -/* Size of hash calculated from PIN. */ -#define PIN_HASH_SZ 32 -/* Size of seed used when calculating hash from PIN. */ -#define PIN_SEED_SZ 16 -/* Size of token's label. */ -#define LABEL_SZ 32 - -/* Length of seed from global random to seed local random. */ -#define RNG_SEED_SZ 32 - - - -/* Maximum size of storage for generated/derived DH key. */ -#ifdef WOLFPKCS11_NSS -#define WP11_MAX_DH_KEY_SZ (8192/8) -#else -#define WP11_MAX_DH_KEY_SZ (4096/8) -#endif - -/* Maximum size of storage for generated/derived symmetric key. */ -#ifdef WOLFPKCS11_NSS -#define WP11_MAX_SYM_KEY_SZ (2048) -#elif !defined(NO_DH) -#define WP11_MAX_SYM_KEY_SZ (4096/8) -#elif defined(HAVE_ECC) -#define WP11_MAX_SYM_KEY_SZ ((521+7)/8) -#else -#define WP11_MAX_SYM_KEY_SZ 64 -#endif - -#ifndef WP11_MAX_CERT_SZ -#define WP11_MAX_CERT_SZ 4096 -#endif - -#define PKCS11_CHECK_VALUE_SIZE 3 - -/* Sizes for storage. */ -#define WP11_MAX_IV_SZ 16 -#define WP11_MAX_GCM_NONCE_SZ 16 -#define WP11_MAX_GCM_TAG_SZ 16 -#define WP11_MAX_GCM_TAG_BITS 128 - -/* wolfSSL ECC signatures are ASN.1 encoding - need to encode and decode. */ -#define ASN_INTEGER 0x02 -#define ASN_OCTET_STRING 0x04 -#define ASN_OBJECT_ID 0x06 -#define ASN_SEQUENCE 0x10 -#define ASN_CONSTRUCTED 0x20 -#define ASN_LONG_LENGTH 0x80 - -/* Create a session handle from slot id and session id. */ -#define SESS_HANDLE(slot, s) (((slot) << 16) | (s)) -/* Determine slot id from session handle. */ -#define SESS_HANDLE_SLOT_ID(s) ((CK_SLOT_ID)((s) >> 16)) -/* Determine session id from session handle. */ -#define SESS_HANDLE_SESS_ID(s) ((s) & 0xffff) - -/* Create an object handle from a onToken and object id. */ -#define OBJ_HANDLE(on, i) (((on) << 28) | (i)) -/* Determine whether object is onToken from object handle. */ -#define OBJ_HANDLE_ON_TOKEN(h) ((int)((h) >> 28)) -/* Determine object id from object handle. */ -#define OBJ_HANDLE_OBJ_ID(h) ((h) & 0xfffffff) - -#ifdef SINGLE_THREADED -/* Disable locking. */ -typedef int WP11_Lock; - -#define WP11_Lock_Init(l) ({ 0; }) -#define WP11_Lock_Free(l) -#define WP11_Lock_LockRW(l) ({ 0; }) -#define WP11_Lock_UnlockRW(l) ({ 0; }) -#define WP11_Lock_LockRO(l) ({ 0; }) -#define WP11_Lock_UnlockRO(l) ({ 0; }) - -#else -typedef struct WP11_Lock { - wolfSSL_Mutex read; /* Mutex for accessing count */ - wolfSSL_Mutex write; /* Mutex for writing */ - int cnt; /* Count of readers */ -} WP11_Lock; -#endif +#include "wp11_types.h" +#include "hbs.h" #ifdef DEBUG_WOLFPKCS11 int wolfpkcs11_debugging = 0; @@ -208,373 +39,6 @@ WP11_API void wolfPKCS11_Debugging_Off(void) #endif -/* Symmetric key data. */ -typedef struct WP11_Data { - byte data[WP11_MAX_SYM_KEY_SZ]; /* Key data */ - word32 len; /* Length of key data in bytes */ -} WP11_Data; - -typedef struct WP11_GenericData { - byte *data; - word32 dataLen; - byte *application; - word32 applicationLen; - byte *objectId; /* CKA_OBJECT_ID, a DER encoded ID */ - word32 objectIdLen; -} WP11_GenericData; - -/* Certificate */ -typedef struct WP11_Cert { - byte *data; /* Certificate data */ - word32 len; /* Length of certificate data in bytes */ - CK_CERTIFICATE_TYPE type; -} WP11_Cert; - -typedef struct WP11_Trust { - byte sha1Hash[WC_SHA_DIGEST_SIZE]; - byte md5Hash[WC_MD5_DIGEST_SIZE]; - CK_ULONG serverAuth; - CK_ULONG clientAuth; - CK_ULONG emailProtection; - CK_ULONG codeSigning; - CK_BBOOL stepUpApproved; -} WP11_Trust; - -#ifndef NO_DH -typedef struct WP11_DhKey { - byte key[WP11_MAX_DH_KEY_SZ]; /* Public or private key */ - word32 len; /* Length of public key */ - DhKey params; /* DH parameters object */ -} WP11_DhKey; -#endif - -/* When adding/modifying new members, add support in WP11_Object_Copy */ -struct WP11_Object { - union { - #ifndef NO_RSA - RsaKey* rsaKey; /* RSA key object */ - #endif - #ifdef HAVE_ECC - ecc_key* ecKey; /* EC key object */ - #endif - #ifdef WOLFPKCS11_MLDSA - wc_MlDsaKey* mldsaKey; /* ML-DSA key object */ - #endif - #ifndef NO_DH - WP11_DhKey* dhKey; /* DH parameters object */ - #endif - #ifdef WOLFPKCS11_MLKEM - MlKemKey* mlKemKey; /* ML-KEM key object */ - #endif - #ifdef WOLFPKCS11_LMS - LmsKey* lmsKey; /* LMS/HSS key object (verify) */ - #endif - #ifdef WOLFPKCS11_XMSS - XmssKey* xmssKey; /* XMSS/XMSS^MT key object (verify) */ - #endif - WP11_Data* symmKey; /* Symmetric key object */ - WP11_GenericData genericData; /* Generic data object */ - WP11_Cert cert; /* Certificate object */ - #ifdef WOLFPKCS11_NSS - WP11_Trust trust; /* Trust object */ - #endif - } data; -#ifdef WOLFPKCS11_TPM - WOLFTPM2_KEYBLOB* tpmKey; -#endif -#ifdef WOLFSSL_STM32U5_DHUK - unsigned char* dhukIv; /* IV used with wrapping and unwrapping AES key. */ - int dhukIvLen; -#endif - CK_KEY_TYPE type; /* Key type of this object */ - word32 size; /* Size of the key in bits or bytes */ -#ifndef WOLFPKCS11_NO_STORE - unsigned char* keyData; /* Encoded key data */ - int keyDataLen; /* Length of encoded key data */ - byte iv[GCM_NONCE_MID_SZ]; /* IV/nonce for encrypt/decrypt */ - byte encoded:1; /* Key isn't in decoded form */ -#endif - - WP11_Session* session; /* Session object belongs to */ - WP11_Slot* slot; /* Slot object belongs to */ - - CK_OBJECT_HANDLE handle; /* Handle of this object */ - CK_OBJECT_CLASS objClass; /* Object class */ - CK_MECHANISM_TYPE keyGenMech; /* Key Gen mechanism created with */ - byte onToken:1; /* Object on token or session */ - byte local:1; /* Locally created object */ - word32 opFlag; /* Flags of operations allowed */ -#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE - word64 statefulStateId; /* Non-zero nonce keying the durable */ - /* signature-state file (0 = none) */ - word64 statefulSigCount; /* Signatures produced so far (bound */ - /* into the state-file AAD) */ -#endif - - char startDate[8]; /* Start date of usage */ - char endDate[8]; /* End data of usage */ - - unsigned char* keyId; /* Key identifier */ - int keyIdLen; /* Length of key identifier */ - unsigned char* label; /* Object label */ - int labelLen; /* Length of object label */ - unsigned char* issuer; /* Certificate issuer */ - int issuerLen; /* Length of certificate issuer */ - unsigned char* serial; /* Certificate serial number */ - int serialLen; /* Length of certificate serial number */ - unsigned char* subject; /* Subject of the object */ - int subjectLen; /* Length of subject */ -#ifdef WOLFPKCS11_NSS - unsigned char* email; /* Certificate email (NSS extension) */ - int emailLen; /* Length of certificate email */ -#endif - - word32 category; /* Category of certificate */ - - WP11_Lock* lock; /* Object specific lock */ - - int devId; - WP11_Object* next; /* Next object in linked list */ -}; - -typedef struct WP11_Find { - int state; /* Whether operation is initialized */ - CK_OBJECT_HANDLE found[WP11_FIND_MAX]; - /* List of object handles found */ - int count; /* Count of object handles */ - int curr; /* Index of last object returned */ -} WP11_Find; - -#ifndef NO_RSA -#ifndef WC_NO_RSA_OAEP -typedef struct WP11_OaepParams { - enum wc_HashType hashType; /* Type of hash algorithm */ - int mgf; /* Mask Generation Function */ - byte* label; /* Label or AAD */ - int labelSz; /* Size of label in bytes */ -} WP11_OaepParams; -#endif - -#ifdef WC_RSA_PSS -typedef struct WP11_PssParams { - enum wc_HashType hashType; - int mgf; - int saltLen; -} WP11_PssParams; -#endif -#endif - -#ifdef WOLFPKCS11_MLDSA -typedef struct WP11_MldsaParams { - enum wc_HashType preHashType; - word32 hedgeType; - byte ctx[256]; - byte ctxSz; -} WP11_MldsaParams; -#endif - -#ifndef NO_AES -#ifdef HAVE_AES_CBC -typedef struct WP11_CbcParams { - unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ - Aes aes; /* AES object from wolfCrypt */ - unsigned char partial[AES_BLOCK_SIZE]; - /* Partial block when streaming */ - byte partialSz; /* Size of partial block data */ -} WP11_CbcParams; -#endif - -#ifdef HAVE_AESCTR -typedef struct WP11_CtrParams { - Aes aes; /* AES object from wolfCrypt */ -} WP11_CtrParams; -#endif - -#ifdef HAVE_AESGCM -typedef struct WP11_GcmParams { - unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; - /* IV/nonce data */ - int ivSz; /* IV/nonce size in bytes */ - unsigned char* aad; /* Additional Authentication Data */ - int aadSz; /* AAD size in bytes */ - int tagBits; /* Authentication tag size in bits */ - unsigned char authTag[WP11_MAX_GCM_TAG_SZ]; - /* Authentication tag calculated */ - unsigned char* enc; /* Cached data for multi-part: buffered */ - /* ciphertext (decrypt) or, without */ - /* streaming GCM, plaintext (encrypt) */ - int encSz; /* Size of cached data in bytes */ -#ifdef WOLFSSL_AESGCM_STREAM - Aes aes; /* Streaming context for multi-part */ - /* encrypt */ - int streamInit; /* Streaming context is initialized */ -#endif -} WP11_GcmParams; -#endif - -#ifdef HAVE_AESCCM -typedef struct WP11_CcmParams { - int dataSz; /* Data size in bytes */ - unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; - /* IV/nonce data */ - int ivSz; /* IV/nonce size in bytes */ - unsigned char* aad; /* Additional Authentication Data */ - int aadSz; /* AAD size in bytes */ - int macSz; /* Size of MAC data in bytes */ -} WP11_CcmParams; -#endif - -#ifdef HAVE_AES_KEYWRAP -typedef struct WP11_KeyWrapParams { - Aes aes; /* AES object from wolfCrypt */ - unsigned char iv[8]; /* IV/nonce data */ - word32 ivSz; /* IV/nonce size in bytes */ -} WP11_KeyWrapParams; -#endif - -#ifdef HAVE_AESCTS -typedef struct WP11_CtsParams { - unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ - Aes aes; /* AES object from wolfCrypt */ -} WP11_CtsParams; -#endif -#endif - -#ifndef NO_HMAC -typedef struct WP11_Hmac { - Hmac hmac; - word32 hmacSz; -} WP11_Hmac; -#endif - -typedef struct WP11_Digest { - wc_HashAlg hash; - enum wc_HashType hashType; -} WP11_Digest; - -#ifdef HAVE_AESCMAC -typedef struct WP11_Cmac { - Cmac cmac; - byte sigLen; -} WP11_Cmac; -#endif - -typedef struct WP11_TlsMacParams { - enum wc_MACAlgorithm mac; - word32 macSz; - CK_OBJECT_HANDLE key; - byte server:1; - byte isTlsPrf:1; -} WP11_TlsMacParams; - -struct WP11_Session { - unsigned char inUse; /* Indicates session has been opened */ - CK_SESSION_HANDLE handle; /* CryptoKi API session handle value */ - CK_MECHANISM_TYPE mechanism; /* Op that is being performed */ - CK_SLOT_ID slotId; /* Id of slot that session is on */ - WP11_Slot* slot; /* Slot that session is on */ - WP11_Object* object; /* Linked list of objects on session */ - int objCnt; /* Count of objects in session */ - WP11_Object* curr; /* Current object */ - WP11_Find find; /* Find data */ - int init; /* Which op is initialized */ - union { -#ifndef NO_RSA - #ifndef WC_NO_RSA_OAEP - WP11_OaepParams oaep; /* RSA-OAEP parameters */ - #endif - #ifdef WC_RSA_PSS - WP11_PssParams pss; /* RSA-PSS parameters */ - #endif -#endif -#ifdef WOLFPKCS11_MLDSA - WP11_MldsaParams mldsa; /* ML-DSA parameters */ -#endif -#ifndef NO_AES - #ifdef HAVE_AES_CBC - WP11_CbcParams cbc; /* AES-CBC parameters */ - #endif - #ifdef HAVE_AESCTR - WP11_CtrParams ctr; /* AES-CTR parameters */ - #endif - #ifdef HAVE_AESGCM - WP11_GcmParams gcm; /* AES-GCM parameters */ - #endif - #ifdef HAVE_AESCCM - WP11_CcmParams ccm; /* AES-CCM parameters */ - #endif - #ifdef HAVE_AESCTS - WP11_CtsParams cts; /* AES-CTS parameters */ - #endif - #ifdef HAVE_AES_KEYWRAP - WP11_KeyWrapParams kw; /* AES-KW parameters */ - #endif -#endif -#ifndef NO_HMAC - WP11_Hmac hmac; /* HMAC parameters */ -#endif - WP11_Digest digest; /* Digest parameters */ -#ifdef HAVE_AESCMAC - WP11_Cmac cmac; /* CMAC parameters */ -#endif - WP11_TlsMacParams tlsMac; /* TLS MAC parameters */ - } params; - - byte* data; /* Held data for one-shot mechs */ - word32 dataSz; /* Size of held data */ - - int devId; - WP11_Session* next; /* Next session for slot */ -}; - -typedef struct WP11_Token { - char label[LABEL_SZ]; /* Token label */ - int state; /* Token initialize state */ - byte soPin[PIN_HASH_SZ]; /* SO's PIN hashed with seed */ - int soPinLen; /* Used to indicate PIN set */ - byte soPinSeed[PIN_SEED_SZ]; /* Seed for calculating SO's PIN */ - int soFailedLogin; /* Count of consecutive failed logins */ - time_t soLastFailedLogin; /* Time of last login if it failed */ - time_t soFailLoginTimeout; /* Timeout after max login fails */ - byte userPin[PIN_HASH_SZ]; /* User's PIN hashed with seed */ - int userPinLen; /* Used to indicate PIN set */ - byte userPinSeed[PIN_SEED_SZ]; /* Seed for calculating user's PIN */ - int userFailedLogin; /* Count of consecutive failed logins */ - time_t userLastFailedLogin; /* Time of last login if it failed */ - time_t userFailLoginTimeout; /* Timeout after max login fails */ -#ifndef WOLFPKCS11_NO_STORE - byte seed[PIN_SEED_SZ]; /* Seed used to calculate key */ - byte key[AES_256_KEY_SIZE]; /* Key to en/decrypt private data */ -#endif - WC_RNG rng; /* Random number generator */ - WP11_Lock rngLock; /* Lock for random access */ - WP11_Lock lock; /* Lock for object access */ - int loginState; /* Login state of the token */ - WP11_Object* object; /* Linked list of token objects */ - int objCnt; /* Count of objects on token */ - int tokenFlags; /* Flags for token */ - int nextObjId; - byte userPinEmpty:2; /* Indicates user PIN is empty - * 0 = not set - * 1 = empty - * 2 = not empty */ -} WP11_Token; - -struct WP11_Slot { - CK_SLOT_ID id; /* CryptoKi API slot id value */ - WP11_Token token; /* Token information for slot */ - WP11_Session* session; /* Linked list of sessions */ - WP11_Lock lock; /* Lock for access to slot info */ - - int devId; -#ifdef WOLFPKCS11_TPM - WOLFTPM2_DEV tpmDev; - WOLFTPM2_KEY tpmSrk; - WOLFTPM2_SESSION tpmSession; - TpmCryptoDevCtx tpmCtx; -#endif -}; - #if defined(HAVE_FIPS) && FIPS_VERSION_LT(6,0) #define USE_LOCAL_CURVE_OID_LOOKUP typedef struct WP11_Ecc_Curve @@ -698,7 +162,7 @@ static void WP11_Lock_Free(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_LockRW(WP11_Lock* lock) +int WP11_Lock_LockRW(WP11_Lock* lock) { int ret; @@ -717,7 +181,7 @@ static int WP11_Lock_LockRW(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_UnlockRW(WP11_Lock* lock) +int WP11_Lock_UnlockRW(WP11_Lock* lock) { int ret; @@ -738,7 +202,7 @@ static int WP11_Lock_UnlockRW(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_LockRO(WP11_Lock* lock) +int WP11_Lock_LockRO(WP11_Lock* lock) { int ret; @@ -765,7 +229,7 @@ static int WP11_Lock_LockRO(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_UnlockRO(WP11_Lock* lock) +int WP11_Lock_UnlockRO(WP11_Lock* lock) { int ret; @@ -786,7 +250,7 @@ static int WP11_Lock_UnlockRO(WP11_Lock* lock) } #endif -static int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng) +int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng) { int ret; unsigned char seed[RNG_SEED_SZ]; @@ -802,7 +266,7 @@ static int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng) return ret; } -static void Rng_Free(WC_RNG* rng) +void Rng_Free(WC_RNG* rng) { wc_FreeRng(rng); } @@ -1179,18 +643,18 @@ typedef struct WP11_FileStoreCtx { XFILE file; int is_write; int has_temp; -#ifdef WOLFPKCS11_LMS_PRIVATE +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE int durable; /* fsync file + directory before commit succeeds */ #endif char final_name[WP11_STORE_MAX_PATH]; char temp_name[WP11_STORE_MAX_PATH]; } WP11_FileStoreCtx; -#ifdef WOLFPKCS11_LMS_PRIVATE +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE /* Mark a write-mode file storage context as requiring full durability (fsync * of file data + parent directory) before the close path returns success. * Call after the store is opened for write, before writing. */ -static void wolfPKCS11_Store_SetDurable(void* store, int durable) +void wolfPKCS11_Store_SetDurable(void* store, int durable) { #ifndef WOLFPKCS11_TPM_STORE WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; @@ -1203,6 +667,42 @@ static void wolfPKCS11_Store_SetDurable(void* store, int durable) } #endif +/* Copy the directory part of path into dirPath, without the trailing + * separator. A path with no separator has no directory to return, and one + * whose only separator is the leading character names the filesystem root, + * which is never a usable token directory - both are rejected. + * + * @param path [in] Full path to a file. + * @param dirPath [out] Buffer receiving the directory part. + * @param dirPathLen [in] Size of dirPath in bytes. + * @return READ_ONLY_E when there is no directory part or it does not fit. + * 0 on success. + */ +static int wolfPKCS11_StoreDirName(const char* path, char* dirPath, + int dirPathLen) +{ + int len; + int slash = -1; + int i; + + if (path == NULL || dirPath == NULL) + return READ_ONLY_E; + + len = (int)XSTRLEN(path); + for (i = len - 1; i >= 0; i--) { + if (path[i] == '/' || path[i] == '\\') { + slash = i; + break; + } + } + if (slash <= 0 || slash >= dirPathLen) + return READ_ONLY_E; + + XMEMCPY(dirPath, path, slash); + dirPath[slash] = '\0'; + return 0; +} + static void wolfPKCS11_StoreAbortTemp(WP11_FileStoreCtx* ctx) { if (ctx != NULL && ctx->has_temp) { @@ -1228,32 +728,15 @@ static int wolfPKCS11_StoreCommitTemp(WP11_FileStoreCtx* ctx) if (rename(ctx->temp_name, ctx->final_name) != 0) { ret = READ_ONLY_E; } -#ifdef WOLFPKCS11_LMS_PRIVATE +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE /* Durable mode: fsync the parent directory so the rename itself is * persisted (the directory entry, not just the file data). Otherwise a * crash could lose the rename and roll the signature state back. */ if (ret == 0 && ctx->durable) { char dirPath[WP11_STORE_MAX_PATH]; - int n = (int)XSTRLEN(ctx->final_name); - int slash = -1, i; - for (i = n - 1; i >= 0; i--) { - if (ctx->final_name[i] == '/') { - slash = i; - break; - } - } - if (slash <= 0) { - dirPath[0] = (slash == 0) ? '/' : '.'; - dirPath[1] = '\0'; - } - else if (slash < (int)sizeof(dirPath)) { - XMEMCPY(dirPath, ctx->final_name, slash); - dirPath[slash] = '\0'; - } - else { - ret = READ_ONLY_E; - } + ret = wolfPKCS11_StoreDirName(ctx->final_name, dirPath, + (int)sizeof(dirPath)); if (ret == 0) { int dirFd = open(dirPath, O_RDONLY); if (dirFd >= 0) { @@ -1746,9 +1229,6 @@ int wolfPKCS11_Store_OpenSz(int type, CK_ULONG id1, CK_ULONG id2, int read, if (ret == 0) { char dirPath[WP11_STORE_MAX_PATH]; - const char* lastSlash = NULL; - size_t nameLen = XSTRLEN(name); - size_t i; XMEMSET(ctx, 0, sizeof(*ctx)); ctx->file = XBADFILE; @@ -1771,30 +1251,12 @@ int wolfPKCS11_Store_OpenSz(int type, CK_ULONG id1, CK_ULONG id2, int read, } } else if (ret == 0) { - for (i = 0; i < nameLen; i++) { - if (name[i] == '/' || name[i] == '\\') { - lastSlash = &name[i]; - } - } - - if (lastSlash == NULL) { - ret = READ_ONLY_E; + ret = wolfPKCS11_StoreDirName(name, dirPath, (int)sizeof(dirPath)); + if (ret == 0) { + ret = wolfPKCS11_StoreEnsureDir(dirPath); } - else { - int dirLen = (int)(lastSlash - name); - - if (dirLen <= 0 || dirLen >= (int)sizeof(dirPath)) { - ret = READ_ONLY_E; - } - else { - XMEMCPY(dirPath, name, dirLen); - dirPath[dirLen] = '\0'; - - ret = wolfPKCS11_StoreEnsureDir(dirPath); - if (ret == 0) { - ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); - } - } + if (ret == 0) { + ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); } } } @@ -1844,17 +1306,17 @@ int wolfPKCS11_Store_Open(int type, CK_ULONG id1, CK_ULONG id2, int read, } /** - * Closes access to location being read or written. - * Any dynamic memory associated with the store is freed here. + * Closes access to location being read or written and reports whether the + * commit succeeded. Any dynamic memory associated with the store is freed + * here. A close that cannot report failure would mask a failed fsync or + * rename, which for stateful signature state risks releasing a signature + * whose advanced state never reached stable storage. * * @param [in] store Context for operation. + * @return 0 on success. + * @return Other value when a durable flush or the temp-file commit failed. */ -/* Close the store and report whether the durable commit succeeded. A plain - * void close would mask a failed fsync/rename, which for stateful signature - * state would risk releasing a signature whose advanced state was not - * persisted. Returns 0 on success, non-zero if a durable flush or the - * temp-file commit failed. */ -static int wolfPKCS11_Store_CloseAndReport(void* store) +int wolfPKCS11_Store_CloseAndReport(void* store) { #ifdef WOLFPKCS11_TPM_STORE WP11_TpmStore* tpmStore = (WP11_TpmStore*)store; @@ -1874,12 +1336,13 @@ static int wolfPKCS11_Store_CloseAndReport(void* store) if (ctx != NULL) { int commitRet = 0; int fsyncFailed = 0; -#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(_WIN32) && !defined(_MSC_VER) +#if defined(WOLFPKCS11_STATEFUL_SIG_PRIVATE) && \ + !defined(_WIN32) && !defined(_MSC_VER) int fd; #endif if (ctx->file != XBADFILE && ctx->file != NULL) { -#ifdef WOLFPKCS11_LMS_PRIVATE +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE /* Durable mode: flush the file data to disk before close so the * rename in CommitTemp commits a fully-persisted file. POSIX uses * fsync(); Windows/MSVC uses _commit() (FlushFileBuffers under the @@ -1926,6 +1389,10 @@ static int wolfPKCS11_Store_CloseAndReport(void* store) return ret; } +/* wolfPKCS11_Store_Close is the store backend interface declared in + * wolfpkcs11/store.h and reimplemented by WOLFPKCS11_CUSTOM_STORE builds, so + * it keeps its void return; callers that need the commit result use + * wolfPKCS11_Store_CloseAndReport above instead. */ void wolfPKCS11_Store_Close(void* store) { (void)wolfPKCS11_Store_CloseAndReport(store); @@ -2038,7 +1505,7 @@ int wolfPKCS11_Store_Write(void* store, unsigned char* buffer, int len) * @return NOT_AVAILABLE_E when data not available. * @return Other value to indicate failure. */ -static int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, +int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, void** storage) { return wolfPKCS11_Store_Open(type, id1, id2, 1, storage); @@ -2056,7 +1523,7 @@ static int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, * @return NOT_AVAILABLE_E when data not available. * @return Other value to indicate failure. */ -static int wp11_storage_open(int type, CK_ULONG id1, CK_ULONG id2, +int wp11_storage_open(int type, CK_ULONG id1, CK_ULONG id2, int variableSz, void** storage) { #ifdef WOLFPKCS11_TPM_STORE @@ -2078,7 +1545,7 @@ static int wp11_storage_remove(int type, CK_ULONG id1, CK_ULONG id2) * * @param [in] store Context for operation. */ -static void wp11_storage_close(void* storage) +void wp11_storage_close(void* storage) { wolfPKCS11_Store_Close(storage); } @@ -2092,7 +1559,7 @@ static void wp11_storage_close(void* storage) * @return 0 on success. * @return BUFFER_E to indicate failure. */ -static int wp11_storage_read(void* storage, unsigned char* buffer, int len) +int wp11_storage_read(void* storage, unsigned char* buffer, int len) { int ret = 0; unsigned char* p = buffer; @@ -2127,7 +1594,7 @@ static int wp11_storage_read(void* storage, unsigned char* buffer, int len) * @return 0 on success. * @return BUFFER_E to indicate failure. */ -static int wp11_storage_write(void* storage, unsigned char* buffer, int len) +int wp11_storage_write(void* storage, unsigned char* buffer, int len) { int ret = 0; unsigned char* p = buffer; @@ -2442,7 +1909,7 @@ static int wp11_storage_read_array(void* storage, unsigned char* buffer, * @return 0 on success. * @return BUFFER_E to indicate failure. */ -static int wp11_storage_write_array(void* storage, +int wp11_storage_write_array(void* storage, unsigned char* buffer, word32 len) { int ret; @@ -2467,7 +1934,7 @@ static int wp11_storage_write_array(void* storage, * @return BUFFER_E to indicate failure. * @return MEMORY_E when dynamic memory allocation fails. */ -static int wp11_storage_read_alloc_array(void* storage, +int wp11_storage_read_alloc_array(void* storage, unsigned char** buffer, int* len) { int ret; @@ -3317,50 +2784,6 @@ static int wp11_DecryptData(byte* out, byte* data, int len, byte* key, return ret; } -#ifdef WOLFPKCS11_LMS_PRIVATE -/* AES-GCM encrypt with caller-supplied AAD. The tag is appended to the - * ciphertext (out[len .. len+AES_BLOCK_SIZE-1]). Used for HSS state files so - * the file header (magic, version, parameters, sig count) is bound to the - * ciphertext via authenticated additional data; any tampering with those - * bytes is detected at decrypt time. */ -static int wp11_EncryptDataAAD(byte* out, const byte* data, int len, - const byte* key, int keySz, const byte* iv, int ivSz, - const byte* aad, int aadSz, int devId) -{ - Aes aes; - int ret; - - ret = wc_AesInit(&aes, NULL, devId); - if (ret == 0) - ret = wc_AesGcmSetKey(&aes, key, keySz); - if (ret == 0) { - ret = wc_AesGcmEncrypt(&aes, out, data, len, iv, ivSz, out + len, - AES_BLOCK_SIZE, aad, aadSz); - } - wc_AesFree(&aes); - - return ret; -} - -static int wp11_DecryptDataAAD(byte* out, const byte* data, int len, - const byte* key, int keySz, const byte* iv, int ivSz, - const byte* aad, int aadSz, int devId) -{ - Aes aes; - int ret; - - ret = wc_AesInit(&aes, NULL, devId); - if (ret == 0) - ret = wc_AesGcmSetKey(&aes, key, keySz); - if (ret == 0) { - ret = wc_AesGcmDecrypt(&aes, out, data, len, iv, ivSz, data + len, - AES_BLOCK_SIZE, aad, aadSz); - } - wc_AesFree(&aes); - - return ret; -} -#endif /* WOLFPKCS11_LMS_PRIVATE */ /** * "Decode" the certificate. @@ -4647,10 +4070,8 @@ static int MldsaKeyTryDecode(wc_MlDsaKey* key, byte level, byte* data, int ret = 0; word32 idx = 0; - /* Init key with the object's devId (INVALID_DEVID for no-device builds via - * wp11_Slot_Init's default) so ML-DSA offloads to a configured - * crypto-callback device and falls through to software otherwise. Mirrors - * the import/copy paths, which already use the object devId. */ + /* Use the object devId, as the import and copy paths do, so a configured + * crypto-callback device is used when there is one. */ ret = wc_MlDsaKey_Init(key, NULL, devId); if (ret == 0) { @@ -5456,2012 +4877,159 @@ static int wp11_Object_Store_MlKemKey(WP11_Object* object, int tokenId, #endif /* WOLFPKCS11_MLKEM */ #ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE -/* =========================================================================== - * Stateful hash-based signature framework - * =========================================================================== - * - * Shared infrastructure for stateful hash-based signature schemes. The first - * concrete user is LMS/HSS (RFC 8554, this file's WOLFPKCS11_LMS_PRIVATE - * block). Future schemes (XMSS, XMSS^MT — RFC 8391) plug into the same - * framework via thin wrappers. - * - * Contract every stateful-sig scheme MUST honour: - * - * 1. Token-resident only. Session-only stateful keys are unsafe (a process - * crash between sign and cleanup can release a signature whose OTS - * index was never persisted). Reject CKA_TOKEN=FALSE in C_GenerateKeyPair - * with CKR_TEMPLATE_INCONSISTENT. - * - * 2. State file keyed by a per-key 64-bit nonce, NOT by the object's - * position in the token list. The nonce is generated at keygen time - * and stored in the shell file's last 8 bytes. WP11_Object holds a - * copy in `statefulStateId`. Token-list reordering (Add/Remove, - * Slot_Store) must NEVER move or delete the state file. - * - * 3. State persistence is synchronous and durable. The wolfSSL write - * callback writes the encrypted state file with fsync + atomic rename - * + parent-directory fsync (unless WOLFPKCS11_STATEFUL_RELAX_FSYNC=1) - * BEFORE returning success — so wolfSSL never releases a signature - * whose state advance has not reached stable storage. - * - * 4. State file uses AES-GCM (slot-key) with the AAD-bound header below. - * Tampering with parameters or the persisted sigCount fails decrypt. - * - * 5. SigCount tracked in the wp11 layer (`statefulSigCount`), persisted - * inside the AAD-bound header, and used to compute KEYS_REMAINING — - * no reads from wolfSSL key internals (priv_raw layout is not public - * API and varies between backends). - * - * 6. Poison-on-failure. Any state-write failure or sign error clears - * WP11_FLAG_STATEFUL_STATE_VALID; subsequent signs return - * CKR_DEVICE_ERROR until the object is reloaded from disk - * (wolfSSL Reload restores in-memory state from the durable file). - * - * 7. Locking: all post-keygen state-mutating paths run under the token - * lock (priv->lock points at &token->lock for token-resident keys). - * EXCEPTION: during the keygen function itself, priv is thread-local - * (no other thread has the handle yet, and priv->lock has not been - * assigned by WP11_Session_AddObject), so the write callback fires - * unlocked. This is safe only because no concurrent reader can find - * priv before keygen returns. - * - * On-disk layouts: - * - * Shell file (per scheme; non-secret metadata): - * [u32 magic][u32 version][scheme-specific params + pub][u64 stateId] - * The trailer (stateId) is always the last 8 bytes — that's how - * wp11_Stateful_PeekStateIdFromShell finds it without parsing. + +/* State files use a 64-bit per-key nonce. The public storage entry points + * (wolfPKCS11_Store_Open / Store_Remove) take ids as CK_ULONG, which is + * 32-bit on LLP64 platforms (Windows) - passing the nonce through them + * truncates it and shrinks the collision space from 2^64 to 2^32. The + * helpers below thread word64 through, formatting the path with %016llx, + * so the nonce stays 64-bit on every platform. * - * State file (AES-GCM, AAD-bound): - * AAD: [u32 magic][u32 version][scheme params (P bytes)][u64 sigCount] - * [u32 ivLen][iv (12 bytes)] - * [u32 ctLen][ciphertext (privSz + 16-byte GCM tag)] - */ + * A TPM store is forbidden for stateful private keys (compile-time #error in + * wolfpkcs11/internal.h), so this code path is file-backed-storage only. */ -/* Big-endian byte helpers for encoding/decoding the AAD-bound state header - * and the shell trailer (compiled in sign builds only - the whole framework - * is gated by WOLFPKCS11_STATEFUL_SIG_PRIVATE above). */ -static word32 wp11_Stateful_ReadU32(const byte* p) -{ - return ((word32)p[0] << 24) | ((word32)p[1] << 16) - | ((word32)p[2] << 8) | (word32)p[3]; -} +/* Bytes a state-file name needs on top of the token directory and the scheme + * prefix, both of which vary and are measured by the caller: + * "/wp11_" (6) + "_priv_state_" (12) + two 16-digit hex ids joined by '_' + * (33) + the terminating NUL (1). */ +#define WP11_STATEFUL_STATE_NAME_FIXED_LEN (6 + 12 + 33 + 1) -static word64 wp11_Stateful_ReadU64(const byte* p) +static int wp11_Stateful_StateFile_Path(const char* schemeFilePrefix, + int tokenId, word64 stateId, char* name, int nameLen) { - return ((word64)p[0] << 56) | ((word64)p[1] << 48) - | ((word64)p[2] << 40) | ((word64)p[3] << 32) - | ((word64)p[4] << 24) | ((word64)p[5] << 16) - | ((word64)p[6] << 8) | (word64)p[7]; -} + const char* str = NULL; + char homePath[256]; -static void wp11_Stateful_WriteU32(byte* p, word32 v) -{ - p[0] = (byte)(v >> 24); - p[1] = (byte)(v >> 16); - p[2] = (byte)(v >> 8); - p[3] = (byte)v; -} - -static void wp11_Stateful_WriteU64(byte* p, word64 v) -{ - p[0] = (byte)(v >> 56); - p[1] = (byte)(v >> 48); - p[2] = (byte)(v >> 40); - p[3] = (byte)(v >> 32); - p[4] = (byte)(v >> 24); - p[5] = (byte)(v >> 16); - p[6] = (byte)(v >> 8); - p[7] = (byte)v; -} - -/* IV length for the AES-GCM-encrypted state file. The full 96-bit GCM - * standard IV; same for every scheme. */ -#define WP11_STATEFUL_IV_LEN 12 - -/* Cached env-var read once at first use: when 1, the state-write callback - * skips fsync() (file and directory). DANGEROUS: documented as non-production - * only. Default is to always fsync. The variable applies to ALL stateful - * hash-based signature schemes (LMS/HSS today; XMSS in the future) — there - * is no per-scheme knob, since the durability requirement is identical. */ -static int wp11_StatefulRelaxFsync = -1; - -static int wp11_StatefulShouldFsync(void) -{ - if (wp11_StatefulRelaxFsync < 0) { -#ifndef WOLFPKCS11_NO_ENV - const char* v = XGETENV("WOLFPKCS11_STATEFUL_RELAX_FSYNC"); - wp11_StatefulRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; -#else - wp11_StatefulRelaxFsync = 0; -#endif - if (wp11_StatefulRelaxFsync) { - /* Single, prominent stderr line — the README documents this is - * non-production. Operators who reach this path must see it. */ - fprintf(stderr, - "wolfPKCS11: WARNING: WOLFPKCS11_STATEFUL_RELAX_FSYNC=1 is " - "set. Stateful hash-signature state writes will skip fsync; " - "a power loss can roll back the leaf-index advance and cause " - "OTS-key REUSE. Do NOT use this setting in production.\n"); - } - } - return wp11_StatefulRelaxFsync ? 0 : 1; -} - -/* State files use a 64-bit per-key nonce. The public storage entry points - * (wolfPKCS11_Store_Open / Store_Remove) take ids as CK_ULONG, which is - * 32-bit on LLP64 platforms (Windows) — passing the nonce through them - * truncates it and shrinks the collision space from 2^64 to 2^32. The - * helpers below thread word64 through, formatting the path with %016llx, - * so the nonce stays 64-bit on every platform. - * - * WOLFPKCS11_TPM_STORE is forbidden combined with WOLFPKCS11_LMS_PRIVATE - * (compile-time #error in wolfpkcs11/internal.h), so this code path is - * file-backed-storage only. */ -static int wp11_Stateful_StateFile_Path(const char* schemeFilePrefix, - int tokenId, word64 stateId, char* name, int nameLen) -{ - const char* str = NULL; - char homePath[256]; - /* "/wp11__priv_state_%016lx_%016llx". Reserve enough for - * the longest scheme prefix we might use ("xmssmtkey" = 9). */ - enum { WP11_STATEFUL_STATE_SUFFIX_RESERVE = 72 }; - - if (schemeFilePrefix == NULL || schemeFilePrefix[0] == '\0') - return -1; -#ifndef WOLFPKCS11_NO_ENV - str = XGETENV("WOLFPKCS11_TOKEN_PATH"); -#endif -#ifdef WOLFPKCS11_NSS - if (str == NULL) - str = storeDir; -#endif - if (str == NULL) { - const char* homeDir = NULL; -#if defined(_WIN32) || defined(_MSC_VER) - homeDir = XGETENV("%APPDIR%"); - if (homeDir != NULL && XSTRLEN(homeDir) <= sizeof(homePath) - 13) { - int len = XSNPRINTF(homePath, sizeof(homePath), - "%s\\wolfPKCS11", homeDir); - if (len > 0 && len < (int)sizeof(homePath)) - str = homePath; - } -#else - homeDir = XGETENV("HOME"); - if (homeDir != NULL && XSTRLEN(homeDir) <= sizeof(homePath) - 13) { - int len = XSNPRINTF(homePath, sizeof(homePath), - "%s/.wolfPKCS11", homeDir); - if (len > 0 && len < (int)sizeof(homePath)) - str = homePath; - } -#endif - } -#ifdef WOLFPKCS11_DEFAULT_TOKEN_PATH - if (str == NULL) - str = WC_STRINGIFY(WOLFPKCS11_DEFAULT_TOKEN_PATH); -#endif - if (str == NULL) - return -1; - if (nameLen <= WP11_STATEFUL_STATE_SUFFIX_RESERVE) - return -1; - if (XSTRLEN(str) + XSTRLEN(schemeFilePrefix) > - (size_t)(nameLen - WP11_STATEFUL_STATE_SUFFIX_RESERVE - 1)) { - return -1; - } - return XSNPRINTF(name, nameLen, - "%s/wp11_%s_priv_state_%016lx_%016llx", - str, schemeFilePrefix, (unsigned long)tokenId, - (unsigned long long)stateId); -} - -/* Open the state file for read or atomic-rename write. Mirrors - * wolfPKCS11_Store_OpenSz's file-mode body but uses the word64-safe path. */ -static int wp11_Stateful_StateFile_Open(const char* schemeFilePrefix, - int tokenId, word64 stateId, int read, void** store) -{ - int ret; - char name[WP11_STORE_MAX_PATH] = ""; - WP11_FileStoreCtx* ctx = NULL; - -#ifndef WOLFPKCS11_NO_ENV - if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) - return NOT_AVAILABLE_E; -#endif - ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, - name, sizeof(name)); - if (ret > 0 && ret < (int)sizeof(name)) - ret = 0; - else - ret = -1; - if (ret == 0) { - ctx = (WP11_FileStoreCtx*)XMALLOC(sizeof(*ctx), NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (ctx == NULL) - ret = MEMORY_E; - } - if (ret == 0) { - char dirPath[WP11_STORE_MAX_PATH]; - const char* lastSlash = NULL; - size_t nameSz = XSTRLEN(name); - size_t i; - XMEMSET(ctx, 0, sizeof(*ctx)); - ctx->file = XBADFILE; - ctx->is_write = (read == 0); - if (nameSz >= sizeof(ctx->final_name)) - ret = READ_ONLY_E; - else - XMEMCPY(ctx->final_name, name, nameSz + 1); - - if (ret == 0 && read) { - ctx->file = XFOPEN(name, "rb"); - if (ctx->file == NULL) - ret = NOT_AVAILABLE_E; - } - else if (ret == 0) { - for (i = 0; i < nameSz; i++) { - if (name[i] == '/' || name[i] == '\\') - lastSlash = &name[i]; - } - if (lastSlash == NULL) { - ret = READ_ONLY_E; - } - else { - int dirLen = (int)(lastSlash - name); - if (dirLen <= 0 || dirLen >= (int)sizeof(dirPath)) { - ret = READ_ONLY_E; - } - else { - XMEMCPY(dirPath, name, dirLen); - dirPath[dirLen] = '\0'; - ret = wolfPKCS11_StoreEnsureDir(dirPath); - if (ret == 0) - ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); - } - } - } - } - if (ret == 0 && (ctx->file == NULL || ctx->file == XBADFILE)) - ret = READ_ONLY_E; - if (ret == 0) { - *store = ctx; - } - else if (ctx != NULL) { - if (ctx->file != NULL && ctx->file != XBADFILE) - XFCLOSE(ctx->file); - if (ctx->has_temp) - wolfPKCS11_StoreAbortTemp(ctx); - XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - return ret; -} - -static int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, - int tokenId, word64 stateId) -{ - int ret; - char name[WP11_STORE_MAX_PATH]; - -#ifndef WOLFPKCS11_NO_ENV - if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) - return NOT_AVAILABLE_E; -#endif - ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, - name, sizeof(name)); - if (ret > 0 && ret < (int)sizeof(name)) - return remove(name); - return -1; -} - -/* Sanity caps for inputs to the generic state-blob helpers. The wolfSSL - * state buffer for any practical scheme is well below 1 MiB; the scheme - * parameter region is small (HSS = 12 bytes, XMSS = 4-12 bytes). Reject - * absurd inputs up front so the size-arithmetic below cannot overflow. */ -#define WP11_STATEFUL_PRIV_MAX (1U << 20) /* 1 MiB */ -#define WP11_STATEFUL_PARAMS_MAX 256U - -/* Persist the encrypted state file durably. Caller passes the scheme's - * (schemeFilePrefix, magic, version) and a packed scheme-params buffer - * that is bound into the GCM AAD. On a successful return the state file - * at (slot_id, statefulStateId) is fsync'd and renamed atomically. - * - * AAD-bound layout (caught by AES-GCM tag): - * [u32 magic][u32 version][u32 schemeParamLen][schemeParamBytes] - * [u64 sigCount] - * Including schemeParamLen in the AAD diagnoses thin-wrapper version skew - * cleanly (a length mismatch surfaces as a header-mismatch BAD_FUNC_ARG - * before decrypt rather than as opaque AES-GCM tag failure). */ -static int wp11_Stateful_WriteStateBlob(WP11_Object* o, - const char* schemeFilePrefix, word32 magic, word32 version, - const byte* schemeParamBytes, word32 schemeParamLen, - const byte* priv, word32 privSz) -{ - int ret; - void* storage = NULL; - byte iv[WP11_STATEFUL_IV_LEN]; - byte* hdr = NULL; - word32 hdrLen; - byte* ct = NULL; - word32 ctLen; - int tokenId; - - if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) - return BAD_FUNC_ARG; - /* Stateful keys are token-resident only — reject any attempt to - * persist state for a session-only object. The HSS write CB enforces - * this too, but the framework helper enforces it for all schemes. */ - if (!o->onToken) - return BAD_FUNC_ARG; - if (o->statefulStateId == 0) - return BAD_FUNC_ARG; - if (schemeParamBytes == NULL && schemeParamLen != 0) - return BAD_FUNC_ARG; - /* Defensive caps: prevent size-arithmetic overflow. */ - if (privSz > WP11_STATEFUL_PRIV_MAX) - return BAD_FUNC_ARG; - if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) - return BAD_FUNC_ARG; - - ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ - hdrLen = 4 + 4 + 4 + schemeParamLen + 8; - hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (hdr == NULL) - return MEMORY_E; - - wp11_Stateful_WriteU32(hdr + 0, magic); - wp11_Stateful_WriteU32(hdr + 4, version); - wp11_Stateful_WriteU32(hdr + 8, schemeParamLen); - if (schemeParamLen > 0) - XMEMCPY(hdr + 12, schemeParamBytes, schemeParamLen); - wp11_Stateful_WriteU64(hdr + 12 + schemeParamLen, o->statefulSigCount); - - /* Fresh GCM nonce per write (NEVER reuse object->iv). */ - ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_STATEFUL_IV_LEN); - if (ret != 0) { - XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return ret; - } - - ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (ct == NULL) { - XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return MEMORY_E; - } - - ret = wp11_EncryptDataAAD(ct, priv, (int)privSz, - o->slot->token.key, (int)sizeof(o->slot->token.key), - iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); - if (ret != 0) { - wc_ForceZero(ct, ctLen); - XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return ret; - } - - /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag — - * unused for file storage, but kept for parity with TPM_STORE. */ - (void)(hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen); - - tokenId = (int)o->slot->id; - ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, - o->statefulStateId, 0, &storage); - if (ret == 0) { - if (wp11_StatefulShouldFsync()) - wolfPKCS11_Store_SetDurable(storage, 1); - ret = wp11_storage_write(storage, hdr, (int)hdrLen); - if (ret == 0) { - byte ivLenBuf[4]; - wp11_Stateful_WriteU32(ivLenBuf, WP11_STATEFUL_IV_LEN); - ret = wp11_storage_write(storage, ivLenBuf, 4); - } - if (ret == 0) - ret = wp11_storage_write(storage, iv, WP11_STATEFUL_IV_LEN); - if (ret == 0) { - byte ctLenBuf[4]; - wp11_Stateful_WriteU32(ctLenBuf, ctLen); - ret = wp11_storage_write(storage, ctLenBuf, 4); - } - if (ret == 0) - ret = wp11_storage_write(storage, ct, (int)ctLen); - /* Capture the close-and-commit result. A void close would mask - * a failed rename/fsync; that would let wolfSSL release a signature - * whose state advance never reached durable storage. */ - { - int closeRet = wolfPKCS11_Store_CloseAndReport(storage); - if (ret == 0) - ret = closeRet; - } - } - - wc_ForceZero(ct, ctLen); - XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); - XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return ret; -} - -/* Read and decrypt the state file. The full header is verified as AAD by - * the AES-GCM tag; tampering with magic, version, scheme params, scheme- - * params length, or the persisted sigCount fails decrypt. The expected - * scheme params are passed by the caller (typically: re-pack from the - * in-memory wolfSSL key); a mismatch returns BAD_FUNC_ARG before decrypt. - * The decrypted sigCount is restored into o->statefulSigCount. */ -static int wp11_Stateful_ReadStateBlob(WP11_Object* o, - const char* schemeFilePrefix, word32 magic, word32 version, - const byte* expectedSchemeParamBytes, word32 schemeParamLen, - byte* priv, word32 privSz) -{ - int ret; - void* storage = NULL; - byte* hdr = NULL; - word32 hdrLen; - byte iv[WP11_STATEFUL_IV_LEN]; - byte* ct = NULL; - word32 m, v, persistedParamLen, ivLen, ctLen; - word64 sigCount = 0; - int tokenId; - - if (o == NULL || priv == NULL || privSz == 0) - return BAD_FUNC_ARG; - if (o->statefulStateId == 0) - return BAD_FUNC_ARG; - if (expectedSchemeParamBytes == NULL && schemeParamLen != 0) - return BAD_FUNC_ARG; - if (privSz > WP11_STATEFUL_PRIV_MAX) - return BAD_FUNC_ARG; - if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) - return BAD_FUNC_ARG; - - hdrLen = 4 + 4 + 4 + schemeParamLen + 8; - hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (hdr == NULL) { - wc_ForceZero(priv, privSz); - return MEMORY_E; - } - - tokenId = (int)o->slot->id; - ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, - o->statefulStateId, 1, &storage); - if (ret != 0) { - XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); - wc_ForceZero(priv, privSz); - return ret; - } - - ret = wp11_storage_read(storage, hdr, (int)hdrLen); - if (ret == 0) { - m = wp11_Stateful_ReadU32(hdr); - v = wp11_Stateful_ReadU32(hdr + 4); - persistedParamLen = wp11_Stateful_ReadU32(hdr + 8); - sigCount = wp11_Stateful_ReadU64(hdr + 12 + schemeParamLen); - if (m != magic || v != version) { - ret = BAD_FUNC_ARG; - } - else if (persistedParamLen != schemeParamLen) { - /* Scheme-params length mismatch — diagnoses thin-wrapper - * version skew before AES-GCM tag failure. */ - ret = BAD_FUNC_ARG; - } - else if (schemeParamLen > 0 && - XMEMCMP(hdr + 12, expectedSchemeParamBytes, schemeParamLen) - != 0) { - ret = BAD_FUNC_ARG; - } - } - if (ret == 0) { - byte buf[4]; - ret = wp11_storage_read(storage, buf, 4); - if (ret == 0) { - ivLen = wp11_Stateful_ReadU32(buf); - if (ivLen != WP11_STATEFUL_IV_LEN) - ret = BAD_FUNC_ARG; - } - } - if (ret == 0) - ret = wp11_storage_read(storage, iv, WP11_STATEFUL_IV_LEN); - if (ret == 0) { - byte buf[4]; - ret = wp11_storage_read(storage, buf, 4); - if (ret == 0) { - ctLen = wp11_Stateful_ReadU32(buf); - if (ctLen < AES_BLOCK_SIZE || - ctLen - AES_BLOCK_SIZE != privSz) { - ret = BAD_FUNC_ARG; - } - } - } - if (ret == 0) { - ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (ct == NULL) - ret = MEMORY_E; - } - if (ret == 0) - ret = wp11_storage_read(storage, ct, (int)ctLen); - if (ret == 0) { - ret = wp11_DecryptDataAAD(priv, ct, (int)privSz, - o->slot->token.key, (int)sizeof(o->slot->token.key), - iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); - /* AES-GCM authentication failure manifests here. */ - } - wp11_storage_close(storage); - if (ct != NULL) { - wc_ForceZero(ct, ctLen); - XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (ret == 0) { - /* AES-GCM verified the header; sigCount cannot have been rolled - * back without detection. Restore into the in-memory object. */ - o->statefulSigCount = sigCount; - } - if (ret != 0) - wc_ForceZero(priv, privSz); - return ret; -} - -/* Recover the per-key stateId from the on-disk shell file. The shell - * format is otherwise scheme-specific, but by contract the LAST 8 bytes - * are always the u64 stateId. Used by Unstore to find the state file - * before deleting it, when the in-memory object hasn't been decoded yet. - * - * Validates the shell's magic + version against caller-supplied values - * before returning the trailer. Without this check, a truncated, replaced, - * or wrong-scheme file at the same (tokenId, objId) would yield a garbage - * stateId that the caller then passes to Store_Remove — silently no-op'ing - * or deleting the wrong file while the real state file orphans on disk. - * - * Returns BAD_FUNC_ARG if magic or version don't match (e.g., wrong scheme, - * file truncated below the header size, or the file is something else). */ -static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, - int storeTypeShell, word32 expectedShellMagic, word32 expectedShellVersion, - word64* outStateId) -{ - int ret; - void* storage = NULL; - unsigned char* buf = NULL; - int bufLen = 0; - - if (outStateId == NULL) - return BAD_FUNC_ARG; - *outStateId = 0; - - ret = wp11_storage_open_readonly(storeTypeShell, - (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); - if (ret != 0) - return ret; - ret = wp11_storage_read_alloc_array(storage, &buf, &bufLen); - wp11_storage_close(storage); - if (ret != 0) - return ret; - - /* Need at least [u32 magic][u32 version] + [u64 stateId trailer]. */ - if (bufLen < 16) { - XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return BAD_FUNC_ARG; - } - if (wp11_Stateful_ReadU32(buf) != expectedShellMagic || - wp11_Stateful_ReadU32(buf + 4) != expectedShellVersion) { - XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return BAD_FUNC_ARG; - } - *outStateId = wp11_Stateful_ReadU64(buf + (bufLen - 8)); - XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return 0; -} - -#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE */ -#ifdef WOLFPKCS11_LMS -/* =========================================================================== - * LMS/HSS (RFC 8554): verification and public-key import. - * - * Verify-only: this build imports raw HSS public keys and verifies - * signatures. There is no private-key/keygen/sign or persisted signature - * state here. The public key is self-describing, so wolfSSL derives the - * parameter set from the key bytes on import; the CKA_HSS_LEVELS / - * CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes are optional and, when - * supplied, are validated against the imported key. - * ========================================================================= */ - -/* Map tree height -> RFC 8554 LMS typecode. Returns 0 on unknown height. */ -static CK_LMS_TYPE wp11_HssHeightToLmsType(int h) -{ - switch (h) { - case 5: return CKL_LMS_SHA256_M32_H5; - case 10: return CKL_LMS_SHA256_M32_H10; - case 15: return CKL_LMS_SHA256_M32_H15; - case 20: return CKL_LMS_SHA256_M32_H20; - case 25: return CKL_LMS_SHA256_M32_H25; - default: return 0; - } -} - -/* Map Winternitz -> LMOTS typecode. Returns 0 on unknown. */ -static CK_LMOTS_TYPE wp11_HssWToLmotsType(int w) -{ - switch (w) { - case 1: return CKL_LMOTS_SHA256_N32_W1; - case 2: return CKL_LMOTS_SHA256_N32_W2; - case 4: return CKL_LMOTS_SHA256_N32_W4; - case 8: return CKL_LMOTS_SHA256_N32_W8; - default: return 0; - } -} - -#ifdef WOLFPKCS11_LMS_PRIVATE -/* Forward parameter maps (RFC 8554 typecode -> height / Winternitz), used by - * keygen to translate the CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes. - * Return 0 on unknown. */ -static int wp11_HssLmsTypeToHeight(CK_LMS_TYPE t) -{ - switch (t) { - case CKL_LMS_SHA256_M32_H5: return 5; - case CKL_LMS_SHA256_M32_H10: return 10; - case CKL_LMS_SHA256_M32_H15: return 15; - case CKL_LMS_SHA256_M32_H20: return 20; - case CKL_LMS_SHA256_M32_H25: return 25; - default: return 0; - } -} - -static int wp11_HssLmotsTypeToW(CK_LMOTS_TYPE t) -{ - switch (t) { - case CKL_LMOTS_SHA256_N32_W1: return 1; - case CKL_LMOTS_SHA256_N32_W2: return 2; - case CKL_LMOTS_SHA256_N32_W4: return 4; - case CKL_LMOTS_SHA256_N32_W8: return 8; - default: return 0; - } -} - -/* On-disk magic + version for the HSS private-key shell file: non-secret - * metadata (parameters + cached public key) with the 8-byte stateId nonce as - * the last 8 bytes. Persisted at keygen so token load can call - * wc_LmsKey_SetParameters before wc_LmsKey_Reload and locate the state file. */ -#define WP11_HSS_SHELL_MAGIC 0x48535350UL /* "HSSP" */ -#define WP11_HSS_SHELL_VERSION 1U - -/* On-disk magic + version for the encrypted HSS state file. The header - * (levels/height/winternitz + persisted signature counter) is bound into the - * AES-GCM tag via AAD, so tampering is detected at decrypt time. */ -#define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ -#define WP11_HSS_STATE_VERSION 1U - -/* HSS scheme param region inside the AAD-bound state header: 3 u32s. */ -#define WP11_HSS_SCHEME_PARAM_LEN 12U - -/* Serialize the private-key shell header: - * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] - * [u32 pubLen][pub]. The 8-byte stateId trailer is appended by the caller. - * Returns bytes written, or BUFFER_E. */ -static int wp11_HssPackShellHeader(byte* out, word32 outLen, int levels, - int height, int winternitz, const byte* pub, word32 pubLen) -{ - word32 needed = 4 + 4 + 4 + 4 + 4 + 4 + pubLen; - word32 idx = 0; - - if (out == NULL || outLen < needed) - return BUFFER_E; - wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_MAGIC); idx += 4; - wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_VERSION); idx += 4; - wp11_Stateful_WriteU32(out + idx, (word32)levels); idx += 4; - wp11_Stateful_WriteU32(out + idx, (word32)height); idx += 4; - wp11_Stateful_WriteU32(out + idx, (word32)winternitz); idx += 4; - wp11_Stateful_WriteU32(out + idx, pubLen); idx += 4; - if (pub != NULL && pubLen > 0) - XMEMCPY(out + idx, pub, pubLen); - idx += pubLen; - return (int)idx; -} - -/* Parse the private-key shell header. On success *pub points into the input - * buffer (no allocation) and *pubLen is its length. */ -static int wp11_HssParseShellHeader(const byte* in, word32 inLen, - int* levels, int* height, int* winternitz, - const byte** pub, word32* pubLen) -{ - word32 idx = 0; - word32 magic, version, pl; - - if (in == NULL || inLen < 24) - return BAD_FUNC_ARG; - magic = wp11_Stateful_ReadU32(in + idx); idx += 4; - version = wp11_Stateful_ReadU32(in + idx); idx += 4; - if (magic != WP11_HSS_SHELL_MAGIC || version != WP11_HSS_SHELL_VERSION) - return BAD_FUNC_ARG; - *levels = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; - *height = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; - *winternitz = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; - pl = wp11_Stateful_ReadU32(in + idx); idx += 4; - /* Cap pubLen against the max HSS public-key size so a malformed shell - * cannot force a giant ImportPubRaw. */ - if (pl > HSS_MAX_PUBLIC_KEY_LEN) - return BAD_FUNC_ARG; - if (idx + pl > inLen) - return BAD_FUNC_ARG; - *pub = in + idx; - *pubLen = pl; - return 0; -} - -/* Pack the HSS scheme params (levels|height|winternitz, big-endian u32s) into - * the AAD-bound region of the state header. */ -static void wp11_Hss_PackSchemeParams(byte out[WP11_HSS_SCHEME_PARAM_LEN], - int levels, int height, int winternitz) -{ - wp11_Stateful_WriteU32(out + 0, (word32)levels); - wp11_Stateful_WriteU32(out + 4, (word32)height); - wp11_Stateful_WriteU32(out + 8, (word32)winternitz); -} - -/* HSS state-file write, keyed on disk by (slot_id, statefulStateId). Thin - * wrapper over the generic durable state-blob writer. */ -static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, - word32 privSz) -{ - int levels = 0, height = 0, winternitz = 0; - byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; - int ret; - - if (o == NULL || o->data.lmsKey == NULL) - return BAD_FUNC_ARG; - ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, - &winternitz); - if (ret != 0) - return ret; - wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); - return wp11_Stateful_WriteStateBlob(o, "hsskey", - (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, - schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); -} - -/* HSS state-file read. Validates the in-memory key's params against the - * AAD-bound header before AES-GCM decrypt. */ -static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) -{ - int levels = 0, height = 0, winternitz = 0; - byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; - int ret; - - if (o == NULL || o->data.lmsKey == NULL) - return BAD_FUNC_ARG; - ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, - &winternitz); - if (ret != 0) - return ret; - wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); - return wp11_Stateful_ReadStateBlob(o, "hsskey", - (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, - schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); -} - -/* wolfSSL write callback: invoked by wc_LmsKey_MakeKey and wc_LmsKey_Sign - * after each state advance. Must return WC_LMS_RC_SAVED_TO_NV_MEMORY on - * success; any other value aborts the sign (wolfSSL zeroizes the signature). */ -static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) -{ - WP11_Object* o = (WP11_Object*)ctx; - - if (o == NULL || priv == NULL || privSz == 0) - return WC_LMS_RC_BAD_ARG; - /* Stateful HSS keys are always token-resident in this build. */ - if (!o->onToken || o->statefulStateId == 0) - return WC_LMS_RC_WRITE_FAIL; - if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) - return WC_LMS_RC_WRITE_FAIL; - return WC_LMS_RC_SAVED_TO_NV_MEMORY; -} - -static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) -{ - WP11_Object* o = (WP11_Object*)ctx; - - if (o == NULL) - return WC_LMS_RC_BAD_ARG; - if (wp11_Hss_ReadStateBlob(o, priv, privSz) != 0) - return WC_LMS_RC_READ_FAIL; - return WC_LMS_RC_READ_TO_MEMORY; -} - -/* Recover the statefulStateId from the on-disk HSS shell (last 8 bytes), - * validating the shell magic+version first. Used by orphan cleanup when the - * object has not been decoded into memory. */ -static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, - word64* outStateId) -{ - return wp11_Stateful_PeekStateIdFromShell(tokenId, objId, - WOLFPKCS11_STORE_HSSKEY_PRIV, (word32)WP11_HSS_SHELL_MAGIC, - WP11_HSS_SHELL_VERSION, outStateId); -} -#endif /* WOLFPKCS11_LMS_PRIVATE */ - -/* Import a raw HSS public key into the given wolfSSL LmsKey. wolfSSL derives - * the parameter set from the key bytes (RFC 8554 self-describing public key), - * so no parameters need to be set first. On failure the (partially - * initialized) key is NOT freed here; the caller owns it. */ -static int wp11_HssImportPub(LmsKey* key, int devId, const byte* pub, - word32 pubLen) -{ - int ret; - - ret = wc_LmsKey_Init(key, NULL, devId); - if (ret == 0) - ret = wc_LmsKey_ImportPubRaw(key, pub, pubLen); - return ret; -} - -/* Validate caller-supplied HSS parameter attributes against the parameters - * wolfSSL derived from the just-imported public key. Per the PKCS#11 v3.3 HSS - * profile the public-key parameters are the individual CK_ULONG attributes - * CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE (top-level tree height encoding) and - * CKA_HSS_LMOTS_TYPE (Winternitz encoding). Each is optional; a supplied value - * that does not match the key is rejected with BAD_FUNC_ARG. levelsAttr, - * lmsAttr and lmotsAttr are the raw attribute buffers (NULL when absent) with - * lengths levelsLen/lmsLen/lmotsLen. */ -static int wp11_HssCheckParamAttrs(LmsKey* key, - const byte* levelsAttr, word32 levelsLen, - const byte* lmsAttr, word32 lmsLen, - const byte* lmotsAttr, word32 lmotsLen) -{ - int ret, levels = 0, height = 0, winternitz = 0; - CK_ULONG v; - - ret = wc_LmsKey_GetParameters(key, &levels, &height, &winternitz); - if (ret != 0) - return ret; - if (levelsAttr != NULL) { - if (levelsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, levelsAttr, sizeof(v)); - if (v != (CK_ULONG)levels) - return BAD_FUNC_ARG; - } - if (lmsAttr != NULL) { - if (lmsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, lmsAttr, sizeof(v)); - if (v != (CK_ULONG)wp11_HssHeightToLmsType(height)) - return BAD_FUNC_ARG; - } - if (lmotsAttr != NULL) { - if (lmotsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, lmotsAttr, sizeof(v)); - if (v != (CK_ULONG)wp11_HssWToLmotsType(winternitz)) - return BAD_FUNC_ARG; - } - return 0; -} - -/* Serialize the public key: keyData holds the raw RFC 8554 HSS public key. */ -static int wp11_Object_Encode_HssKey(WP11_Object* object) -{ - int ret; - word32 pubLen = 0; - - if (object->objClass == CKO_PUBLIC_KEY) { - /* Public key: raw RFC 8554 public key, unencrypted (Step-1 format). */ - ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); - if (ret == 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) - ret = MEMORY_E; - } - if (ret == 0) { - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, object->keyData, - &pubLen); - if (ret == 0) - object->keyDataLen = (int)pubLen; - } - /* On failure release the buffer so a later store does not skip - * re-encoding and persist an uninitialised buffer. Public key material - * only, so no zeroization needed. */ - if (ret != 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - return ret; - } -#ifdef WOLFPKCS11_LMS_PRIVATE - if (object->objClass == CKO_PRIVATE_KEY) { - /* Private key: a non-secret metadata shell (params + cached public - * key + 8-byte stateId trailer). The secret one-time-signature state - * is NEVER written to keyData — it lives only in the AES-GCM state - * file written by the wolfSSL write callback. - * - * The shell is immutable after keygen. Once materialized (built at - * keygen, or loaded from disk), reuse it: a reloaded signing key - * cannot re-export its public root (wolfSSL keeps a key in either - * signing or verify state), so rebuilding here would fail. */ - int levels = 0, height = 0, winternitz = 0; - byte pub[HSS_MAX_PUBLIC_KEY_LEN]; - int hdrLen; - word32 needed; - - if (object->keyData != NULL && object->keyDataLen > 0) - return 0; - - pubLen = sizeof(pub); - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, - &winternitz); - if (ret == 0) - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, pub, &pubLen); - if (ret == 0 && object->statefulStateId == 0) - ret = BAD_FUNC_ARG; - if (ret == 0) { - needed = 24 + pubLen + 8; - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(needed, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) - ret = MEMORY_E; - } - if (ret == 0) { - hdrLen = wp11_HssPackShellHeader(object->keyData, needed, levels, - height, winternitz, pub, pubLen); - if (hdrLen < 0) { - ret = hdrLen; - } - else { - wp11_Stateful_WriteU64(object->keyData + hdrLen, - object->statefulStateId); - object->keyDataLen = hdrLen + 8; - } - } - wc_ForceZero(pub, sizeof(pub)); - if (ret != 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - return ret; - } -#endif - return NOT_AVAILABLE_E; -} - -/* Reconstruct the live wolfSSL key from keyData. Public keys import the raw - * key directly; private keys parse the shell, reload the durable signature - * state (via the read callback), and mark the state valid. */ -static int wp11_Object_Decode_HssKey(WP11_Object* object) -{ - int ret; - - if (object->objClass == CKO_PUBLIC_KEY) { - /* Raw RFC 8554 public key; wolfSSL re-derives the parameter set. */ - ret = wp11_HssImportPub(object->data.lmsKey, object->devId, - object->keyData, (word32)object->keyDataLen); - object->encoded = (ret != 0) ? 1 : 0; - return ret; - } -#ifdef WOLFPKCS11_LMS_PRIVATE - if (object->objClass == CKO_PRIVATE_KEY) { - int levels = 0, height = 0, winternitz = 0; - const byte* pub = NULL; - word32 pubLen = 0; - word32 idx = 0; - - ret = wp11_HssParseShellHeader(object->keyData, - (word32)object->keyDataLen, &levels, &height, &winternitz, - &pub, &pubLen); - if (ret == 0) { - idx = 24 + pubLen; /* 8-byte stateId trailer follows the pub */ - if (idx + 8 > (word32)object->keyDataLen) - ret = BAD_FUNC_ARG; - } - if (ret == 0) { - object->statefulStateId = - wp11_Stateful_ReadU64(object->keyData + idx); - if (object->statefulStateId == 0) - ret = BAD_FUNC_ARG; - } - /* INVALID_DEVID unconditionally (not object->devId): HSS stateful - * signing is software-only by design - HSM/device-backed LMS is out of - * scope. This must hold even when the slot has a real devId (a hardware- - * offload build), so it is pinned here rather than inheriting the slot - * default. A real devId would route to the crypto callback and make - * wc_LmsKey_Reload treat the key as HSM-backed and skip the read - * callback (a no-op), leaving the reloaded key with no private state - - * any later Sign would then reuse leaf index 0 or crash. */ - if (ret == 0) - ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); - if (ret == 0) - ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, height, - winternitz); - /* Wire callbacks before Reload: Reload reads the encrypted state via - * the read callback; the write callback is needed for later signing. */ - if (ret == 0) - ret = wc_LmsKey_SetWriteCb(object->data.lmsKey, - wp11_Hss_WriteState_Cb); - if (ret == 0) - ret = wc_LmsKey_SetReadCb(object->data.lmsKey, - wp11_Hss_ReadState_Cb); - if (ret == 0) - ret = wc_LmsKey_SetContext(object->data.lmsKey, object); - if (ret == 0) - ret = wc_LmsKey_Reload(object->data.lmsKey); - /* Do NOT ImportPubRaw here: wolfSSL keeps an LmsKey in either signing - * (reloaded) or verify (pub) state, not both, and rejects a pub import - * after Reload with BAD_STATE_E. The public root is not needed for - * signing, and the cached shell (kept in keyData) preserves it for - * re-storage. */ - (void)pub; - if (ret == 0) { - object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; - object->encoded = 0; - } - else { - /* Clear the validity flag so a later Sign cannot use a half-built - * key, and free the wolfSSL key. encoded=1 stops Object_Free from - * freeing the wolfSSL key a second time. */ - object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; - wc_LmsKey_Free(object->data.lmsKey); - object->encoded = 1; - } - return ret; - } -#endif - return NOT_AVAILABLE_E; -} - -static int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret; - void* storage = NULL; - int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; - -#ifdef WOLFPKCS11_LMS_PRIVATE - if (object->objClass == CKO_PRIVATE_KEY) - storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; -#endif - ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); - if (ret == 0) { - ret = wp11_storage_read_alloc_array(storage, &object->keyData, - &object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -static int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret = 0; - void* storage = NULL; - int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; - -#ifdef WOLFPKCS11_LMS_PRIVATE - if (object->objClass == CKO_PRIVATE_KEY) - storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; -#endif - if (object->keyData == NULL) - ret = wp11_Object_Encode_HssKey(object); - if (ret == 0) - ret = wp11_storage_open(storeType, tokenId, objId, - object->keyDataLen, &storage); - if (ret == 0) { - ret = wp11_storage_write_array(storage, object->keyData, - object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -/* Import an HSS public key from a C_CreateObject template. Following the - * PKCS#11 v3.3 HSS profile the collected attributes are data[0]=CKA_HSS_LEVELS, - * data[1]=CKA_HSS_LMS_TYPE, data[2]=CKA_HSS_LMOTS_TYPE (all optional CK_ULONG) - * and data[3]=CKA_VALUE, the raw RFC 8554 public key. wolfSSL derives the - * parameter set from the key bytes; any supplied CKA_HSS_* attribute is - * validated against the derived parameters and a mismatch is rejected. - * Private-key import is rejected: caller-supplied stateful private material is - * a forgery vector and out of scope for verify-only support. */ -int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, - CK_ULONG* len) -{ - int ret = 0; - LmsKey* newKey = NULL; - - if (object == NULL || data == NULL || len == NULL) - return BAD_FUNC_ARG; -#ifdef WOLFPKCS11_LMS_PRIVATE - /* Keygen (C_GenerateKeyPair) creates both objects with no CKA_VALUE and - * populates them in WP11_Hss_GenerateKeyPair, so a missing value is a - * deferred keygen object, not an error. A private key must never be - * imported WITH a value: caller-supplied one-time-signature state is a - * forgery vector. */ - if (object->objClass == CKO_PRIVATE_KEY) { - if (data[3] != NULL) - return BAD_FUNC_ARG; - return 0; - } - if (object->objClass != CKO_PUBLIC_KEY) - return BAD_FUNC_ARG; - if (data[3] == NULL) - return 0; -#else - if (object->objClass != CKO_PUBLIC_KEY) - return BAD_FUNC_ARG; - /* A public key must carry its value. There is no keygen path in a - * verify-only build that would populate it later, so an absent - * CKA_VALUE is an incomplete template rather than a deferred import; - * reject it instead of creating an unusable object. */ - if (data[3] == NULL) - return BAD_FUNC_ARG; -#endif - - if (object->onToken) - WP11_Lock_LockRW(object->lock); - - /* Import into a temporary key and swap it in only after it fully validates, - * so a failed re-import (C_SetAttributeValue with a new CKA_VALUE) leaves - * the existing key unchanged as PKCS#11 requires. */ - newKey = (LmsKey*)XMALLOC(sizeof(LmsKey), NULL, DYNAMIC_TYPE_KEY); - if (newKey == NULL) - ret = MEMORY_E; - if (ret == 0) { - XMEMSET(newKey, 0, sizeof(LmsKey)); - ret = wp11_HssImportPub(newKey, object->devId, data[3], - (word32)len[3]); - } - if (ret == 0) { - ret = wp11_HssCheckParamAttrs(newKey, data[0], (word32)len[0], - data[1], (word32)len[1], data[2], (word32)len[2]); - } - if (ret == 0) { - /* Replace the live key and drop the cached serialization so a later - * store re-encodes from the new key instead of persisting stale bytes. */ - if (object->data.lmsKey != NULL) { - if (!object->encoded) - wc_LmsKey_Free(object->data.lmsKey); - XFREE(object->data.lmsKey, NULL, DYNAMIC_TYPE_KEY); - } - object->data.lmsKey = newKey; - object->encoded = 0; - newKey = NULL; - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - if (newKey != NULL) { - wc_LmsKey_Free(newKey); - XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); - } - - if (object->onToken) - WP11_Lock_UnlockRW(object->lock); - return ret; -} - -/* Verify an HSS signature over a raw message. Stateless on the verifier. */ -int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, - word32 dataLen, int* stat, WP11_Object* pub) -{ - int ret; - word32 expSigLen = 0; - - if (pub == NULL || pub->data.lmsKey == NULL || stat == NULL) - return BAD_FUNC_ARG; - - /* Verification only reads the key: wc_LmsKey_Verify works from a locally - * allocated LmsState, so a shared RO lock is safe for concurrent verifies - * on the same token object. */ - if (pub->onToken) - WP11_Lock_LockRO(pub->lock); - - /* A signature whose length does not match the key's parameter set cannot - * be valid; report it as an invalid signature (stat = 0 -> - * CKR_SIGNATURE_INVALID) rather than letting wolfSSL's length check - * surface as a function failure. */ - if (wc_LmsKey_GetSigLen(pub->data.lmsKey, &expSigLen) == 0 && - sigLen != expSigLen) { - *stat = 0; - ret = 0; - } - else { - /* Unlike XMSS, whose wolfSSL verify rejects a zero-length message with - * BAD_FUNC_ARG, wc_LmsKey_Verify accepts an empty message, so HSS needs - * no dataLen == 0 special-case here (see WP11_Xmss_Verify). */ - ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, - (int)dataLen); - /* Only a genuine verification mismatch (SIG_VERIFY_E) maps to stat=0. - * Remaining argument/state errors surface as a function failure so - * they are not silently reported as an invalid signature. */ - if (ret == 0) { - *stat = 1; - } - else if (ret == SIG_VERIFY_E) { - *stat = 0; - ret = 0; - } - else { - *stat = 0; - } - } - - if (pub->onToken) - WP11_Lock_UnlockRO(pub->lock); - - return ret; -} - -#ifdef WOLFPKCS11_LMS_PRIVATE -/* Generate an HSS key pair. levels/lmsType/lmotsType come from the v3.3 - * keygen template attributes; wolfSSL requires uniform parameters across - * levels. The private key's one-time-signature state is durably persisted at - * genesis (via the write callback invoked inside wc_LmsKey_MakeKey) before - * this returns success. */ -int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, - CK_ULONG levels, CK_LMS_TYPE lmsType, - CK_LMOTS_TYPE lmotsType, WP11_Slot* slot) -{ - int ret; - int nLevels, height, winternitz; - WC_RNG rng; - byte pubBuf[HSS_MAX_PUBLIC_KEY_LEN]; - word32 pubLen = sizeof(pubBuf); - int stateWritten = 0; - - if (pub == NULL || priv == NULL || slot == NULL || - pub->data.lmsKey == NULL || priv->data.lmsKey == NULL) { - return BAD_FUNC_ARG; - } - - /* Translate the v3.3 typecodes to wolfSSL (levels, height, winternitz), - * enforcing the wolfSSL level limit. */ - nLevels = (int)levels; - height = wp11_HssLmsTypeToHeight(lmsType); - winternitz = wp11_HssLmotsTypeToW(lmotsType); - if (nLevels < 1 || nLevels > LMS_MAX_LEVELS || height == 0 || - winternitz == 0) { - return BAD_FUNC_ARG; - } - - /* Generate the per-key 64-bit nonce keying the state file. Must be - * non-zero (0 is the "none" sentinel); retry on the vanishing chance of - * zero, else treat as an RNG failure. */ - { - byte nonceBuf[8]; - int tries; - for (tries = 0; tries < 4; tries++) { - ret = WP11_Slot_GenerateRandom(slot, nonceBuf, sizeof(nonceBuf)); - if (ret != 0) - return ret; - priv->statefulStateId = wp11_Stateful_ReadU64(nonceBuf); - if (priv->statefulStateId != 0) - break; - } - if (priv->statefulStateId == 0) - return RNG_FAILURE_E; - } - - /* Mark on-token so the write CB commits; AddObject formalizes the - * registration shortly. priv->lock is assigned later by Session_AddObject; - * during keygen no other thread holds a handle, so the CB fires unlocked. */ - priv->onToken = 1; - priv->statefulSigCount = 0; - - /* INVALID_DEVID unconditionally (not priv->devId): HSS stateful signing is - * software-only by design - HSM/device-backed LMS is out of scope - so this - * is pinned even when the slot has a real devId (a hardware-offload build), - * mirroring the reload path in wp11_Object_Decode_HssKey. A real devId would - * route MakeKey/Sign through the crypto callback and, critically, make a - * later wc_LmsKey_Reload a no-op, so the persisted state could never be - * restored. */ - ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, INVALID_DEVID); - if (ret == 0) - ret = wc_LmsKey_SetParameters(priv->data.lmsKey, nLevels, height, - winternitz); - /* Wire callbacks BEFORE MakeKey so genesis state is persisted durably. */ - if (ret == 0) - ret = wc_LmsKey_SetWriteCb(priv->data.lmsKey, wp11_Hss_WriteState_Cb); - if (ret == 0) - ret = wc_LmsKey_SetReadCb(priv->data.lmsKey, wp11_Hss_ReadState_Cb); - if (ret == 0) - ret = wc_LmsKey_SetContext(priv->data.lmsKey, priv); - - if (ret == 0) - ret = Rng_New(&slot->token.rng, &slot->token.rngLock, &rng); - if (ret == 0) { - /* MakeKey invokes the write CB to persist genesis state; on success - * the genesis state file is durable on disk. */ - ret = wc_LmsKey_MakeKey(priv->data.lmsKey, &rng); - Rng_Free(&rng); - if (ret == 0) - stateWritten = 1; - } - - /* Mirror parameters + public key into the public-key object. */ - if (ret == 0) - ret = wc_LmsKey_ExportPubRaw(priv->data.lmsKey, pubBuf, &pubLen); - if (ret == 0) - ret = wc_LmsKey_Init(pub->data.lmsKey, NULL, pub->devId); - if (ret == 0) - ret = wc_LmsKey_SetParameters(pub->data.lmsKey, nLevels, height, - winternitz); - if (ret == 0) - ret = wc_LmsKey_ImportPubRaw(pub->data.lmsKey, pubBuf, pubLen); - - if (ret == 0) { - priv->local = pub->local = 1; - priv->keyGenMech = pub->keyGenMech = CKM_HSS_KEY_PAIR_GEN; - priv->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; - } - else { - wc_LmsKey_Free(priv->data.lmsKey); - /* Mark encoded so the C_GenerateKeyPair cleanup (WP11_Object_Free) does - * not free the wolfSSL key a second time; mirrors the failure path in - * wp11_Object_Decode_HssKey. */ - priv->encoded = 1; - if (stateWritten && priv->statefulStateId != 0) { - (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, - priv->statefulStateId); - } - priv->statefulStateId = 0; - } - - wc_ForceZero(pubBuf, sizeof(pubBuf)); - return ret; -} - -/* Remove the durable signature-state file backing an HSS private key that was - * generated but never committed to the token - e.g. a later C_GenerateKeyPair - * step (AddObject/SetInitialStates/store) failed after keygen already wrote the - * genesis state to disk. Without this the encrypted state file is orphaned. - * No-op unless priv is a CKK_HSS private key with a live state id; safe to call - * on any object and more than once. */ -void WP11_Hss_RemoveOrphanState(WP11_Object* priv, WP11_Slot* slot) -{ - if (priv != NULL && slot != NULL && priv->type == CKK_HSS && - priv->objClass == CKO_PRIVATE_KEY && priv->statefulStateId != 0) { - (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, - priv->statefulStateId); - priv->statefulStateId = 0; - } -} - -/* Sign a raw message with an HSS private key. State is advanced and persisted - * (via the write CB inside wc_LmsKey_Sign) BEFORE the signature is returned. - * On any failure the in-memory state is poisoned (STATE_VALID cleared) to - * force a reload from durable storage before the next attempt. Token-resident - * keys always have priv->lock == &token->lock. */ -int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, - word32* sigLen, WP11_Object* priv) -{ - int ret = 0; - word32 outLen = 0; - - if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || - sigLen == NULL || priv->lock == NULL) { - return BAD_FUNC_ARG; - } - - WP11_Lock_LockRW(priv->lock); - - if ((priv->opFlag & WP11_FLAG_STATEFUL_STATE_VALID) == 0) { - ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ - } - if (ret == 0) { - word64 prevCount = priv->statefulSigCount; - priv->statefulSigCount = prevCount + 1; - outLen = *sigLen; - ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, - (int)dataLen); - if (ret == 0) { - *sigLen = outLen; - } - else { - /* Write CB failed (durable state may lag the in-memory advance) - * or the key is exhausted. Poison until reloaded; zero any bytes - * wolfSSL wrote and revert the in-memory counter. */ - priv->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; - if (outLen > 0) - XMEMSET(sig, 0, outLen); - priv->statefulSigCount = prevCount; - } - } - - WP11_Lock_UnlockRW(priv->lock); - return ret; -} - -/* Signature length for an HSS private key (parameter-set dependent). Returns - * 0 on error. */ -word32 WP11_Hss_SigLen(WP11_Object* key) -{ - word32 sigLen = 0; - - if (key == NULL || key->data.lmsKey == NULL) - return 0; - if (wc_LmsKey_GetSigLen(key->data.lmsKey, &sigLen) != 0) - return 0; - return sigLen; -} - -/* Remaining one-time keys for an HSS private key. Uses the wp11-layer - * statefulSigCount (persisted in the GCM-authenticated state header) rather - * than reaching into wolfSSL LmsKey internals. total = 2^(levels*height); - * saturates at UINT32_MAX for large parameter sets. */ -int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) -{ - int levels = 0, height = 0, winternitz = 0; - int totalH; - word64 used, total; - - if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) - return BAD_FUNC_ARG; - - if (wc_LmsKey_SigsLeft(key->data.lmsKey) == 0) { - *remaining = 0; - return 0; - } - if (wc_LmsKey_GetParameters(key->data.lmsKey, &levels, &height, - &winternitz) != 0) { - *remaining = 0; - return BAD_FUNC_ARG; - } - totalH = levels * height; - if (totalH <= 0) { - *remaining = 0; - return BAD_FUNC_ARG; - } - if (totalH >= 64) { - *remaining = 0xFFFFFFFFU; - return 0; - } - total = ((word64)1) << totalH; - used = key->statefulSigCount; - if (used >= total) - *remaining = 0; - else if ((total - used) > 0xFFFFFFFFULL) - *remaining = 0xFFFFFFFFU; - else - *remaining = (word32)(total - used); - return 0; -} -#endif /* WOLFPKCS11_LMS_PRIVATE */ - -/* Serve HSS-specific attributes from a public-key object. */ -static int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, - byte* data, CK_ULONG* len) -{ - int ret = 0; - int levels = 0, height = 0, winternitz = 0; - - if (object == NULL || object->data.lmsKey == NULL || len == NULL) - return BAD_FUNC_ARG; - - switch (type) { - case CKA_HSS_LEVELS: { - CK_ULONG v; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - v = (CK_ULONG)levels; - if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - } - break; - } - case CKA_HSS_LMS_TYPE: { - CK_LMS_TYPE v; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - v = wp11_HssHeightToLmsType(height); - if (v == 0) - ret = NOT_AVAILABLE_E; - else if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - } - break; - } - case CKA_HSS_LMOTS_TYPE: { - CK_LMOTS_TYPE v; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - v = wp11_HssWToLmotsType(winternitz); - if (v == 0) - ret = NOT_AVAILABLE_E; - else if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - } - break; - } - case CKA_HSS_LMS_TYPES: - case CKA_HSS_LMOTS_TYPES: { - /* wolfSSL uses uniform LMS/LMOTS parameters across all HSS levels, so - * every array entry is the top-level type. Per-level types (allowed - * by the PKCS#11 v3.3 profile) are not expressible by wolfSSL and are - * not supported here. */ - CK_ULONG total; - CK_ULONG fill; - int i; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - total = (CK_ULONG)(sizeof(CK_ULONG) * levels); - fill = (type == CKA_HSS_LMS_TYPES) - ? wp11_HssHeightToLmsType(height) - : wp11_HssWToLmotsType(winternitz); - if (fill == 0) - ret = NOT_AVAILABLE_E; - else if (data == NULL) - *len = total; - else if (*len < total) { - *len = total; - ret = BUFFER_E; - } - else { - for (i = 0; i < levels; i++) - XMEMCPY(data + i * sizeof(CK_ULONG), &fill, - sizeof(CK_ULONG)); - *len = total; - } - } - break; - } - case CKA_HSS_KEYS_REMAINING: { - /* Remaining-signature count is a private-key/state property. In a - * verify-only build (or for a public key) it is unavailable. */ -#ifdef WOLFPKCS11_LMS_PRIVATE - if (object->objClass == CKO_PRIVATE_KEY) { - word32 remaining = 0; - CK_ULONG v; - if (WP11_Hss_SigsLeft(object, &remaining) != 0) { - ret = NOT_AVAILABLE_E; - break; - } - v = (CK_ULONG)remaining; - if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - break; - } -#endif - ret = NOT_AVAILABLE_E; - break; - } - case CKA_VALUE: - if (object->objClass == CKO_PUBLIC_KEY) { - word32 pubLen = 0; - ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); - if (ret == 0) { - if (data == NULL) - *len = pubLen; - else if (*len < pubLen) { - *len = pubLen; - ret = BUFFER_E; - } - else { - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, - data, &pubLen); - if (ret == 0) - *len = pubLen; - } - } - } - else { - *len = CK_UNAVAILABLE_INFORMATION; - ret = NOT_AVAILABLE_E; - } - break; - default: - ret = NOT_AVAILABLE_E; - break; - } - return ret; -} - -/* Validate a supplied CKA_HSS_LMS_TYPES / CKA_HSS_LMOTS_TYPES array against the - * imported public key. wolfSSL uses uniform LMS/LMOTS parameters across all HSS - * levels, so a conforming array holds one CK_ULONG per level, each equal to the - * single top-level type served by HssObject_GetAttr. A wrong length or any - * differing entry is rejected so a malformed or mismatched template is not - * silently accepted; a matching array (as read back from this key) is accepted. - * Returns 0 when the array matches, BAD_FUNC_ARG otherwise. The caller holds the - * object lock. */ -static int wp11_HssCheckTypesAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, - const byte* data, CK_ULONG len) -{ - int ret; - int levels = 0; - int height = 0; - int winternitz = 0; - int i; - CK_ULONG expected; - CK_ULONG entry; - - if (object->data.lmsKey == NULL || data == NULL) - return BAD_FUNC_ARG; - - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, - &winternitz); - if (ret != 0) - return ret; - - expected = (type == CKA_HSS_LMS_TYPES) - ? (CK_ULONG)wp11_HssHeightToLmsType(height) - : (CK_ULONG)wp11_HssWToLmotsType(winternitz); - if (expected == 0) - return BAD_FUNC_ARG; - if (len != (CK_ULONG)(sizeof(CK_ULONG) * levels)) - return BAD_FUNC_ARG; - - for (i = 0; i < levels; i++) { - XMEMCPY(&entry, data + i * sizeof(CK_ULONG), sizeof(entry)); - if (entry != expected) - return BAD_FUNC_ARG; - } - - return 0; -} -#endif /* WOLFPKCS11_LMS */ - -#ifdef WOLFPKCS11_XMSS -/* =========================================================================== - * XMSS/XMSS^MT (RFC 8391): verification and public-key import. - * - * Verify-only. One wolfSSL XmssKey type serves both XMSS and XMSS^MT; the - * PKCS#11 key type (CKK_XMSS vs CKK_XMSSMT) selects which via the is_xmssmt - * flag. The raw public key carries the parameter-set OID in its leading - * bytes, so wc_XmssKey_ImportPubRaw_ex derives the parameter set on import. - * A CKA_PARAMETER_SET attribute is optional and, when supplied, is validated - * against the OID in the key. - * ========================================================================= */ - -/* Length of the parameter-set OID prefix in a raw RFC 8391 public key. wolfSSL - * only exposes XMSS_OID_LEN when built with WOLFSSL_HAVE_XMSS and it is absent - * from some released headers, so define our own to keep the build independent - * of the wolfSSL version. */ -#define WP11_XMSS_OID_LEN 4 - -/* Big-endian 32-bit read from a byte buffer (used to read the XMSS OID). */ -static word32 wp11_HbsReadU32(const byte* p) -{ - return ((word32)p[0] << 24) | ((word32)p[1] << 16) - | ((word32)p[2] << 8) | (word32)p[3]; -} - -/* Import a raw XMSS/XMSS^MT public key into the given wolfSSL XmssKey, deriving - * the parameter set from the embedded OID. is_xmssmt disambiguates the XMSS and - * XMSS^MT OID namespaces (which overlap) and comes from the PKCS#11 key type. - * When the caller supplies a CKA_PARAMETER_SET (params != NULL, the OID as a - * CK_ULONG) it must match the OID in the public key. On failure the key is NOT - * freed here; the caller owns it. */ -static int wp11_XmssImportPub(XmssKey* key, int is_xmssmt, int devId, - const byte* pub, word32 pubLen, const byte* params, word32 paramsLen) -{ - int ret; - - ret = wc_XmssKey_Init(key, NULL, devId); - if (ret == 0 && params != NULL) { - CK_ULONG userOid; - if (paramsLen != sizeof(CK_ULONG) || pubLen < WP11_XMSS_OID_LEN) { - ret = BAD_FUNC_ARG; + if (schemeFilePrefix == NULL || schemeFilePrefix[0] == '\0') + return -1; +#ifndef WOLFPKCS11_NO_ENV + str = XGETENV("WOLFPKCS11_TOKEN_PATH"); +#endif +#ifdef WOLFPKCS11_NSS + if (str == NULL) + str = storeDir; +#endif + if (str == NULL) { + const char* homeDir = NULL; +#if defined(_WIN32) || defined(_MSC_VER) + homeDir = XGETENV("%APPDIR%"); + if (homeDir != NULL && XSTRLEN(homeDir) <= sizeof(homePath) - 13) { + int len = XSNPRINTF(homePath, sizeof(homePath), + "%s\\wolfPKCS11", homeDir); + if (len > 0 && len < (int)sizeof(homePath)) + str = homePath; } - else { - XMEMCPY(&userOid, params, sizeof(userOid)); - if (userOid != (CK_ULONG)wp11_HbsReadU32(pub)) - ret = BAD_FUNC_ARG; +#else + homeDir = XGETENV("HOME"); + if (homeDir != NULL && XSTRLEN(homeDir) <= sizeof(homePath) - 13) { + int len = XSNPRINTF(homePath, sizeof(homePath), + "%s/.wolfPKCS11", homeDir); + if (len > 0 && len < (int)sizeof(homePath)) + str = homePath; } +#endif } - if (ret == 0) - ret = wc_XmssKey_ImportPubRaw_ex(key, pub, pubLen, is_xmssmt); - return ret; +#ifdef WOLFPKCS11_DEFAULT_TOKEN_PATH + if (str == NULL) + str = WC_STRINGIFY(WOLFPKCS11_DEFAULT_TOKEN_PATH); +#endif + if (str == NULL) + return -1; + if (nameLen <= WP11_STATEFUL_STATE_NAME_FIXED_LEN) + return -1; + if (XSTRLEN(str) + XSTRLEN(schemeFilePrefix) > + (size_t)(nameLen - WP11_STATEFUL_STATE_NAME_FIXED_LEN)) { + return -1; + } + return XSNPRINTF(name, nameLen, + "%s/wp11_%s_priv_state_%016lx_%016llx", + str, schemeFilePrefix, (unsigned long)tokenId, + (unsigned long long)stateId); } -/* Serialize the public key: keyData holds the raw RFC 8391 public key. */ -static int wp11_Object_Encode_XmssKey(WP11_Object* object) +/* Open the state file for read or atomic-rename write. Mirrors + * wolfPKCS11_Store_OpenSz's file-mode body but uses the word64-safe path. */ +int wp11_Stateful_StateFile_Open(const char* schemeFilePrefix, + int tokenId, word64 stateId, int read, void** store) { int ret; - word32 pubLen = 0; + char name[WP11_STORE_MAX_PATH] = ""; + WP11_FileStoreCtx* ctx = NULL; - if (object->objClass != CKO_PUBLIC_KEY) +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) return NOT_AVAILABLE_E; - - ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + ret = 0; + else + ret = -1; if (ret == 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + ctx = (WP11_FileStoreCtx*)XMALLOC(sizeof(*ctx), NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) + if (ctx == NULL) ret = MEMORY_E; } if (ret == 0) { - ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, object->keyData, - &pubLen); - if (ret == 0) - object->keyDataLen = (int)pubLen; - } - /* On failure release the buffer so a later store does not skip re-encoding - * (keyData != NULL) and persist an uninitialised buffer with a stale - * length. This is public key material only, so no zeroization is needed. */ - if (ret != 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - return ret; -} - -static int wp11_Object_Decode_XmssKey(WP11_Object* object) -{ - int ret; - - if (object->objClass != CKO_PUBLIC_KEY) - return NOT_AVAILABLE_E; - - /* Reload from the stored raw public key; wolfSSL re-derives the parameter - * set from the embedded OID (no separate parameter set is persisted). */ - ret = wp11_XmssImportPub(object->data.xmssKey, - (object->type == CKK_XMSSMT) ? 1 : 0, object->devId, object->keyData, - (word32)object->keyDataLen, NULL, 0); - /* Object owns the key on all paths; encoded stays 0 for the single free in - * WP11_Object_Free. See wp11_Object_Decode_HssKey. */ - object->encoded = 0; - return ret; -} - -static int wp11_Object_Load_XmssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret; - void* storage = NULL; - - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, - objId, &storage); - if (ret == 0) { - ret = wp11_storage_read_alloc_array(storage, &object->keyData, - &object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -static int wp11_Object_Store_XmssKey(WP11_Object* object, int tokenId, - int objId) -{ - int ret = 0; - void* storage = NULL; - - if (object->keyData == NULL) - ret = wp11_Object_Encode_XmssKey(object); - if (ret == 0) - ret = wp11_storage_open(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, objId, - object->keyDataLen, &storage); - if (ret == 0) { - ret = wp11_storage_write_array(storage, object->keyData, - object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -/* Import an XMSS/XMSS^MT public key from a C_CreateObject template. data[1] is - * the raw RFC 8391 public key; data[0] (CKA_PARAMETER_SET, the OID as a - * CK_ULONG) is optional and, when present, is validated against the key. - * Private-key import is rejected. */ -int WP11_Object_SetXmssKey(WP11_Object* object, unsigned char** data, - CK_ULONG* len) -{ - int ret = 0; - XmssKey* newKey = NULL; - - if (object == NULL || data == NULL || len == NULL) - return BAD_FUNC_ARG; - if (object->objClass != CKO_PUBLIC_KEY) - return BAD_FUNC_ARG; - /* A public key must carry its value; an absent CKA_VALUE is an - * incomplete template (no keygen path fills it later). Reject it. */ - if (data[1] == NULL) - return BAD_FUNC_ARG; + char dirPath[WP11_STORE_MAX_PATH]; + size_t nameSz = XSTRLEN(name); - if (object->onToken) - WP11_Lock_LockRW(object->lock); + XMEMSET(ctx, 0, sizeof(*ctx)); + ctx->file = XBADFILE; + ctx->is_write = (read == 0); + if (nameSz >= sizeof(ctx->final_name)) + ret = READ_ONLY_E; + else + XMEMCPY(ctx->final_name, name, nameSz + 1); - /* Import into a temporary key and swap it in only after it validates, so a - * failed re-import leaves the existing key unchanged (PKCS#11 requires a - * failed C_SetAttributeValue to be a no-op). */ - newKey = (XmssKey*)XMALLOC(sizeof(XmssKey), NULL, DYNAMIC_TYPE_KEY); - if (newKey == NULL) - ret = MEMORY_E; - if (ret == 0) { - XMEMSET(newKey, 0, sizeof(XmssKey)); - ret = wp11_XmssImportPub(newKey, (object->type == CKK_XMSSMT) ? 1 : 0, - object->devId, data[1], (word32)len[1], data[0], (word32)len[0]); + if (ret == 0 && read) { + ctx->file = XFOPEN(name, "rb"); + if (ctx->file == NULL) + ret = NOT_AVAILABLE_E; + } + else if (ret == 0) { + ret = wolfPKCS11_StoreDirName(name, dirPath, (int)sizeof(dirPath)); + if (ret == 0) + ret = wolfPKCS11_StoreEnsureDir(dirPath); + if (ret == 0) + ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); + } } + if (ret == 0 && (ctx->file == NULL || ctx->file == XBADFILE)) + ret = READ_ONLY_E; if (ret == 0) { - /* Replace the live key and drop the cached serialization so a later - * store re-encodes from the new key. */ - if (object->data.xmssKey != NULL) { - if (!object->encoded) - wc_XmssKey_Free(object->data.xmssKey); - XFREE(object->data.xmssKey, NULL, DYNAMIC_TYPE_KEY); - } - object->data.xmssKey = newKey; - object->encoded = 0; - newKey = NULL; - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; + *store = ctx; } - if (newKey != NULL) { - wc_XmssKey_Free(newKey); - XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); + else if (ctx != NULL) { + if (ctx->file != NULL && ctx->file != XBADFILE) + XFCLOSE(ctx->file); + if (ctx->has_temp) + wolfPKCS11_StoreAbortTemp(ctx); + XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); } - - if (object->onToken) - WP11_Lock_UnlockRW(object->lock); return ret; } -/* Verify an XMSS/XMSS^MT signature over a raw message. Stateless. */ -int WP11_Xmss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, - word32 dataLen, int* stat, WP11_Object* pub) +int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, + int tokenId, word64 stateId) { int ret; - word32 expSigLen = 0; - - if (pub == NULL || pub->data.xmssKey == NULL || stat == NULL) - return BAD_FUNC_ARG; - - /* Verification only reads the key: wc_XmssKey_Verify works from a locally - * allocated XmssState, so a shared RO lock is safe for concurrent verifies - * on the same token object. */ - if (pub->onToken) - WP11_Lock_LockRO(pub->lock); - - /* A wrong-length signature cannot be valid for this key; report it as an - * invalid signature rather than a function failure. wolfSSL's XMSS verify - * also rejects a zero-length message with BAD_FUNC_ARG; treat that the same - * way (an invalid verification, not an internal failure). */ - if (wc_XmssKey_GetSigLen(pub->data.xmssKey, &expSigLen) == 0 && - sigLen != expSigLen) { - *stat = 0; - ret = 0; - } - else if (dataLen == 0) { - *stat = 0; - ret = 0; - } - else { - ret = wc_XmssKey_Verify(pub->data.xmssKey, sig, sigLen, data, - (int)dataLen); - /* Only SIG_VERIFY_E maps to stat=0; other argument/state errors - * surface as a function failure so they are not silently reported as - * an invalid signature. */ - if (ret == 0) { - *stat = 1; - } - else if (ret == SIG_VERIFY_E) { - *stat = 0; - ret = 0; - } - else { - *stat = 0; - } - } - - if (pub->onToken) - WP11_Lock_UnlockRO(pub->lock); + char name[WP11_STORE_MAX_PATH]; - return ret; +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) + return NOT_AVAILABLE_E; +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + return remove(name); + return -1; } +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE */ -/* Serve XMSS-specific attributes from a public-key object. */ -static int XmssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, - byte* data, CK_ULONG* len) -{ - int ret = 0; - - if (object == NULL || object->data.xmssKey == NULL || len == NULL) - return BAD_FUNC_ARG; - - switch (type) { - case CKA_VALUE: - if (object->objClass == CKO_PUBLIC_KEY) { - word32 pubLen = 0; - ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); - if (ret == 0) { - if (data == NULL) - *len = pubLen; - else if (*len < pubLen) { - *len = pubLen; - ret = BUFFER_E; - } - else { - ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, - data, &pubLen); - if (ret == 0) - *len = pubLen; - } - } - } - else { - *len = CK_UNAVAILABLE_INFORMATION; - ret = NOT_AVAILABLE_E; - } - break; - case CKA_PARAMETER_SET: { - /* The XMSS parameter set is identified by the OID in the leading - * bytes of the raw public key; return it as a CK_ULONG. */ - CK_ULONG v; - word32 pubLen = 0; - byte* raw; - - if (object->objClass != CKO_PUBLIC_KEY) { - *len = CK_UNAVAILABLE_INFORMATION; - ret = NOT_AVAILABLE_E; - break; - } - if (data == NULL) { - *len = sizeof(v); - break; - } - if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - break; - } - ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); - if (ret == 0 && pubLen < WP11_XMSS_OID_LEN) - ret = NOT_AVAILABLE_E; - if (ret == 0) { - raw = (byte*)XMALLOC(pubLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (raw == NULL) { - ret = MEMORY_E; - } - else { - ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, raw, - &pubLen); - if (ret == 0) { - v = (CK_ULONG)wp11_HbsReadU32(raw); - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - XFREE(raw, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - } - break; - } - default: - ret = NOT_AVAILABLE_E; - break; - } - return ret; -} -#endif /* WOLFPKCS11_XMSS */ /** * Decode the symmetric key - requires decryption. @@ -8246,26 +5814,15 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) #endif #ifdef WOLFPKCS11_LMS case CKK_HSS: + storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; #ifdef WOLFPKCS11_LMS_PRIVATE if (object->objClass == CKO_PRIVATE_KEY) { - /* Remove the encrypted signature-state file too. Recover the - * per-key nonce from memory if decoded, else peek it from the - * on-disk shell (validating magic/version). Otherwise the - * state file would orphan on disk after C_DestroyObject. */ - word64 stateId = object->statefulStateId; - if (stateId == 0) { - (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, - &stateId); - } - if (stateId != 0) { - (void)wp11_Stateful_StateFile_Remove("hsskey", tokenId, - stateId); - } storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV; - break; + /* The signature-state file lives outside the object store and + * would orphan on disk after C_DestroyObject. */ + wp11_Hss_RemoveStoredState(object, tokenId, objId); } #endif - storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; break; #endif #ifdef WOLFPKCS11_XMSS diff --git a/src/wp11_types.h b/src/wp11_types.h new file mode 100644 index 00000000..0b8c5126 --- /dev/null +++ b/src/wp11_types.h @@ -0,0 +1,626 @@ +/* wp11_types.h + * + * Copyright (C) 2006-2023 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 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. + * + * wolfPKCS11 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 + */ + +/* Layout of the core wolfPKCS11 objects, private to src/. + * + * wolfpkcs11/internal.h is the installed header that the rest of the library + * and the test programs code against, and it keeps WP11_Object, WP11_Session, + * WP11_Token and WP11_Slot opaque on purpose. This header carries their actual + * layout, along with the helpers internal.c provides to its sibling source + * files. + * + * Include it only from the sources that genuinely need the layout (internal.c + * and hbs.c). crypto.c, slot.c and wolfpkcs11.c stay on the opaque interface, + * which is what stops them reaching into object internals. Never installed. + */ + +#ifndef WOLFPKCS11_WP11_TYPES_H +#define WOLFPKCS11_WP11_TYPES_H + +#ifdef HAVE_CONFIG_H + #include +#endif +#include "wolfpkcs11/pkcs11.h" + +#ifndef WOLFSSL_USER_SETTINGS +#include +#else +#include "user_settings.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef WOLFPKCS11_MLKEM +#include +#endif + +#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) +/* OS-specific includes for directory creation */ +#if defined(_WIN32) || defined(_MSC_VER) + #include + #include + #define MKDIR(path) _mkdir(path) +#else + #include + #define MKDIR(path) mkdir(path, 0700) +#endif +#endif + +#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) + #include + #include + #if defined(_WIN32) || defined(_MSC_VER) + #include + #include + #include + #include + #else + #include + #include + #include + #endif +#endif + +#include +#include + +#ifdef WOLFPKCS11_TPM + #include + #include + + #ifndef WOLFPKCS11_TPM_CUST_IO + #include + #ifndef TPM2_IOCB_CTX + #define TPM2_IOCB_CTX NULL + #endif + #endif +#endif + +#ifdef WOLFSSL_MAXQ10XX_CRYPTO +#include +#include +#define MAX_CERT_DATASIZE 2048 +#define MAX_SIG_DATASIZE 64 +#define ECC_KEYCOMPLEN 32 +#endif /* WOLFSSL_MAXQ10XX_CRYPTO */ + +#if defined(WC_RSA_BLINDING) && (!defined(HAVE_FIPS) || \ + (defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION > 2))) +#define WOLFPKCS11_NEED_RSA_RNG +#endif + +#if defined(WOLFPKCS11_TPM) && defined(WOLFSSL_MAXQ10XX_CRYPTO) + #error "wolfTPM and MAXQ10XX are incompatible with each other." +#endif + +/* Helper to get size of struct field */ +#define FIELD_SIZE(type, field) (sizeof(((type *)0)->field)) + +/* Size of hash calculated from PIN. */ +#define PIN_HASH_SZ 32 +/* Size of seed used when calculating hash from PIN. */ +#define PIN_SEED_SZ 16 +/* Size of token's label. */ +#define LABEL_SZ 32 + +/* Length of seed from global random to seed local random. */ +#define RNG_SEED_SZ 32 + + + +/* Maximum size of storage for generated/derived DH key. */ +#ifdef WOLFPKCS11_NSS +#define WP11_MAX_DH_KEY_SZ (8192/8) +#else +#define WP11_MAX_DH_KEY_SZ (4096/8) +#endif + +/* Maximum size of storage for generated/derived symmetric key. */ +#ifdef WOLFPKCS11_NSS +#define WP11_MAX_SYM_KEY_SZ (2048) +#elif !defined(NO_DH) +#define WP11_MAX_SYM_KEY_SZ (4096/8) +#elif defined(HAVE_ECC) +#define WP11_MAX_SYM_KEY_SZ ((521+7)/8) +#else +#define WP11_MAX_SYM_KEY_SZ 64 +#endif + +#ifndef WP11_MAX_CERT_SZ +#define WP11_MAX_CERT_SZ 4096 +#endif + +#define PKCS11_CHECK_VALUE_SIZE 3 + +/* Sizes for storage. */ +#define WP11_MAX_IV_SZ 16 +#define WP11_MAX_GCM_NONCE_SZ 16 +#define WP11_MAX_GCM_TAG_SZ 16 +#define WP11_MAX_GCM_TAG_BITS 128 + +/* wolfSSL ECC signatures are ASN.1 encoding - need to encode and decode. */ +#define ASN_INTEGER 0x02 +#define ASN_OCTET_STRING 0x04 +#define ASN_OBJECT_ID 0x06 +#define ASN_SEQUENCE 0x10 +#define ASN_CONSTRUCTED 0x20 +#define ASN_LONG_LENGTH 0x80 + +/* Create a session handle from slot id and session id. */ +#define SESS_HANDLE(slot, s) (((slot) << 16) | (s)) +/* Determine slot id from session handle. */ +#define SESS_HANDLE_SLOT_ID(s) ((CK_SLOT_ID)((s) >> 16)) +/* Determine session id from session handle. */ +#define SESS_HANDLE_SESS_ID(s) ((s) & 0xffff) + +/* Create an object handle from a onToken and object id. */ +#define OBJ_HANDLE(on, i) (((on) << 28) | (i)) +/* Determine whether object is onToken from object handle. */ +#define OBJ_HANDLE_ON_TOKEN(h) ((int)((h) >> 28)) +/* Determine object id from object handle. */ +#define OBJ_HANDLE_OBJ_ID(h) ((h) & 0xfffffff) + +#ifdef SINGLE_THREADED +/* Disable locking. */ +typedef int WP11_Lock; + +#define WP11_Lock_Init(l) ({ 0; }) +#define WP11_Lock_Free(l) +#define WP11_Lock_LockRW(l) ({ 0; }) +#define WP11_Lock_UnlockRW(l) ({ 0; }) +#define WP11_Lock_LockRO(l) ({ 0; }) +#define WP11_Lock_UnlockRO(l) ({ 0; }) + +#else +typedef struct WP11_Lock { + wolfSSL_Mutex read; /* Mutex for accessing count */ + wolfSSL_Mutex write; /* Mutex for writing */ + int cnt; /* Count of readers */ +} WP11_Lock; +#endif + +/* Symmetric key data. */ +typedef struct WP11_Data { + byte data[WP11_MAX_SYM_KEY_SZ]; /* Key data */ + word32 len; /* Length of key data in bytes */ +} WP11_Data; + +typedef struct WP11_GenericData { + byte *data; + word32 dataLen; + byte *application; + word32 applicationLen; + byte *objectId; /* CKA_OBJECT_ID, a DER encoded ID */ + word32 objectIdLen; +} WP11_GenericData; + +/* Certificate */ +typedef struct WP11_Cert { + byte *data; /* Certificate data */ + word32 len; /* Length of certificate data in bytes */ + CK_CERTIFICATE_TYPE type; +} WP11_Cert; + +typedef struct WP11_Trust { + byte sha1Hash[WC_SHA_DIGEST_SIZE]; + byte md5Hash[WC_MD5_DIGEST_SIZE]; + CK_ULONG serverAuth; + CK_ULONG clientAuth; + CK_ULONG emailProtection; + CK_ULONG codeSigning; + CK_BBOOL stepUpApproved; +} WP11_Trust; + +#ifndef NO_DH +typedef struct WP11_DhKey { + byte key[WP11_MAX_DH_KEY_SZ]; /* Public or private key */ + word32 len; /* Length of public key */ + DhKey params; /* DH parameters object */ +} WP11_DhKey; +#endif + +/* When adding/modifying new members, add support in WP11_Object_Copy */ +struct WP11_Object { + union { + #ifndef NO_RSA + RsaKey* rsaKey; /* RSA key object */ + #endif + #ifdef HAVE_ECC + ecc_key* ecKey; /* EC key object */ + #endif + #ifdef WOLFPKCS11_MLDSA + wc_MlDsaKey* mldsaKey; /* ML-DSA key object */ + #endif + #ifndef NO_DH + WP11_DhKey* dhKey; /* DH parameters object */ + #endif + #ifdef WOLFPKCS11_MLKEM + MlKemKey* mlKemKey; /* ML-KEM key object */ + #endif + #ifdef WOLFPKCS11_LMS + LmsKey* lmsKey; /* LMS/HSS key object (verify) */ + #endif + #ifdef WOLFPKCS11_XMSS + XmssKey* xmssKey; /* XMSS/XMSS^MT key object (verify) */ + #endif + WP11_Data* symmKey; /* Symmetric key object */ + WP11_GenericData genericData; /* Generic data object */ + WP11_Cert cert; /* Certificate object */ + #ifdef WOLFPKCS11_NSS + WP11_Trust trust; /* Trust object */ + #endif + } data; +#ifdef WOLFPKCS11_TPM + WOLFTPM2_KEYBLOB* tpmKey; +#endif +#ifdef WOLFSSL_STM32U5_DHUK + unsigned char* dhukIv; /* IV used with wrapping and unwrapping AES key. */ + int dhukIvLen; +#endif + CK_KEY_TYPE type; /* Key type of this object */ + word32 size; /* Size of the key in bits or bytes */ +#ifndef WOLFPKCS11_NO_STORE + unsigned char* keyData; /* Encoded key data */ + int keyDataLen; /* Length of encoded key data */ + byte iv[GCM_NONCE_MID_SZ]; /* IV/nonce for encrypt/decrypt */ + byte encoded:1; /* Key isn't in decoded form */ +#endif + + WP11_Session* session; /* Session object belongs to */ + WP11_Slot* slot; /* Slot object belongs to */ + + CK_OBJECT_HANDLE handle; /* Handle of this object */ + CK_OBJECT_CLASS objClass; /* Object class */ + CK_MECHANISM_TYPE keyGenMech; /* Key Gen mechanism created with */ + byte onToken:1; /* Object on token or session */ + byte local:1; /* Locally created object */ + word32 opFlag; /* Flags of operations allowed */ +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + word64 statefulStateId; /* Non-zero nonce keying the durable */ + /* signature-state file (0 = none) */ + word64 statefulSigCount; /* Signatures produced so far (bound */ + /* into the state-file AAD) */ +#endif + + char startDate[8]; /* Start date of usage */ + char endDate[8]; /* End data of usage */ + + unsigned char* keyId; /* Key identifier */ + int keyIdLen; /* Length of key identifier */ + unsigned char* label; /* Object label */ + int labelLen; /* Length of object label */ + unsigned char* issuer; /* Certificate issuer */ + int issuerLen; /* Length of certificate issuer */ + unsigned char* serial; /* Certificate serial number */ + int serialLen; /* Length of certificate serial number */ + unsigned char* subject; /* Subject of the object */ + int subjectLen; /* Length of subject */ +#ifdef WOLFPKCS11_NSS + unsigned char* email; /* Certificate email (NSS extension) */ + int emailLen; /* Length of certificate email */ +#endif + + word32 category; /* Category of certificate */ + + WP11_Lock* lock; /* Object specific lock */ + + int devId; + WP11_Object* next; /* Next object in linked list */ +}; + +typedef struct WP11_Find { + int state; /* Whether operation is initialized */ + CK_OBJECT_HANDLE found[WP11_FIND_MAX]; + /* List of object handles found */ + int count; /* Count of object handles */ + int curr; /* Index of last object returned */ +} WP11_Find; + +#ifndef NO_RSA +#ifndef WC_NO_RSA_OAEP +typedef struct WP11_OaepParams { + enum wc_HashType hashType; /* Type of hash algorithm */ + int mgf; /* Mask Generation Function */ + byte* label; /* Label or AAD */ + int labelSz; /* Size of label in bytes */ +} WP11_OaepParams; +#endif + +#ifdef WC_RSA_PSS +typedef struct WP11_PssParams { + enum wc_HashType hashType; + int mgf; + int saltLen; +} WP11_PssParams; +#endif +#endif + +#ifdef WOLFPKCS11_MLDSA +typedef struct WP11_MldsaParams { + enum wc_HashType preHashType; + word32 hedgeType; + byte ctx[256]; + byte ctxSz; +} WP11_MldsaParams; +#endif + +#ifndef NO_AES +#ifdef HAVE_AES_CBC +typedef struct WP11_CbcParams { + unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ + Aes aes; /* AES object from wolfCrypt */ + unsigned char partial[AES_BLOCK_SIZE]; + /* Partial block when streaming */ + byte partialSz; /* Size of partial block data */ +} WP11_CbcParams; +#endif + +#ifdef HAVE_AESCTR +typedef struct WP11_CtrParams { + Aes aes; /* AES object from wolfCrypt */ +} WP11_CtrParams; +#endif + +#ifdef HAVE_AESGCM +typedef struct WP11_GcmParams { + unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; + /* IV/nonce data */ + int ivSz; /* IV/nonce size in bytes */ + unsigned char* aad; /* Additional Authentication Data */ + int aadSz; /* AAD size in bytes */ + int tagBits; /* Authentication tag size in bits */ + unsigned char authTag[WP11_MAX_GCM_TAG_SZ]; + /* Authentication tag calculated */ + unsigned char* enc; /* Cached data for multi-part: buffered */ + /* ciphertext (decrypt) or, without */ + /* streaming GCM, plaintext (encrypt) */ + int encSz; /* Size of cached data in bytes */ +#ifdef WOLFSSL_AESGCM_STREAM + Aes aes; /* Streaming context for multi-part */ + /* encrypt */ + int streamInit; /* Streaming context is initialized */ +#endif +} WP11_GcmParams; +#endif + +#ifdef HAVE_AESCCM +typedef struct WP11_CcmParams { + int dataSz; /* Data size in bytes */ + unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; + /* IV/nonce data */ + int ivSz; /* IV/nonce size in bytes */ + unsigned char* aad; /* Additional Authentication Data */ + int aadSz; /* AAD size in bytes */ + int macSz; /* Size of MAC data in bytes */ +} WP11_CcmParams; +#endif + +#ifdef HAVE_AES_KEYWRAP +typedef struct WP11_KeyWrapParams { + Aes aes; /* AES object from wolfCrypt */ + unsigned char iv[8]; /* IV/nonce data */ + word32 ivSz; /* IV/nonce size in bytes */ +} WP11_KeyWrapParams; +#endif + +#ifdef HAVE_AESCTS +typedef struct WP11_CtsParams { + unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ + Aes aes; /* AES object from wolfCrypt */ +} WP11_CtsParams; +#endif +#endif + +#ifndef NO_HMAC +typedef struct WP11_Hmac { + Hmac hmac; + word32 hmacSz; +} WP11_Hmac; +#endif + +typedef struct WP11_Digest { + wc_HashAlg hash; + enum wc_HashType hashType; +} WP11_Digest; + +#ifdef HAVE_AESCMAC +typedef struct WP11_Cmac { + Cmac cmac; + byte sigLen; +} WP11_Cmac; +#endif + +typedef struct WP11_TlsMacParams { + enum wc_MACAlgorithm mac; + word32 macSz; + CK_OBJECT_HANDLE key; + byte server:1; + byte isTlsPrf:1; +} WP11_TlsMacParams; + +struct WP11_Session { + unsigned char inUse; /* Indicates session has been opened */ + CK_SESSION_HANDLE handle; /* CryptoKi API session handle value */ + CK_MECHANISM_TYPE mechanism; /* Op that is being performed */ + CK_SLOT_ID slotId; /* Id of slot that session is on */ + WP11_Slot* slot; /* Slot that session is on */ + WP11_Object* object; /* Linked list of objects on session */ + int objCnt; /* Count of objects in session */ + WP11_Object* curr; /* Current object */ + WP11_Find find; /* Find data */ + int init; /* Which op is initialized */ + union { +#ifndef NO_RSA + #ifndef WC_NO_RSA_OAEP + WP11_OaepParams oaep; /* RSA-OAEP parameters */ + #endif + #ifdef WC_RSA_PSS + WP11_PssParams pss; /* RSA-PSS parameters */ + #endif +#endif +#ifdef WOLFPKCS11_MLDSA + WP11_MldsaParams mldsa; /* ML-DSA parameters */ +#endif +#ifndef NO_AES + #ifdef HAVE_AES_CBC + WP11_CbcParams cbc; /* AES-CBC parameters */ + #endif + #ifdef HAVE_AESCTR + WP11_CtrParams ctr; /* AES-CTR parameters */ + #endif + #ifdef HAVE_AESGCM + WP11_GcmParams gcm; /* AES-GCM parameters */ + #endif + #ifdef HAVE_AESCCM + WP11_CcmParams ccm; /* AES-CCM parameters */ + #endif + #ifdef HAVE_AESCTS + WP11_CtsParams cts; /* AES-CTS parameters */ + #endif + #ifdef HAVE_AES_KEYWRAP + WP11_KeyWrapParams kw; /* AES-KW parameters */ + #endif +#endif +#ifndef NO_HMAC + WP11_Hmac hmac; /* HMAC parameters */ +#endif + WP11_Digest digest; /* Digest parameters */ +#ifdef HAVE_AESCMAC + WP11_Cmac cmac; /* CMAC parameters */ +#endif + WP11_TlsMacParams tlsMac; /* TLS MAC parameters */ + } params; + + byte* data; /* Held data for one-shot mechs */ + word32 dataSz; /* Size of held data */ + + int devId; + WP11_Session* next; /* Next session for slot */ +}; + +typedef struct WP11_Token { + char label[LABEL_SZ]; /* Token label */ + int state; /* Token initialize state */ + byte soPin[PIN_HASH_SZ]; /* SO's PIN hashed with seed */ + int soPinLen; /* Used to indicate PIN set */ + byte soPinSeed[PIN_SEED_SZ]; /* Seed for calculating SO's PIN */ + int soFailedLogin; /* Count of consecutive failed logins */ + time_t soLastFailedLogin; /* Time of last login if it failed */ + time_t soFailLoginTimeout; /* Timeout after max login fails */ + byte userPin[PIN_HASH_SZ]; /* User's PIN hashed with seed */ + int userPinLen; /* Used to indicate PIN set */ + byte userPinSeed[PIN_SEED_SZ]; /* Seed for calculating user's PIN */ + int userFailedLogin; /* Count of consecutive failed logins */ + time_t userLastFailedLogin; /* Time of last login if it failed */ + time_t userFailLoginTimeout; /* Timeout after max login fails */ +#ifndef WOLFPKCS11_NO_STORE + byte seed[PIN_SEED_SZ]; /* Seed used to calculate key */ + byte key[AES_256_KEY_SIZE]; /* Key to en/decrypt private data */ +#endif + WC_RNG rng; /* Random number generator */ + WP11_Lock rngLock; /* Lock for random access */ + WP11_Lock lock; /* Lock for object access */ + int loginState; /* Login state of the token */ + WP11_Object* object; /* Linked list of token objects */ + int objCnt; /* Count of objects on token */ + int tokenFlags; /* Flags for token */ + int nextObjId; + byte userPinEmpty:2; /* Indicates user PIN is empty + * 0 = not set + * 1 = empty + * 2 = not empty */ +} WP11_Token; + +struct WP11_Slot { + CK_SLOT_ID id; /* CryptoKi API slot id value */ + WP11_Token token; /* Token information for slot */ + WP11_Session* session; /* Linked list of sessions */ + WP11_Lock lock; /* Lock for access to slot info */ + + int devId; +#ifdef WOLFPKCS11_TPM + WOLFTPM2_DEV tpmDev; + WOLFTPM2_KEY tpmSrk; + WOLFTPM2_SESSION tpmSession; + TpmCryptoDevCtx tpmCtx; +#endif +}; + +/* Helpers implemented in internal.c and used by its sibling source files. + * Not part of the public or the wolfpkcs11/internal.h interface. */ + +#ifndef SINGLE_THREADED +WP11_LOCAL int WP11_Lock_LockRW(WP11_Lock* lock); +WP11_LOCAL int WP11_Lock_UnlockRW(WP11_Lock* lock); +WP11_LOCAL int WP11_Lock_LockRO(WP11_Lock* lock); +WP11_LOCAL int WP11_Lock_UnlockRO(WP11_Lock* lock); +#endif + +WP11_LOCAL int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng); +WP11_LOCAL void Rng_Free(WC_RNG* rng); + +#ifndef WOLFPKCS11_NO_STORE +WP11_LOCAL int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, + void** storage); +WP11_LOCAL int wp11_storage_open(int type, CK_ULONG id1, CK_ULONG id2, + int variableSz, void** storage); +WP11_LOCAL void wp11_storage_close(void* storage); +WP11_LOCAL int wp11_storage_read(void* storage, unsigned char* buffer, int len); +WP11_LOCAL int wp11_storage_write(void* storage, unsigned char* buffer, + int len); +WP11_LOCAL int wp11_storage_read_alloc_array(void* storage, + unsigned char** buffer, int* len); +WP11_LOCAL int wp11_storage_write_array(void* storage, unsigned char* buffer, + word32 len); + +#ifndef WOLFPKCS11_CUSTOM_STORE +/* Report the result of the close-and-commit rather than discarding it. */ +WP11_LOCAL int wolfPKCS11_Store_CloseAndReport(void* store); +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +#ifndef WOLFPKCS11_TPM_STORE +/* Ask the file store to fsync the data and its parent directory before the + * atomic rename commits. */ +WP11_LOCAL void wolfPKCS11_Store_SetDurable(void* store, int durable); +#endif + +/* Per-key state files for stateful hash-based signature schemes. Keyed by a + * 64-bit nonce rather than the CK_ULONG ids the wolfPKCS11_Store_* API takes, + * which would truncate to 32 bits on LLP64 platforms. */ +WP11_LOCAL int wp11_Stateful_StateFile_Open(const char* schemeFilePrefix, + int tokenId, word64 stateId, + int read, void** store); +WP11_LOCAL int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, + int tokenId, word64 stateId); +#endif +#endif /* !WOLFPKCS11_CUSTOM_STORE */ +#endif /* !WOLFPKCS11_NO_STORE */ + +#endif /* WOLFPKCS11_WP11_TYPES_H */ diff --git a/tests/hbs_private_test.c b/tests/hbs_private_test.c index 259d0ddd..fbc3eb25 100644 --- a/tests/hbs_private_test.c +++ b/tests/hbs_private_test.c @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA * - * Functional test for the EXPERIMENTAL sign-capable LMS/HSS build + * Functional test for the sign-capable LMS/HSS build * (--enable-lms-private / WOLFPKCS11_LMS_PRIVATE). Exercises the paths that * the verify-only tests cannot reach: * diff --git a/tests/hss_concurrency_test.c b/tests/hss_concurrency_test.c index 9bde95e8..7799ee68 100644 --- a/tests/hss_concurrency_test.c +++ b/tests/hss_concurrency_test.c @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA * - * Concurrency test for the EXPERIMENTAL sign-capable LMS/HSS build + * Concurrency test for the sign-capable LMS/HSS build * (--enable-lms-private). The one catastrophic failure mode of a stateful * hash-based scheme is two signers using the same one-time-signature (OTS) * leaf index. This test drives many threads signing concurrently with a single diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index bd668380..550ac4d7 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -267,12 +267,10 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_FLAG_NOT_COPYABLE 0x00200000 #define WP11_FLAG_NOT_DESTROYABLE 0x00400000 #define WP11_FLAG_NOT_MODIFIABLE 0x00800000 -#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE /* Set when a stateful private key's one-time-signature state is live and * usable; cleared ("poisoned") on any keygen/sign/reload failure so a * subsequent sign returns CKR_DEVICE_ERROR instead of risking OTS reuse. */ #define WP11_FLAG_STATEFUL_STATE_VALID 0x01000000 -#endif /* Flags for token. */ #define WP11_TOKEN_FLAG_USER_PIN_SET 0x00000001 From 9b884ed631e0e6b2282b780ab377891135d2712c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 29 Jul 2026 09:08:52 +0200 Subject: [PATCH 16/16] LMS/HSS: fix state-file loss, login lockout and a private-key leak Four defects in the stateful private-key work, each with a regression test. Destroying any token object renumbers the ones that outlive it, and wp11_Object_Unstore is what the renumbering loop calls. It also removed the signature-state file, which is keyed by the per-key nonce rather than the object id, so re-storing the renumbered object never brought it back. Since C_GenerateKeyPair adds the public key first and WP11_Session_AddObject prepends, destroying the public key walked the private key through that loop and permanently destroyed its state. Unstore now takes a destroying flag and only drops the state file when the object really is going away. wc_LmsKey_Reload rejects a key with no signatures left, and exhaustion is the normal end of life for every LMS key. The decode loops behind C_Login and the empty-PIN token load abort on the first error, so a spent key made the whole token unusable, including the unrelated RSA, ECC and AES objects on it. An HSS private key that cannot be reloaded is now non-fatal: it stays poisoned, and the loop continues. The key itself is skipped by C_FindObjects, the same as any other object that could not be decoded; README documents that as a known limitation, since reporting it instead would mean quarantining objects in the shared decode path for every key type. The CKK_HSS arm of wp11_Object_Encode ignored the protect flag, unlike every other private-key type. C_Logout therefore left the live signing key, holding the LMS private seed and the one-time-signature state, allocated and un-zeroized. The next C_Login re-Init'd it, and wc_LmsKey_Init only memsets the handle, leaking the heap-allocated priv_data on every cycle. It now calls wc_LmsKey_Free and sets encoded, matching the RSA/ECC/ML-DSA arms. The durable close discarded the results of XFFLUSH and XFCLOSE. fsync only pushes what the kernel already holds, so bytes left in the stdio buffer were invisible to it: on ENOSPC the flush failed, the fsync of an empty queue succeeded, and the commit reported success while the state file was short. Both results now feed the failure path. A failed flush also no longer commits the rename, which used to replace a known-durable state file with one that may never have reached the disk. Also: - Register WP11_INIT_HSS_SIGN as a sign-class operation. Without it a second C_SignInit(CKM_HSS) silently replaced the in-flight stateful operation instead of returning CKR_OPERATION_ACTIVE. - Reject WOLFPKCS11_LMS_PRIVATE with WOLFPKCS11_CUSTOM_STORE. The durable state file needs the built-in file store, and the combination previously failed deep in the compile of hbs.c. - Build and run hbs_private_test and hss_concurrency_test under CMake. The CMake lane enabled the feature but ran neither of its tests. - Require CKA_VALUE for a CKK_HSS public key created through C_CreateObject. Keygen builds both objects empty and fills them in, but C_CreateObject has no such second step and was handing back permanently unusable objects. - Only require CKA_TOKEN=TRUE on the private half of an HSS key pair. A public key holds no state, so demanding it there was stricter than any other keygen mechanism for no safety gain. - Detect a verify-only wolfSSL at configure time rather than in the middle of compiling hbs.c, and note the coupling on the reusable workflow input, whose wolfssl_lms default is verify-only. - Resolve WOLFPKCS11_STATEFUL_RELAX_FSYNC during library init. Read lazily, it was an unsynchronised static write shared by signing threads on different slots, which ThreadSanitizer reports. - Mirror the write path's parameter validation in wp11_Stateful_ReadStateBlob, drop a dead expression statement, and rename the two new internal helpers to the wp11_storage_* prefix so the wolfPKCS11_Store_* namespace stays the documented backend interface. - Skip the chmod-based write-failure injection under root, which bypasses the permission check the test depends on, and restore the directory's original mode instead of a hard-coded 0700. - Keep expected-failure paths quiet so a passing run no longer writes "FAIL" into test-suite.log, and use ASCII hyphens in the new CMake messages. Verified with make check at 51 pass / 2 skip / 0 fail, the autotools configuration matrix, CMake with ctest -j, and ASan plus UBSan over hbs_private_test, hss_concurrency_test, pkcs11v3test and pkcs11test. --- .github/workflows/build-workflow.yml | 3 + CMakeLists.txt | 30 ++- README.md | 16 ++ configure.ac | 19 ++ src/crypto.c | 37 ++- src/hbs.c | 60 ++--- src/hbs.h | 6 + src/internal.c | 123 +++++++-- src/wp11_types.h | 4 +- tests/hbs_private_test.c | 358 +++++++++++++++++++++++++-- tests/pkcs11v3test.c | 19 +- wolfpkcs11/internal.h | 3 + 12 files changed, 579 insertions(+), 99 deletions(-) diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index 535ebf2a..a623ea2b 100644 --- a/.github/workflows/build-workflow.yml +++ b/.github/workflows/build-workflow.yml @@ -12,6 +12,9 @@ on: type: string default: 'make check' wolfssl_lms: + # Callers passing --enable-lms-private in `config` must also set this + # to 'yes': wolfPKCS11 keygen and signing need a sign-capable wolfSSL + # LMS build, and its configure now fails outright on a verify-only one. required: false type: string default: 'verify-only' diff --git a/CMakeLists.txt b/CMakeLists.txt index deffadc6..cb6a836c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -474,7 +474,7 @@ endif() if(WOLFPKCS11_MLDSA) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "ML-DSA requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "ML-DSA requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -498,7 +498,7 @@ endif() if(WOLFPKCS11_MLKEM) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "ML-KEM requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "ML-KEM requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -522,7 +522,7 @@ endif() if(WOLFPKCS11_LMS) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "LMS/HSS requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "LMS/HSS requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -548,7 +548,7 @@ if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_SHA256) endif() if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_LMS) - message(STATUS "WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS — enabling WOLFPKCS11_LMS automatically") + message(STATUS "WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS - enabling WOLFPKCS11_LMS automatically") override_cache(WOLFPKCS11_LMS "yes") if(NOT WOLFPKCS11_PKCS11_V3_2) override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") @@ -578,7 +578,7 @@ endif() if(WOLFPKCS11_XMSS) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "XMSS requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "XMSS requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -818,6 +818,26 @@ if(WOLFPKCS11_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/tests/hbs_persistence_test.c) set_tests_properties(hbs_persistence_test PROPERTIES SKIP_RETURN_CODE 77) endif() + + # LMS/HSS keygen and signing tests. Both exit 77 (skip) when the feature is + # compiled out, so they can be registered unconditionally like the test + # above and still behave in a verify-only build. + # + # Each gets its own token directory. They wipe the store between sub-tests + # to start from a known state, which would clobber the other tests running + # beside them under `ctest -j`. + add_wpkcs11_test(hbs_private_test + ${CMAKE_CURRENT_SOURCE_DIR}/tests/hbs_private_test.c) + set_tests_properties(hbs_private_test PROPERTIES + SKIP_RETURN_CODE 77 + ENVIRONMENT + "WOLFPKCS11_TOKEN_PATH=${CMAKE_CURRENT_BINARY_DIR}/store-hbs-private") + add_wpkcs11_test(hss_concurrency_test + ${CMAKE_CURRENT_SOURCE_DIR}/tests/hss_concurrency_test.c) + set_tests_properties(hss_concurrency_test PROPERTIES + SKIP_RETURN_CODE 77 + ENVIRONMENT + "WOLFPKCS11_TOKEN_PATH=${CMAKE_CURRENT_BINARY_DIR}/store-hss-concurrency") endif() if(WOLFPKCS11_EXAMPLES) diff --git a/README.md b/README.md index d89d84a5..94f79dd2 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,22 @@ HSS signing is not secret-input timing-sensitive the way an RSA/ECC private scalar is (the message and the tree position are not confidential), so no constant-time treatment beyond the existing token-key handling is required. +#### Known limitation: a spent or damaged key is hidden, not reportable + +An HSS private key whose state cannot be reloaded - because the tree is +exhausted, or because the state file was damaged - stays flagged as undecoded, +which is how this library already treats any object it cannot decode for the +current user. Such an object is skipped by `C_FindObjects`. + +The token itself keeps working: `C_Login` succeeds and every other object on it +stays usable. But the spent key is not enumerable, so it cannot be handed to +`C_DestroyObject`; its object and state files have to be removed from the token +directory by hand. + +Reporting the key instead of hiding it means quarantining objects individually +in the shared decode-at-login path, which affects every key type. That is +planned as a separate change. + ### Build options and defines #### Define WOLFPKCS11_TPM_STORE diff --git a/configure.ac b/configure.ac index ac0242b1..be0d84cf 100644 --- a/configure.ac +++ b/configure.ac @@ -630,8 +630,27 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS" fi +# A verify-only wolfSSL LMS build has no wc_LmsKey_MakeKey/_Sign, which would +# otherwise surface as a pile of errors deep in the compile of src/hbs.c. Catch +# it here instead. WOLFSSL_LMS_VERIFY_ONLY is only visible when the installed +# headers expose it, so probe the symbol rather than the macro. if test "$ENABLED_LMS_PRIVATE" = "yes" then + AC_MSG_CHECKING([for a sign-capable wolfSSL LMS build]) + SAVED_LIBS="$LIBS" + LIBS="$LIBS -lwolfssl" + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[#include + #include + #include ]], + [[LmsKey* k = (LmsKey*)0; WC_RNG* r = (WC_RNG*)0; + return wc_LmsKey_MakeKey(k, r);]])], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([--enable-lms-private needs a sign-capable wolfSSL LMS build; wolfSSL configured with --enable-lms=verify-only provides verification only. Rebuild wolfSSL with --enable-lms=yes.])]) + LIBS="$SAVED_LIBS" + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS_PRIVATE" fi diff --git a/src/crypto.c b/src/crypto.c index 799f3ec4..aad2628c 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -1516,6 +1516,23 @@ static CK_RV CreateObject(WP11_Session* session, CK_ATTRIBUTE_PTR pTemplate, } } +#ifdef WOLFPKCS11_LMS_PRIVATE + /* A sign-capable build lets an HSS key object be created empty, because + * C_GenerateKeyPair builds both objects from templates that carry no key + * material and fills them in afterwards. C_CreateObject has no such second + * step, so an HSS public key imported without CKA_VALUE would be a + * permanently unusable object whose first C_Verify fails inside wolfSSL. + * Require the value here, matching what a verify-only build enforces in + * WP11_Object_SetHssKey. */ + if (objType == CKK_HSS && objectClass == CKO_PUBLIC_KEY) { + CK_ATTRIBUTE_PTR value = NULL; + + FindAttributeType(pTemplate, ulCount, CKA_VALUE, &value); + if (value == NULL || value->pValue == NULL || value->ulValueLen == 0) + return CKR_ATTRIBUTE_VALUE_INVALID; + } +#endif + rv = NewObject(session, objType, objectClass, pTemplate, ulCount, object); return rv; @@ -7861,18 +7878,17 @@ static int HssTemplateIsToken(CK_ATTRIBUTE_PTR pTemplate, CK_ULONG ulCount) *(CK_BBOOL*)attr->pValue == CK_TRUE; } -/* Stateful keys must be token-resident: a session-only key that vanishes on a - * crash could release a signature whose one-time index was never persisted. - * Both arms of the key pair must ask for CKA_TOKEN=TRUE explicitly. */ -static CK_RV HssKeyPairGenCheckToken(CK_ATTRIBUTE_PTR pPublicKeyTemplate, - CK_ULONG ulPublicKeyAttributeCount, - CK_ATTRIBUTE_PTR pPrivateKeyTemplate, +/* The private key must be token-resident: a session-only key that vanishes on + * a crash could release a signature whose one-time index was never persisted, + * and the durable state file is only written for token objects. A public key + * holds no state, so it may be session-scoped like any other public key - + * requiring CKA_TOKEN on that arm too would be stricter than every other + * keygen mechanism here, for no safety gain. */ +static CK_RV HssKeyPairGenCheckToken(CK_ATTRIBUTE_PTR pPrivateKeyTemplate, CK_ULONG ulPrivateKeyAttributeCount) { - if (!HssTemplateIsToken(pPublicKeyTemplate, ulPublicKeyAttributeCount) || - !HssTemplateIsToken(pPrivateKeyTemplate, ulPrivateKeyAttributeCount)) { + if (!HssTemplateIsToken(pPrivateKeyTemplate, ulPrivateKeyAttributeCount)) return CKR_TEMPLATE_INCONSISTENT; - } return CKR_OK; } @@ -8075,8 +8091,7 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, pMechanism->ulParameterLen != 0) { return CKR_MECHANISM_PARAM_INVALID; } - rv = HssKeyPairGenCheckToken(pPublicKeyTemplate, - ulPublicKeyAttributeCount, pPrivateKeyTemplate, + rv = HssKeyPairGenCheckToken(pPrivateKeyTemplate, ulPrivateKeyAttributeCount); if (rv != CKR_OK) return rv; diff --git a/src/hbs.c b/src/hbs.c index ba9444e0..8d5e279c 100644 --- a/src/hbs.c +++ b/src/hbs.c @@ -178,32 +178,40 @@ static void wp11_Stateful_WriteU64(byte* p, word64 v) * standard IV; same for every scheme. */ #define WP11_STATEFUL_IV_LEN 12 -/* Cached env-var read once at first use: when 1, the state-write callback - * skips fsync() (file and directory). DANGEROUS: documented as non-production - * only. Default is to always fsync. The variable applies to ALL stateful - * hash-based signature schemes (LMS/HSS today; XMSS in the future) - there - * is no per-scheme knob, since the durability requirement is identical. */ -static int wp11_StatefulRelaxFsync = -1; - -static int wp11_StatefulShouldFsync(void) +/* When 1, the state-write callback skips fsync() of the file and its parent + * directory. DANGEROUS: documented as non-production only. Default is to + * always fsync. The setting applies to ALL stateful hash-based signature + * schemes (LMS/HSS today; XMSS in the future) - there is no per-scheme knob, + * since the durability requirement is identical. + * + * Resolved once from WP11_Stateful_InitRelaxFsync() during library init, not + * lazily on first use: signing threads on different slots hold different token + * locks, so a lazy initialiser here would be an unsynchronised write to this + * static, which ThreadSanitizer reports and which no lock in the sign path + * covers. */ +static int wp11_StatefulRelaxFsync = 0; + +void WP11_Stateful_InitRelaxFsync(void) { - if (wp11_StatefulRelaxFsync < 0) { #ifndef WOLFPKCS11_NO_ENV - const char* v = XGETENV("WOLFPKCS11_STATEFUL_RELAX_FSYNC"); - wp11_StatefulRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; + const char* v = XGETENV("WOLFPKCS11_STATEFUL_RELAX_FSYNC"); + wp11_StatefulRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; #else - wp11_StatefulRelaxFsync = 0; + wp11_StatefulRelaxFsync = 0; #endif - if (wp11_StatefulRelaxFsync) { - /* Single, prominent stderr line - the README documents this is - * non-production. Operators who reach this path must see it. */ - fprintf(stderr, - "wolfPKCS11: WARNING: WOLFPKCS11_STATEFUL_RELAX_FSYNC=1 is " - "set. Stateful hash-signature state writes will skip fsync; " - "a power loss can roll back the leaf-index advance and cause " - "OTS-key REUSE. Do NOT use this setting in production.\n"); - } + if (wp11_StatefulRelaxFsync) { + /* Single, prominent stderr line - the README documents this is + * non-production. Operators who reach this path must see it. */ + fprintf(stderr, + "wolfPKCS11: WARNING: WOLFPKCS11_STATEFUL_RELAX_FSYNC=1 is " + "set. Stateful hash-signature state writes will skip fsync; " + "a power loss can roll back the leaf-index advance and cause " + "OTS-key REUSE. Do NOT use this setting in production.\n"); } +} + +static int wp11_StatefulShouldFsync(void) +{ return wp11_StatefulRelaxFsync ? 0 : 1; } @@ -292,16 +300,12 @@ static int wp11_Stateful_WriteStateBlob(WP11_Object* o, return ret; } - /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag - - * unused for file storage, but kept for parity with TPM_STORE. */ - (void)(hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen); - tokenId = (int)o->slot->id; ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, o->statefulStateId, 0, &storage); if (ret == 0) { if (wp11_StatefulShouldFsync()) - wolfPKCS11_Store_SetDurable(storage, 1); + wp11_storage_set_durable(storage, 1); ret = wp11_storage_write(storage, hdr, (int)hdrLen); if (ret == 0) { byte ivLenBuf[4]; @@ -321,7 +325,7 @@ static int wp11_Stateful_WriteStateBlob(WP11_Object* o, * a failed rename/fsync; that would let wolfSSL release a signature * whose state advance never reached durable storage. */ { - int closeRet = wolfPKCS11_Store_CloseAndReport(storage); + int closeRet = wp11_storage_close_report(storage); if (ret == 0) ret = closeRet; } @@ -354,7 +358,7 @@ static int wp11_Stateful_ReadStateBlob(WP11_Object* o, word64 sigCount = 0; int tokenId; - if (o == NULL || priv == NULL || privSz == 0) + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) return BAD_FUNC_ARG; if (o->statefulStateId == 0) return BAD_FUNC_ARG; diff --git a/src/hbs.h b/src/hbs.h index 3cf8cd67..31e2b6d1 100644 --- a/src/hbs.h +++ b/src/hbs.h @@ -32,6 +32,12 @@ #include "wp11_types.h" +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* Read WOLFPKCS11_STATEFUL_RELAX_FSYNC once during library init, before any + * signing thread exists. */ +WP11_LOCAL void WP11_Stateful_InitRelaxFsync(void); +#endif + #ifdef WOLFPKCS11_LMS WP11_LOCAL int wp11_Object_Encode_HssKey(WP11_Object* object); diff --git a/src/internal.c b/src/internal.c index 571c69fa..f01ef2f1 100644 --- a/src/internal.c +++ b/src/internal.c @@ -654,7 +654,7 @@ typedef struct WP11_FileStoreCtx { /* Mark a write-mode file storage context as requiring full durability (fsync * of file data + parent directory) before the close path returns success. * Call after the store is opened for write, before writing. */ -void wolfPKCS11_Store_SetDurable(void* store, int durable) +void wp11_storage_set_durable(void* store, int durable) { #ifndef WOLFPKCS11_TPM_STORE WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; @@ -1316,7 +1316,7 @@ int wolfPKCS11_Store_Open(int type, CK_ULONG id1, CK_ULONG id2, int read, * @return 0 on success. * @return Other value when a durable flush or the temp-file commit failed. */ -int wolfPKCS11_Store_CloseAndReport(void* store) +int wp11_storage_close_report(void* store) { #ifdef WOLFPKCS11_TPM_STORE WP11_TpmStore* tpmStore = (WP11_TpmStore*)store; @@ -1347,24 +1347,47 @@ int wolfPKCS11_Store_CloseAndReport(void* store) * rename in CommitTemp commits a fully-persisted file. POSIX uses * fsync(); Windows/MSVC uses _commit() (FlushFileBuffers under the * hood) because MoveFileExA's MOVEFILE_WRITE_THROUGH makes only the - * rename durable, not the file data buffered before close. */ + * rename durable, not the file data buffered before close. + * + * Every result here has to be checked. fsync only pushes what the + * kernel already holds, so bytes still sitting in the stdio buffer + * are invisible to it: a failed XFFLUSH followed by a successful + * fsync on an empty queue would otherwise look like a clean + * durable write. XFCLOSE is the last chance to learn about a + * deferred write error. */ if (ctx->durable && ctx->is_write) { - (void)XFFLUSH(ctx->file); + if (XFFLUSH(ctx->file) != 0) + fsyncFailed = 1; #if defined(_WIN32) || defined(_MSC_VER) - if (_commit(_fileno(ctx->file)) != 0) + if (!fsyncFailed && _commit(_fileno(ctx->file)) != 0) fsyncFailed = 1; #else - fd = fileno(ctx->file); - if (fd < 0 || fsync(fd) != 0) - fsyncFailed = 1; + if (!fsyncFailed) { + fd = fileno(ctx->file); + if (fd < 0 || fsync(fd) != 0) + fsyncFailed = 1; + } #endif + if (XFCLOSE(ctx->file) != 0) + fsyncFailed = 1; + ctx->file = XBADFILE; } #endif - XFCLOSE(ctx->file); - ctx->file = XBADFILE; + if (ctx->file != XBADFILE && ctx->file != NULL) { + XFCLOSE(ctx->file); + ctx->file = XBADFILE; + } } - if (ctx->is_write && ctx->has_temp) { + /* A durable write that did not reach stable storage must not replace + * the file that is already there. Renaming the temp file over it would + * swap a known-good state file for one that may be short or empty, + * turning a transient I/O error into permanent loss of the key. */ + if (fsyncFailed) { + wolfPKCS11_StoreAbortTemp(ctx); + ret = READ_ONLY_E; + } + else if (ctx->is_write && ctx->has_temp) { commitRet = wolfPKCS11_StoreCommitTemp(ctx); if (commitRet != 0) { wolfPKCS11_StoreAbortTemp(ctx); @@ -1379,9 +1402,6 @@ int wolfPKCS11_Store_CloseAndReport(void* store) wolfPKCS11_StoreAbortTemp(ctx); } - if (ret == 0 && fsyncFailed) - ret = READ_ONLY_E; - XMEMSET(ctx, 0, sizeof(*ctx)); XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); } @@ -1391,11 +1411,13 @@ int wolfPKCS11_Store_CloseAndReport(void* store) /* wolfPKCS11_Store_Close is the store backend interface declared in * wolfpkcs11/store.h and reimplemented by WOLFPKCS11_CUSTOM_STORE builds, so - * it keeps its void return; callers that need the commit result use - * wolfPKCS11_Store_CloseAndReport above instead. */ + * it keeps its void return. Internal callers that need the commit result use + * wp11_storage_close_report above, which stays in the wp11_storage_* namespace + * the other internal wrappers use so it cannot be mistaken for part of the + * backend contract. */ void wolfPKCS11_Store_Close(void* store) { - (void)wolfPKCS11_Store_CloseAndReport(store); + (void)wp11_storage_close_report(store); } /** @@ -5609,6 +5631,19 @@ static int wp11_Object_Decode(WP11_Object* object) #ifdef WOLFPKCS11_LMS case CKK_HSS: ret = wp11_Object_Decode_HssKey(object); + #ifdef WOLFPKCS11_LMS_PRIVATE + /* A stateful private key that cannot be reloaded must not take + * the whole token down with it. wc_LmsKey_Reload fails for an + * exhausted key as well as a damaged one, and exhaustion is + * the normal end of life for every LMS key, so this is an + * expected outcome rather than an error. Decode_HssKey has + * already left the object poisoned, which makes C_Sign on it + * return CKR_DEVICE_ERROR while every other object on the + * token stays usable - including for the C_DestroyObject that + * clears the spent key. */ + if (ret != 0 && object->objClass == CKO_PRIVATE_KEY) + ret = 0; + #endif break; #endif #ifdef WOLFPKCS11_XMSS @@ -5707,6 +5742,20 @@ static int wp11_Object_Encode(WP11_Object* object, int protect) #ifdef WOLFPKCS11_LMS case CKK_HSS: ret = wp11_Object_Encode_HssKey(object); + #ifdef WOLFPKCS11_LMS_PRIVATE + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + /* Release the live signing key like the other private-key + * types do. wc_LmsKey_Free is what frees the heap-allocated + * priv_data and zeroizes the raw private key; a later + * wc_LmsKey_Init would only memset the handle and leak it. + * The durable state file is untouched, so the next login + * reloads the key from it. */ + wc_LmsKey_Free(object->data.lmsKey); + object->encoded = 1; + /* The state is no longer live in memory. */ + object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + } + #endif break; #endif #ifdef WOLFPKCS11_XMSS @@ -5738,11 +5787,21 @@ static int wp11_Object_Encode(WP11_Object* object, int protect) * * Empties the contents of the object. * - * @param [in] object Key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. + * Called for two different reasons, which the destroying flag separates. When + * an object is really being removed from the token it is 1, and state held + * outside the object store goes with it. When a surviving object is only being + * renumbered after some other object was removed, it is 0: the caller unstores + * it at its old id and re-stores it at the new one, so anything that is not + * keyed by the object id must be left alone. + * + * @param [in] object Key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + * @param [in] destroying 1 when the object is being removed, 0 when it is + * only being renumbered. */ -static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) +static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId, + int destroying) { int ret; int storeObjType = -1; @@ -5819,8 +5878,12 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) if (object->objClass == CKO_PRIVATE_KEY) { storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV; /* The signature-state file lives outside the object store and - * would orphan on disk after C_DestroyObject. */ - wp11_Hss_RemoveStoredState(object, tokenId, objId); + * would orphan on disk after C_DestroyObject. It is keyed by + * the per-key nonce rather than the object id, so re-storing a + * renumbered object would never recreate it - only remove it + * when the key really is going away. */ + if (destroying) + wp11_Hss_RemoveStoredState(object, tokenId, objId); } #endif break; @@ -6572,6 +6635,12 @@ int WP11_Library_Init(void) #endif if (libraryInitCount == 0) { +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + /* Resolve the relax-fsync setting once, here, while only one thread + * can be running - the sign path reads it without a lock that would + * cover a lazy initialiser. */ + WP11_Stateful_InitRelaxFsync(); +#endif ret = WP11_Lock_Init(&globalLock); if (ret == 0) { @@ -7813,6 +7882,9 @@ static int wp11_init_get_op_category(int init) case WP11_INIT_AES_CMAC_SIGN: case WP11_INIT_TLS_MAC_SIGN: case WP11_INIT_MLDSA_SIGN: +#ifdef WOLFPKCS11_LMS_PRIVATE + case WP11_INIT_HSS_SIGN: +#endif return WP11_OP_SIGN; case WP11_INIT_HMAC_VERIFY: @@ -8804,7 +8876,8 @@ int WP11_Session_RemoveObjectByHandle(WP11_Session* session, #ifndef WOLFPKCS11_NO_STORE /* remove any id's with higher value */ - ret = wp11_Object_Unstore(*curr, (int)session->slotId, id); + /* Renumbering a surviving object, not destroying it. */ + ret = wp11_Object_Unstore(*curr, (int)session->slotId, id, 0); #ifdef DEBUG_WOLFPKCS11 if (ret != 0) { printf("Failed to unstore slot %d, id: %d, ret: %d, continuing...\n", @@ -8819,7 +8892,7 @@ int WP11_Session_RemoveObjectByHandle(WP11_Session* session, } #ifndef WOLFPKCS11_NO_STORE - ret = wp11_Object_Unstore(object, (int)session->slotId, id); + ret = wp11_Object_Unstore(object, (int)session->slotId, id, 1); #ifdef DEBUG_WOLFPKCS11 if (ret != 0) { printf("Failed to unstore slot %d, id: %d, ret: %d\n", diff --git a/src/wp11_types.h b/src/wp11_types.h index 0b8c5126..0f80a10b 100644 --- a/src/wp11_types.h +++ b/src/wp11_types.h @@ -603,12 +603,12 @@ WP11_LOCAL int wp11_storage_write_array(void* storage, unsigned char* buffer, #ifndef WOLFPKCS11_CUSTOM_STORE /* Report the result of the close-and-commit rather than discarding it. */ -WP11_LOCAL int wolfPKCS11_Store_CloseAndReport(void* store); +WP11_LOCAL int wp11_storage_close_report(void* store); #ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE #ifndef WOLFPKCS11_TPM_STORE /* Ask the file store to fsync the data and its parent directory before the * atomic rename commits. */ -WP11_LOCAL void wolfPKCS11_Store_SetDurable(void* store, int durable); +WP11_LOCAL void wp11_storage_set_durable(void* store, int durable); #endif /* Per-key state files for stateful hash-based signature schemes. Keyed by a diff --git a/tests/hbs_private_test.c b/tests/hbs_private_test.c index fbc3eb25..df3de600 100644 --- a/tests/hbs_private_test.c +++ b/tests/hbs_private_test.c @@ -340,8 +340,10 @@ static CK_RV gen_hss_key(CK_SESSION_HANDLE session, CK_ULONG levels, return ret; } -/* Find a single token object of the given class by CKA_ID. */ -static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, +/* Find a single token object of the given class by CKA_ID, without reporting + * a miss. Tests that expect the object to be gone use this so a passing run + * never writes "FAIL" into test-suite.log. */ +static CK_RV find_by_id_quiet(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, CK_BYTE* id, CK_ULONG idLen, CK_OBJECT_HANDLE* obj) { CK_RV ret; @@ -353,17 +355,24 @@ static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, ret = funcList->C_FindObjectsInit(session, tmpl, sizeof(tmpl) / sizeof(*tmpl)); - CHECK_CKR(ret, "FindObjectsInit"); if (ret == CKR_OK) { ret = funcList->C_FindObjects(session, obj, 1, &count); - CHECK_CKR(ret, "FindObjects"); funcList->C_FindObjectsFinal(session); } - if (ret == CKR_OK && count != 1) { - fprintf(stderr, "\nExpected 1 object, found %lu - FAIL\n", - (unsigned long)count); + if (ret == CKR_OK && count != 1) ret = CKR_GENERAL_ERROR; - } + return ret; +} + +/* Find a single token object of the given class by CKA_ID. Reports a miss, + * so only use it where the object is expected to be present. */ +static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, + CK_BYTE* id, CK_ULONG idLen, CK_OBJECT_HANDLE* obj) +{ + CK_RV ret = find_by_id_quiet(session, cls, id, idLen, obj); + + if (ret != CKR_OK) + fprintf(stderr, "\nExpected 1 object with the given CKA_ID - FAIL\n"); return ret; } @@ -979,7 +988,7 @@ static CK_RV test_state_tamper_detected(void) printf(" tamper detected at login: %lx\n", (unsigned long)rret); } - else if (find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + else if (find_by_id_quiet(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv) != CKR_OK) { printf(" tampered key not usable after login\n"); } @@ -1012,6 +1021,9 @@ static CK_RV test_write_failure_poison(void) static CK_BYTE id[] = "hss-wfail"; CK_BYTE* sig = NULL; CK_ULONG sigLen = 0, before = 0, after = 0; + struct stat dirStat; + int dirModeSaved = 0; + int skipInjection = 0; printf(" state-write failure poisons the key...\n"); ret = fresh_token(&session); @@ -1031,10 +1043,22 @@ static CK_RV test_write_failure_poison(void) } if (ret == CKR_OK) ret = get_keys_remaining(session, priv, &before); - /* Make the store directory unwritable so the durable state write fails. */ - if (ret == CKR_OK && chmod(storeDir, 0500) != 0) - ret = CKR_GENERAL_ERROR; - if (ret == CKR_OK) { + /* Make the store directory unwritable so the durable state write fails. + * Root bypasses DAC permission checks, so the write would still succeed + * and the assertion below would fail for a reason that says nothing about + * the code under test. */ + if (ret == CKR_OK && geteuid() == 0) { + printf(" skipping write-failure injection (running as root)\n"); + skipInjection = 1; + } + if (ret == CKR_OK && !skipInjection) { + /* Capture the real mode so it can be put back exactly. */ + if (stat(storeDir, &dirStat) != 0 || chmod(storeDir, 0500) != 0) + ret = CKR_GENERAL_ERROR; + else + dirModeSaved = 1; + } + if (ret == CKR_OK && !skipInjection) { sret = hss_sign(session, priv, &sig, &sigLen); if (sret == CKR_OK) { fprintf(stderr, "\nsigned despite an unwritable store - FAIL\n"); @@ -1048,7 +1072,10 @@ static CK_RV test_write_failure_poison(void) printf(" write-failed sign refused: %lx\n", (unsigned long)sret); } } - (void)chmod(storeDir, 0700); /* restore, even on failure */ + if (dirModeSaved) + (void)chmod(storeDir, dirStat.st_mode); /* restore, even on failure */ + if (skipInjection) + return ret; /* The key is poisoned until reloaded: the next sign is CKR_DEVICE_ERROR. */ if (ret == CKR_OK) { sret = hss_sign(session, priv, &sig, &sigLen); @@ -1258,7 +1285,8 @@ static CK_RV corrupt_state_and_check(const char* label, int type) printf(" detected at login: %lx\n", (unsigned long)ret); ret = CKR_OK; } - else if (find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv) + else if (find_by_id_quiet(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv) != CKR_OK) { printf(" key not usable after corruption\n"); } @@ -1468,6 +1496,298 @@ static CK_RV test_multilevel_rollover(void) return ret; } +/* Destroying an unrelated token object renumbers the survivors, which unstores + * and re-stores them. The re-store rewrites only the object shell, so if the + * renumbering path also dropped the signature-state file the surviving private + * key would be permanently unusable. Destroy the PUBLIC key, which sits after + * the private key in the token list and so forces the private key through the + * renumbering loop, then reload and sign. */ +static CK_RV test_destroy_sibling_keeps_state(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-sibling"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + + printf(" destroying the public key keeps the private state...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + /* One signature so the on-disk state is past the genesis index. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "baseline sign"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) { + ret = funcList->C_DestroyObject(session, pub); + CHECK_CKR(ret, "C_DestroyObject(public)"); + } + close_session(session); + pkcs11_final(); + + /* Reload the token: the private key must still decode and sign. */ + if (ret == CKR_OK) { + ret = reopen_token(&session); + CHECK_CKR(ret, "reopen after destroying the public key"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + CHECK_CKR(ret, "private key survived"); + } + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "sign after the sibling was destroyed"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + if (ret == CKR_OK) + printf(" private key still signs\n"); + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* A spent key cannot be reloaded - wolfSSL rejects a key with no signatures + * left. That must not take the whole token down: C_Login has to succeed so the + * other objects stay reachable, while the spent key itself reports + * CKR_DEVICE_ERROR. */ +static CK_RV test_exhausted_key_login(void) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE aes = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-spent"; + CK_BYTE aesId[] = "aes-bystander"; + CK_MECHANISM mech; + CK_BYTE sig[4096]; + CK_ULONG sigLen; + int i; + CK_OBJECT_CLASS secretClass = CKO_SECRET_KEY; + CK_KEY_TYPE aesType = CKK_AES; + CK_BYTE aesKey[32] = { 0 }; + CK_ATTRIBUTE aesTmpl[] = { + { CKA_CLASS, &secretClass, sizeof(secretClass) }, + { CKA_KEY_TYPE, &aesType, sizeof(aesType) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, aesId, sizeof(aesId) - 1 }, + { CKA_VALUE, aesKey, sizeof(aesKey) }, + }; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + printf(" an exhausted key does not break login for the token...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + /* An unrelated token object that must remain reachable afterwards. */ + if (ret == CKR_OK) { + ret = funcList->C_CreateObject(session, aesTmpl, + sizeof(aesTmpl) / sizeof(*aesTmpl), &aes); + CHECK_CKR(ret, "create bystander AES key"); + } + /* Spend the whole tree. */ + for (i = 0; ret == CKR_OK && i < HSS_H5_SIGS + 4; i++) { + sret = funcList->C_SignInit(session, &mech, priv); + if (sret != CKR_OK) + break; + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (sret != CKR_OK) + break; + } + close_session(session); + pkcs11_final(); + + /* The whole point: login must still work. */ + if (ret == CKR_OK) { + ret = reopen_token(&session); + CHECK_CKR(ret, "C_Login with an exhausted HSS key on the token"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_SECRET_KEY, aesId, sizeof(aesId) - 1, + &aes); + CHECK_CKR(ret, "bystander key still reachable"); + } + if (ret == CKR_OK) { + printf(" login succeeded, other objects reachable\n"); + /* A key whose state cannot be reloaded stays flagged as undecoded, + * which is how this library already treats any object it could not + * decode for the current user: wp11_Session_FindNext skips it. So the + * spent key is not enumerable rather than enumerable-but-erroring. + * Assert that, and that it is the ONLY thing lost - the token and its + * other objects keep working. */ + if (find_by_id_quiet(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv) == CKR_OK) { + /* Should it become enumerable one day, it must still refuse. */ + sret = funcList->C_SignInit(session, &mech, priv); + if (sret == CKR_OK) { + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, + TEST_MSG_LEN, sig, &sigLen); + } + CHECK_CKR_FAIL(sret, CKR_DEVICE_ERROR, "spent-key sign"); + if (sret != CKR_DEVICE_ERROR) + ret = CKR_GENERAL_ERROR; + else + printf(" spent key reports CKR_DEVICE_ERROR\n"); + } + else { + printf(" spent key not enumerable, token otherwise intact\n"); + } + } + /* The bystander key must still be usable and destroyable, proving the + * token is fully operational and not merely loggable-into. */ + if (ret == CKR_OK) { + ret = funcList->C_DestroyObject(session, aes); + CHECK_CKR(ret, "C_DestroyObject on the bystander key"); + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* C_Logout re-encodes the live objects with protect set, which releases the + * signing key; the next C_Login decodes it again from the durable state. Both + * halves happen inside one C_Initialize, a path no other test covers. Run it + * twice so a leak or a stale key surfaces on the second cycle. */ +static CK_RV test_logout_login_cycle(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-cycle"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + CK_ULONG remaining = 0, prevRemaining = 0; + int cycle; + + printf(" logout/login cycle keeps the key usable...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &prevRemaining); + + for (cycle = 0; ret == CKR_OK && cycle < 2; cycle++) { + if (ret == CKR_OK) { + ret = funcList->C_Logout(session); + CHECK_CKR(ret, "C_Logout"); + } + if (ret == CKR_OK) { + ret = funcList->C_Login(session, CKU_USER, userPin, userPinLen); + CHECK_CKR(ret, "C_Login after logout"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv); + CHECK_CKR(ret, "private key after re-login"); + } + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "sign after re-login"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, + &pub); + } + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + /* The index must move forward across the cycle, never back. */ + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remaining); + CHECK_CKR(ret, "keys-remaining after cycle"); + } + if (ret == CKR_OK && remaining >= prevRemaining) { + fprintf(stderr, + "\nkeys-remaining did not advance across cycle %d " + "(%lu -> %lu) - FAIL\n", cycle, + (unsigned long)prevRemaining, (unsigned long)remaining); + ret = CKR_GENERAL_ERROR; + } + prevRemaining = remaining; + } + if (ret == CKR_OK) + printf(" two logout/login cycles, state advanced each time\n"); + close_session(session); + pkcs11_final(); + return ret; +} + +/* A second C_SignInit while an HSS signing operation is already active must be + * refused. This only works if WP11_INIT_HSS_SIGN is registered as a sign-class + * operation; if it is not, the second init silently replaces the first. */ +static CK_RV test_sign_init_operation_active(void) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-active"; + CK_MECHANISM mech; + CK_BYTE sig[4096]; + CK_ULONG sigLen; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + printf(" a second C_SignInit is refused while one is active...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + if (ret == CKR_OK) { + ret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR(ret, "first C_SignInit"); + } + if (ret == CKR_OK) { + sret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR_FAIL(sret, CKR_OPERATION_ACTIVE, "second C_SignInit"); + if (sret != CKR_OPERATION_ACTIVE) + ret = CKR_GENERAL_ERROR; + else + printf(" second C_SignInit returned CKR_OPERATION_ACTIVE\n"); + } + /* The first operation must still be the live one and complete normally. */ + if (ret == CKR_OK) { + sigLen = sizeof(sig); + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + CHECK_CKR(ret, "C_Sign completes the original operation"); + } + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + close_session(session); + pkcs11_final(); + return ret; +} + static CK_RV run_all(void) { CK_RV ret; @@ -1483,6 +1803,14 @@ static CK_RV run_all(void) ret = test_import_value_rejected(); if (ret == CKR_OK) ret = test_exhaustion_poison(); + if (ret == CKR_OK) + ret = test_sign_init_operation_active(); + if (ret == CKR_OK) + ret = test_exhausted_key_login(); + if (ret == CKR_OK) + ret = test_logout_login_cycle(); + if (ret == CKR_OK) + ret = test_destroy_sibling_keeps_state(); if (ret == CKR_OK) ret = test_param_matrix(); if (ret == CKR_OK) diff --git a/tests/pkcs11v3test.c b/tests/pkcs11v3test.c index d4d76dd5..ef4c3f0e 100644 --- a/tests/pkcs11v3test.c +++ b/tests/pkcs11v3test.c @@ -3167,19 +3167,12 @@ static CK_RV hbs_empty_template_rejected(CK_SESSION_HANDLE session, ret = funcList->C_CreateObject(session, tmpl, sizeof(tmpl) / sizeof(*tmpl), &obj); -#ifdef WOLFPKCS11_LMS_PRIVATE - if (keyType == CKK_HSS) { - /* Sign-capable build: an HSS public key with no CKA_VALUE is a valid - * deferred-keygen placeholder (C_GenerateKeyPair populates it), so it - * is accepted rather than rejected. */ - CHECK_CKR(ret, "HSS empty-template public key (deferred keygen)"); - } - else -#endif - { - CHECK_CKR_FAIL(ret, CKR_ATTRIBUTE_VALUE_INVALID, - "HBS empty-template public key rejected"); - } + /* This holds in a sign-capable build too. C_GenerateKeyPair does create + * both objects from templates with no key material, but that is internal + * to keygen; C_CreateObject has no second step that would fill the value + * in, so an empty template is still an incomplete one. */ + CHECK_CKR_FAIL(ret, CKR_ATTRIBUTE_VALUE_INVALID, + "HBS empty-template public key rejected"); if (obj != CK_INVALID_HANDLE) funcList->C_DestroyObject(session, obj); return ret; diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 550ac4d7..0fd5f01e 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -145,6 +145,9 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_TPM_STORE) #error WOLFPKCS11_LMS_PRIVATE is incompatible with WOLFPKCS11_TPM_STORE (encrypted state blobs exceed TPM NV capacity). #endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_CUSTOM_STORE) +#error WOLFPKCS11_LMS_PRIVATE is incompatible with WOLFPKCS11_CUSTOM_STORE (the durable signature-state file needs the built-in file store). +#endif /* Stateful hash-based signature framework selector. _PRIVATE gates the * encrypted durable-state infrastructure (keygen + signing); it is compiled