From 888fe280f3984dff4941afa97c201c77660dd011 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 6 May 2026 21:45:25 +0000 Subject: [PATCH 1/6] Add LMS/HSS (RFC 8554) signature support Introduces PKCS#11 v3.2 surface for the stateful hash-based signature scheme LMS / HSS via two compile-time flags that separate the safe verifier-only build from the experimental stateful signer build: --enable-lms (CMake WOLFPKCS11_LMS=yes) Verification + public-key import only. --enable-lms-private (CMake WOLFPKCS11_LMS_PRIVATE=yes; implies --enable-lms) Adds key generation and signing. Marked EXPERIMENTAL. Highlights of the implementation: * Standard PKCS#11 v3.2 constants in wolfpkcs11/pkcs11.h: CKK_HSS, CKM_HSS_KEY_PAIR_GEN, CKM_HSS, CKA_HSS_*, CK_HSS_PARAMS. * CKM_HSS sign/verify takes the whole message (matches wc_LmsKey_Sign). * Two on-disk files per HSS private key: a non-secret "shell" with parameters and cached pubkey, and an encrypted "state" file rewritten on every signature via the wolfSSL write callback. * State file is AES-GCM-encrypted with the token master key and a fresh nonce per write; the on-disk header is bound into the GCM tag via AAD so any parameter tampering is detected at decrypt time. * Atomic durable write path: mkstemp -> fsync(file) -> rename -> fsync(parent dir) so a returned signature always corresponds to a state index that is durably on disk. WOLFPKCS11_HSS_RELAX_FSYNC=1 env var provides a documented non-production opt-out. * WP11_FLAG_HSS_STATE_VALID poisoning: any state-write failure clears the flag and forces a Reload from durable storage on the next sign attempt. * Private-key import (C_CreateObject of CKO_PRIVATE_KEY+CKK_HSS), private-key export (CKA_VALUE on a private HSS key, ignoring CKA_EXTRACTABLE), and C_CopyObject of an HSS private key are all rejected unconditionally. * Compatible with current wolfSSL master: the legacy WOLFSSL_WC_LMS toggle is no longer required; --enable-lms alone activates the wolfCrypt LMS impl. A build-time check rejects WOLFPKCS11_LMS_PRIVATE combined with wolfSSL's --enable-lms=verify-only. Tests: * tests/pkcs11v3test.c: keygen+sign+verify roundtrip, CKA_HSS_KEYS_REMAINING decrement, CKA_VALUE blocked even with EXTRACTABLE=TRUE, copy rejected, private-import rejected, verify-only-build keygen rejected. * tests/lms_state_persistence_test.c: end-to-end test covering token re-initialization, state reload, and KEYS_REMAINING continuity. * CI matrix gains two new jobs: lms (verify-only) and lms_private. The full design is captured at /root/.claude/plans/i-want-to-add-zany-porcupine.md including the operational responsibilities (do not snapshot the token while HSS keys are present) that the README now documents. https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- .github/workflows/build-workflow.yml | 3 +- .github/workflows/unit-test.yml | 9 + CMakeLists.txt | 35 + README.md | 66 ++ cmake/options.h.in | 4 + configure.ac | 40 + src/crypto.c | 114 +++ src/internal.c | 1331 ++++++++++++++++++++++++++ src/slot.c | 34 + tests/include.am | 6 + tests/lms_state_persistence_test.c | 430 +++++++++ tests/pkcs11v3test.c | 294 ++++++ wolfpkcs11/internal.h | 56 ++ wolfpkcs11/pkcs11.h | 40 + wolfpkcs11/store.h | 3 + 15 files changed, 2464 insertions(+), 1 deletion(-) create mode 100644 tests/lms_state_persistence_test.c diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index 428929dc..c87ed39a 100644 --- a/.github/workflows/build-workflow.yml +++ b/.github/workflows/build-workflow.yml @@ -35,7 +35,8 @@ jobs: working-directory: ./wolfssl run: | ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 \ - --enable-mldsa C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa --enable-lms=small \ + C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP -DWOLFSSL_LMS_MAX_LEVELS=2 -DWOLFSSL_LMS_MAX_HEIGHT=10" - name: wolfssl make install working-directory: ./wolfssl run: make diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 1400247c..07c75434 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -110,6 +110,15 @@ jobs: uses: ./.github/workflows/build-workflow.yml with: config: --enable-mlkem + lms: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms + lms_private: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private + check: make check && ./tests/lms_state_persistence_test debug: uses: ./.github/workflows/build-workflow.yml with: diff --git a/CMakeLists.txt b/CMakeLists.txt index a868a2f7..9446527b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,6 +510,41 @@ if(WOLFPKCS11_MLKEM) endif() +# LMS/HSS verify-only support (RFC 8554) +add_option("WOLFPKCS11_LMS" + "Enable wolfPKCS11 LMS/HSS verification (default: disabled)" + "no" "yes;no" +) + +# LMS/HSS keygen + signing (stateful, EXPERIMENTAL); implies WOLFPKCS11_LMS +add_option("WOLFPKCS11_LMS_PRIVATE" + "Enable wolfPKCS11 LMS/HSS keygen + signing (EXPERIMENTAL, default: disabled)" + "no" "yes;no" +) + +if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_LMS) + message(STATUS "WOLFPKCS11_LMS_PRIVATE implies WOLFPKCS11_LMS; enabling automatically") + override_cache(WOLFPKCS11_LMS "yes") +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") + 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) + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS_PRIVATE") +endif() + + # If wolfpkcs11/options.h exists, delete it to avoid # a mixup with build/wolfpkcs11/options.h. if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/wolfpkcs11/options.h") diff --git a/README.md b/README.md index b81bc0c5..9f54a4aa 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,70 @@ 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) + +LMS (Leighton-Micali) and its hierarchical extension HSS are hash-based +one-time signature schemes specified in +[RFC 8554](https://www.rfc-editor.org/rfc/rfc8554) and standardized in +NIST SP 800-208. The PKCS#11 v3.2 surface for HSS uses `CKK_HSS`, +`CKM_HSS_KEY_PAIR_GEN`, and `CKM_HSS`. Note that `C_Sign` and `C_Verify` +operate on the **whole message** (not a digest). + +Two separate compile flags split this feature into a safe verifier-only +build and a stateful signer build: + +| Flag | What it enables | Risk | +|---|---|---| +| `--enable-lms` (CMake `WOLFPKCS11_LMS=yes`) | Verification + public-key import only. | None — verify is stateless. | +| `--enable-lms-private` (CMake `WOLFPKCS11_LMS_PRIVATE=yes`, implies `--enable-lms`) | Adds key generation and signing. **EXPERIMENTAL.** | Stateful private keys. Mismanagement causes one-time-key reuse and complete forgery. | + +Build wolfSSL with `--enable-lms` (or `--enable-lms=small` for a smaller +footprint) and then build wolfPKCS11 with one of the two flags above. +For `--enable-lms-private` wolfSSL must NOT be built with +`--enable-lms=verify-only` (the build will fail at the prerequisite check). + +#### Operational responsibilities for `--enable-lms-private` + +LMS/HSS are stateful: every signature consumes a one-time key, and re-using +a leaf index breaks security catastrophically. wolfPKCS11 implements the +following safeguards on top of the wolfSSL state callbacks: + +* The per-signature state file is encrypted with the token master key and + written via an `mkstemp` + `fsync(file)` + `rename` + `fsync(parent dir)` + sequence, so a returned signature always corresponds to a state index that + is durably on disk. The on-disk header (parameters, version) is bound into + the AES-GCM tag so any tampering is detected at decrypt time. +* On any state-write failure the in-memory state is *poisoned* — subsequent + sign attempts return `CKR_DEVICE_ERROR` until the key is reloaded from + durable storage. +* **Private key import** (`C_CreateObject` with `CKO_PRIVATE_KEY` + + `CKK_HSS`) is rejected unconditionally. There is no way to install an + HSS private key with a caller-controlled state index. +* **Private key export** (`C_GetAttributeValue` for `CKA_VALUE` on a private + HSS key) returns `CK_UNAVAILABLE_INFORMATION` regardless of + `CKA_EXTRACTABLE` / `CKA_SENSITIVE`. +* **Copying** an HSS private key (`C_CopyObject`) is rejected. + +The operator is responsible for: + +* **Never copying or snapshotting the token directory** while a signing key + is present. A restored copy would re-issue already-consumed indices. +* Treating HSS private keys as bound to the device they were generated on. + `rsync`, `cp`, filesystem snapshots and backup tools will silently break + security if used on a token with active HSS keys. +* Storing the token directory on a journaled filesystem (durability + assumptions rely on `fsync` semantics). + +The default parameter set when none is supplied is `levels = 1`, +`H = 10`, `W = 8` (1024 lifetime signatures, ~3 KiB signature). Mixed +`(H, W)` across HSS levels is rejected with `CKR_MECHANISM_PARAM_INVALID` +because the underlying wolfSSL API requires uniform parameters. + +For non-production rigs (e.g., tmpfs-backed test harnesses) the env var +`WOLFPKCS11_HSS_RELAX_FSYNC=1` skips the per-signature `fsync` calls. +**Never set this in production.** A power loss or kernel panic can then +expose a one-time-key reuse window. + ### Build options and defines #### Define WOLFPKCS11_TPM_STORE @@ -219,6 +283,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 (RFC 8554) | +| `WOLFPKCS11_LMS_PRIVATE` | `no` | LMS/HSS keygen + signing (EXPERIMENTAL) | | `WOLFPKCS11_EXAMPLES` | `yes` | Build examples | | `WOLFPKCS11_TESTS` | `yes` | Build and register tests | | `WOLFPKCS11_COVERAGE` | `no` | Code coverage support | diff --git a/cmake/options.h.in b/cmake/options.h.in index 85137980..1badec67 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -96,6 +96,10 @@ extern "C" { #cmakedefine WOLFPKCS11_MLDSA #undef WOLFPKCS11_MLKEM #cmakedefine WOLFPKCS11_MLKEM +#undef WOLFPKCS11_LMS +#cmakedefine WOLFPKCS11_LMS +#undef WOLFPKCS11_LMS_PRIVATE +#cmakedefine WOLFPKCS11_LMS_PRIVATE #undef WOLFPKCS11_TPM #cmakedefine WOLFPKCS11_TPM #undef WOLFPKCS11_NSS diff --git a/configure.ac b/configure.ac index 79d0517b..e8618c02 100644 --- a/configure.ac +++ b/configure.ac @@ -566,6 +566,44 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_MLKEM" fi +# LMS/HSS verify-only support (RFC 8554) +AC_ARG_ENABLE([lms], + [AS_HELP_STRING([--enable-lms],[Enable LMS/HSS verification and public-key import (default: disabled)])], + [ ENABLED_LMS=$enableval ], + [ ENABLED_LMS=no ] + ) + +# LMS/HSS keygen + signing (stateful, EXPERIMENTAL); implies --enable-lms +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 +fi + +if test "$ENABLED_LMS" = "yes" +then + 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_LMS_PRIVATE" = "yes" +then + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS_PRIVATE" +fi + AM_CONDITIONAL([BUILD_STATIC],[test "x$enable_shared" = "xno"]) @@ -755,6 +793,8 @@ echo " * ECC: $ENABLED_ECC" 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 " * NSS modifications: $ENABLED_NSS" echo " * Default token path: $WOLFPKCS11_DEFAULT_TOKEN_PATH" echo " * PKCS#11 Version 3.0: $ENABLED_PKCS11V3_0" diff --git a/src/crypto.c b/src/crypto.c index d0ae3e18..22bb5b64 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -105,6 +105,15 @@ static CK_ATTRIBUTE_TYPE mldsaKeyParams[] = { #define MLDSA_KEY_PARAMS_CNT (sizeof(mldsaKeyParams)/sizeof(*mldsaKeyParams)) #endif +#ifdef WOLFPKCS11_LMS +/* HSS key data attributes. data[0] = optional CK_HSS_PARAMS, data[1] = pub. */ +static CK_ATTRIBUTE_TYPE hssKeyParams[] = { + CKA_PARAMETER_SET, /* CK_HSS_PARAMS struct (optional, default if absent) */ + CKA_VALUE /* RFC 8554 raw HSS public key */ +}; +#define HSS_KEY_PARAMS_CNT (sizeof(hssKeyParams)/sizeof(*hssKeyParams)) +#endif + #ifndef NO_DH /* DH key data attributes. */ static CK_ATTRIBUTE_TYPE dhKeyParams[] = { @@ -678,6 +687,12 @@ static CK_RV SetAttributeValue(WP11_Session* session, WP11_Object* obj, cnt = MLDSA_KEY_PARAMS_CNT; break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + attrs = hssKeyParams; + cnt = HSS_KEY_PARAMS_CNT; + break; + #endif #ifndef NO_DH case CKK_DH: attrs = dhKeyParams; @@ -754,6 +769,15 @@ static CK_RV SetAttributeValue(WP11_Session* session, WP11_Object* obj, ret = WP11_Object_SetMldsaKey(obj, data, len); break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + ret = WP11_Object_SetHssKey(obj, data, len); + if (ret == BAD_FUNC_ARG) { + /* Private-key import is intentionally rejected. */ + return CKR_ATTRIBUTE_VALUE_INVALID; + } + break; + #endif #ifndef NO_DH case CKK_DH: ret = WP11_Object_SetDhKey(obj, data, len); @@ -4495,6 +4519,17 @@ 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: @@ -4882,6 +4917,31 @@ 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 = (word32)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; + + sigLen = (word32)*pulSignatureLen; + ret = WP11_Hss_Sign(pData, (word32)ulDataLen, pSignature, &sigLen, + obj); + if (ret == NOT_AVAILABLE_E) { + /* State invalid (poisoned) — caller must reload. */ + return CKR_DEVICE_ERROR; + } + *pulSignatureLen = sigLen; + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -5592,6 +5652,17 @@ CK_RV C_VerifyInit(CK_SESSION_HANDLE hSession, init |= WP11_INIT_MLDSA_VERIFY; break; #endif +#ifdef WOLFPKCS11_LMS + 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_VERIFY; + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -5925,6 +5996,14 @@ CK_RV C_Verify(CK_SESSION_HANDLE hSession, CK_BYTE_PTR pData, (int)ulDataLen, &stat, obj, session); break; #endif +#ifdef WOLFPKCS11_LMS + case CKM_HSS: + if (!WP11_Session_IsOpInitialized(session, WP11_INIT_HSS_VERIFY)) + return CKR_OPERATION_NOT_INITIALIZED; + ret = WP11_Hss_Verify(pSignature, (word32)ulSignatureLen, pData, + (word32)ulDataLen, &stat, obj); + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -7218,6 +7297,41 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, } break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS_KEY_PAIR_GEN: { + const CK_HSS_PARAMS* hssParams = NULL; + CK_ULONG hssParamsLen = 0; + if (pMechanism->pParameter != NULL) { + if (pMechanism->ulParameterLen != sizeof(CK_HSS_PARAMS)) + return CKR_MECHANISM_PARAM_INVALID; + hssParams = (const CK_HSS_PARAMS*)pMechanism->pParameter; + hssParamsLen = pMechanism->ulParameterLen; + } + + *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, hssParams, + hssParamsLen, + WP11_Session_GetSlot(session)); + if (ret == BAD_FUNC_ARG) + rv = CKR_MECHANISM_PARAM_INVALID; + else if (ret != 0) + rv = CKR_FUNCTION_FAILED; + } + break; + } +#elif defined(WOLFPKCS11_LMS) + case CKM_HSS_KEY_PAIR_GEN: + /* Verify-only build: no keygen capability advertised. */ + return CKR_MECHANISM_INVALID; +#endif #ifdef WOLFPKCS11_MLKEM case CKM_ML_KEM_KEY_PAIR_GEN: if (pMechanism->pParameter != NULL || diff --git a/src/internal.c b/src/internal.c index b52bfd18..c4fc0b1d 100644 --- a/src/internal.c +++ b/src/internal.c @@ -45,6 +45,10 @@ #ifdef WOLFPKCS11_MLKEM #include #endif +#ifdef WOLFPKCS11_LMS +#include +#include +#endif #if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) /* OS-specific includes for directory creation */ @@ -260,6 +264,9 @@ struct WP11_Object { #ifdef WOLFPKCS11_MLDSA MlDsaKey* mldsaKey; /* ML-DSA key object */ #endif + #ifdef WOLFPKCS11_LMS + LmsKey* lmsKey; /* LMS/HSS key object (stateful priv) */ + #endif #ifndef NO_DH WP11_DhKey* dhKey; /* DH parameters object */ #endif @@ -1103,10 +1110,26 @@ typedef struct WP11_FileStoreCtx { XFILE file; int is_write; int has_temp; + /* When non-zero, fsync the file before close and the parent directory + * after rename. Used by the HSS state-write path so a returned signature + * always corresponds to an index that is durably on disk. */ + int durable; 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 + directory) before the close path returns success. + * Must be called after wolfPKCS11_Store_OpenSz returned, before writes. */ +WP11_LOCAL void wolfPKCS11_Store_SetDurable(void* store, int durable) +{ + WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; + if (ctx != NULL) + ctx->durable = durable ? 1 : 0; +} +#endif + static void wolfPKCS11_StoreAbortTemp(WP11_FileStoreCtx* ctx) { if (ctx != NULL && ctx->has_temp) { @@ -1138,6 +1161,56 @@ static int wolfPKCS11_StoreCommitTemp(WP11_FileStoreCtx* ctx) ctx->has_temp = 0; } + /* Durable mode: fsync the parent directory so the rename is recorded + * on stable storage before returning success to the caller. Without + * this, a power loss could revert the directory entry and silently + * undo the state advance. */ + if (ret == 0 && ctx->durable) { +#if defined(_WIN32) || defined(_MSC_VER) + char dirCopy[WP11_STORE_MAX_PATH]; + char* p; + HANDLE dh; + XSTRNCPY(dirCopy, ctx->final_name, sizeof(dirCopy)); + dirCopy[sizeof(dirCopy) - 1] = '\0'; + p = strrchr(dirCopy, '\\'); + if (p == NULL) p = strrchr(dirCopy, '/'); + if (p != NULL) { + *p = '\0'; + dh = CreateFileA(dirCopy, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (dh != INVALID_HANDLE_VALUE) { + /* Best-effort. Some FS reject FlushFileBuffers on dirs. */ + (void)FlushFileBuffers(dh); + CloseHandle(dh); + } + } +#else + char dirCopy[WP11_STORE_MAX_PATH]; + char* slash; + int dirFd; + XSTRNCPY(dirCopy, ctx->final_name, sizeof(dirCopy)); + dirCopy[sizeof(dirCopy) - 1] = '\0'; + slash = strrchr(dirCopy, '/'); + if (slash != NULL) + *slash = '\0'; + else + XSTRNCPY(dirCopy, ".", sizeof(dirCopy)); + dirFd = open(dirCopy, O_RDONLY +#ifdef O_DIRECTORY + | O_DIRECTORY +#endif + ); + if (dirFd >= 0) { + (void)fsync(dirFd); + close(dirFd); + } + /* If the open or fsync failed, we still consider the rename + * committed — POSIX guarantees rename atomicity on the same FS, + * and dir-fsync is a best-effort durability improvement. */ +#endif + } + return ret; } @@ -1413,6 +1486,22 @@ static int wolfPKCS11_Store_Name(int type, CK_ULONG id1, CK_ULONG id2, char* nam str, id1, id2); break; #endif +#ifdef WOLFPKCS11_LMS + case WOLFPKCS11_STORE_HSSKEY_PUB: + ret = XSNPRINTF(name, nameLen, "%s/wp11_hsskey_pub_%016lx_%016lx", + str, id1, id2); + break; + case WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL: + ret = XSNPRINTF(name, nameLen, + "%s/wp11_hsskey_priv_shell_%016lx_%016lx", + str, id1, id2); + break; + case WOLFPKCS11_STORE_HSSKEY_PRIV_STATE: + ret = XSNPRINTF(name, nameLen, + "%s/wp11_hsskey_priv_state_%016lx_%016lx", + str, id1, id2); + break; +#endif default: ret = -1; @@ -1716,6 +1805,22 @@ void wolfPKCS11_Store_Close(void* store) int commitRet = 0; if (ctx->file != XBADFILE && ctx->file != NULL) { + /* 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); +#if defined(_WIN32) || defined(_MSC_VER) + { + int fd = _fileno(ctx->file); + if (fd >= 0) (void)_commit(fd); + } +#else + { + int fd = fileno(ctx->file); + if (fd >= 0) (void)fsync(fd); + } +#endif + } XFCLOSE(ctx->file); ctx->file = XBADFILE; } @@ -2497,6 +2602,20 @@ int wp11_Object_AllocateTypeData(WP11_Object* object) } break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + if (object->data.lmsKey == NULL) { + object->data.lmsKey = (LmsKey*)XMALLOC(sizeof(LmsKey), + NULL, DYNAMIC_TYPE_LMS); + if (object->data.lmsKey == NULL) { + ret = MEMORY_E; + } + else { + XMEMSET(object->data.lmsKey, 0, sizeof(LmsKey)); + } + } + break; + #endif #ifdef WOLFPKCS11_MLKEM case CKK_ML_KEM: if (object->data.mlKemKey == NULL) { @@ -2920,6 +3039,56 @@ int WP11_Object_Copy(WP11_Object *src, WP11_Object *dest) break; } #endif +#ifdef WOLFPKCS11_LMS + case CKK_HSS: { + /* Copying an HSS private key is fundamentally unsafe: it + * would create two independently-advancing states from + * the same starting index, leading to one-time-key reuse. + * Reject the copy. Public keys can be cloned freely. */ + if (src->objClass == CKO_PRIVATE_KEY) { + ret = BAD_FUNC_ARG; + } + else { + byte* buf = NULL; + word32 bufSz = 0; + int levels = 0, height = 0, w = 0; + + ret = wc_LmsKey_GetParameters(src->data.lmsKey, + &levels, &height, &w); + if (ret == 0) + ret = wc_LmsKey_GetPubLen(src->data.lmsKey, &bufSz); + if (ret == 0) { + buf = (byte*)XMALLOC(bufSz, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (buf == NULL) + ret = MEMORY_E; + } + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(src->data.lmsKey, + buf, &bufSz); + if (ret == 0) + ret = wc_LmsKey_Init(dest->data.lmsKey, NULL, + dest->devId); + if (ret == 0) { + ret = wc_LmsKey_SetParameters(dest->data.lmsKey, + levels, height, w); + if (ret != 0) + wc_LmsKey_Free(dest->data.lmsKey); + } + if (ret == 0) { + ret = wc_LmsKey_ImportPubRaw(dest->data.lmsKey, + buf, bufSz); + if (ret != 0) + wc_LmsKey_Free(dest->data.lmsKey); + } + if (buf != NULL) { + wc_ForceZero(buf, bufSz); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } + break; + } +#endif #ifdef WOLFPKCS11_MLKEM case CKK_ML_KEM: { byte* buf = NULL; @@ -3073,6 +3242,53 @@ 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, IV length) 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. * @@ -4547,6 +4763,614 @@ static int wp11_Object_Store_MldsaKey(WP11_Object* object, int tokenId, } #endif /* WOLFPKCS11_MLDSA */ +#ifdef WOLFPKCS11_LMS +/* On-disk magic + version for the HSS shell file (parameters + cached pub). + * The shell is non-secret metadata persisted once at keygen so that on token + * load we know how to call wc_LmsKey_SetParameters before wc_LmsKey_Reload. */ +#define WP11_HSS_SHELL_MAGIC 0x48535350UL /* "HSSP" */ +#define WP11_HSS_SHELL_VERSION 1U + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* On-disk magic + version for the encrypted HSS state file. The header + * (including levels/height/winternitz) is bound into the AES-GCM tag via + * AAD, so any tampering with parameters is detected at decrypt time. */ +#define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ +#define WP11_HSS_STATE_VERSION 1U +#define WP11_HSS_STATE_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. */ +static int wp11_HssRelaxFsync = -1; + +static int wp11_HssShouldFsync(void) +{ + if (wp11_HssRelaxFsync < 0) { +#ifndef WOLFPKCS11_NO_ENV + const char* v = XGETENV("WOLFPKCS11_HSS_RELAX_FSYNC"); + wp11_HssRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; +#else + wp11_HssRelaxFsync = 0; +#endif + } + return wp11_HssRelaxFsync ? 0 : 1; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Map RFC 8554 LMS typecode → height. Returns 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; + } +} + +/* Map 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 RFC 8554 LMOTS typecode → Winternitz parameter. Returns 0 on unknown. */ +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; + } +} + +/* Map Winternitz → LMOTS typecode. */ +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; + } +} + +/* Translate a CK_HSS_PARAMS struct to wolfSSL (levels, height, winternitz) + * arguments. wolfSSL's wc_LmsKey_SetParameters requires uniform parameters + * across all HSS levels; mixed-(H,W) configurations are rejected with + * BAD_FUNC_ARG which the C-layer maps to CKR_MECHANISM_PARAM_INVALID. + * + * @param params May be NULL/0 → defaults applied. + * @param paramsLen Length of caller-supplied buffer (sanity check). + * @return 0 on success, BAD_FUNC_ARG on invalid input. */ +static int wp11_HssTranslateParams(const CK_HSS_PARAMS* params, + CK_ULONG paramsLen, int* levels, int* height, int* winternitz) +{ + if (levels == NULL || height == NULL || winternitz == NULL) + return BAD_FUNC_ARG; + + if (params == NULL || paramsLen == 0) { + /* Default: 1 level, height 10, Winternitz 8 (1024 sigs, ~3 KiB sig) */ + *levels = 1; + *height = 10; + *winternitz = 8; + return 0; + } + if (paramsLen != sizeof(CK_HSS_PARAMS)) + return BAD_FUNC_ARG; + if (params->ulLevels < 1 || params->ulLevels > 8) + return BAD_FUNC_ARG; + + { + int h, w, i; + h = wp11_HssLmsTypeToHeight(params->lm_type[0]); + w = wp11_HssLmotsTypeToW(params->lm_ots_type[0]); + if (h == 0 || w == 0) + return BAD_FUNC_ARG; + /* Reject mixed parameters across levels (wolfSSL limitation). */ + for (i = 1; i < (int)params->ulLevels; i++) { + if (wp11_HssLmsTypeToHeight(params->lm_type[i]) != h) + return BAD_FUNC_ARG; + if (wp11_HssLmotsTypeToW(params->lm_ots_type[i]) != w) + return BAD_FUNC_ARG; + } + *levels = (int)params->ulLevels; + *height = h; + *winternitz = w; + } + return 0; +} + +/* Pack the shell header into a buffer. Returns the number of bytes written. */ +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; + /* magic */ + out[idx++] = (byte)(WP11_HSS_SHELL_MAGIC >> 24); + out[idx++] = (byte)(WP11_HSS_SHELL_MAGIC >> 16); + out[idx++] = (byte)(WP11_HSS_SHELL_MAGIC >> 8); + out[idx++] = (byte)(WP11_HSS_SHELL_MAGIC); + /* version */ + out[idx++] = (byte)(WP11_HSS_SHELL_VERSION >> 24); + out[idx++] = (byte)(WP11_HSS_SHELL_VERSION >> 16); + out[idx++] = (byte)(WP11_HSS_SHELL_VERSION >> 8); + out[idx++] = (byte)(WP11_HSS_SHELL_VERSION); + /* levels / height / winternitz */ + out[idx++] = (byte)((levels >> 24) & 0xFF); + out[idx++] = (byte)((levels >> 16) & 0xFF); + out[idx++] = (byte)((levels >> 8) & 0xFF); + out[idx++] = (byte)( levels & 0xFF); + out[idx++] = (byte)((height >> 24) & 0xFF); + out[idx++] = (byte)((height >> 16) & 0xFF); + out[idx++] = (byte)((height >> 8) & 0xFF); + out[idx++] = (byte)( height & 0xFF); + out[idx++] = (byte)((winternitz >> 24) & 0xFF); + out[idx++] = (byte)((winternitz >> 16) & 0xFF); + out[idx++] = (byte)((winternitz >> 8) & 0xFF); + out[idx++] = (byte)( winternitz & 0xFF); + /* pubLen + pub */ + out[idx++] = (byte)((pubLen >> 24) & 0xFF); + out[idx++] = (byte)((pubLen >> 16) & 0xFF); + out[idx++] = (byte)((pubLen >> 8) & 0xFF); + out[idx++] = (byte)( pubLen & 0xFF); + if (pub != NULL && pubLen > 0) + XMEMCPY(out + idx, pub, pubLen); + idx += pubLen; + return (int)idx; +} + +/* Helpers for big-endian 32-bit reads from a byte buffer. */ +static word32 wp11_HssReadU32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; +} + +/* Parse the shell header. On success, *pub is set to the in-buffer pub + * pointer (no allocation) and *pubLen 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_HssReadU32(in + idx); idx += 4; + version = wp11_HssReadU32(in + idx); idx += 4; + if (magic != WP11_HSS_SHELL_MAGIC || version != WP11_HSS_SHELL_VERSION) + return BAD_FUNC_ARG; + *levels = (int)wp11_HssReadU32(in + idx); idx += 4; + *height = (int)wp11_HssReadU32(in + idx); idx += 4; + *winternitz = (int)wp11_HssReadU32(in + idx); idx += 4; + pl = wp11_HssReadU32(in + idx); idx += 4; + if (idx + pl > inLen) + return BAD_FUNC_ARG; + *pub = in + idx; + *pubLen = pl; + return 0; +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Persist the encrypted HSS private state file. Always uses durable mode + * (fsync + rename + fsync(parent)) unless WOLFPKCS11_HSS_RELAX_FSYNC=1. + * + * Layout written to disk: + * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] + * [u32 ivLen][iv (12 bytes)] + * [u32 ctLen][ciphertext + 16-byte AES-GCM tag] + * The first 20 bytes (magic..winternitz) are bound into the GCM tag via AAD. + */ +static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, + word32 privSz) +{ + int ret; + void* storage = NULL; + int levels = 0, height = 0, winternitz = 0; + byte iv[WP11_HSS_STATE_IV_LEN]; + byte hdr[20]; + word32 hdrIdx = 0; + byte* ct = NULL; + word32 ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ + word32 totalLen; + int tokenId, objId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + + /* Build the AAD-bound header. */ + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC >> 24); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC >> 16); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC >> 8); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION >> 24); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION >> 16); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION >> 8); + hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION); + hdr[hdrIdx++] = (byte)((levels >> 24) & 0xFF); + hdr[hdrIdx++] = (byte)((levels >> 16) & 0xFF); + hdr[hdrIdx++] = (byte)((levels >> 8) & 0xFF); + hdr[hdrIdx++] = (byte)( levels & 0xFF); + hdr[hdrIdx++] = (byte)((height >> 24) & 0xFF); + hdr[hdrIdx++] = (byte)((height >> 16) & 0xFF); + hdr[hdrIdx++] = (byte)((height >> 8) & 0xFF); + hdr[hdrIdx++] = (byte)( height & 0xFF); + hdr[hdrIdx++] = (byte)((winternitz >> 24) & 0xFF); + hdr[hdrIdx++] = (byte)((winternitz >> 16) & 0xFF); + hdr[hdrIdx++] = (byte)((winternitz >> 8) & 0xFF); + hdr[hdrIdx++] = (byte)( winternitz & 0xFF); + + /* Fresh GCM nonce per write (NEVER reuse object->iv). */ + ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_HSS_STATE_IV_LEN); + if (ret != 0) + return ret; + + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) + return MEMORY_E; + + ret = wp11_EncryptDataAAD(ct, priv, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_HSS_STATE_IV_LEN, hdr, (int)sizeof(hdr), o->devId); + if (ret != 0) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag */ + totalLen = (word32)sizeof(hdr) + 4 + WP11_HSS_STATE_IV_LEN + 4 + ctLen; + + tokenId = (int)o->slot->id; + objId = (int)o->handle; + + ret = wp11_storage_open(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + (CK_ULONG)tokenId, (CK_ULONG)objId, (int)totalLen, &storage); + if (ret == 0) { + if (wp11_HssShouldFsync()) + wolfPKCS11_Store_SetDurable(storage, 1); + if (ret == 0) + ret = wp11_storage_write(storage, hdr, (int)sizeof(hdr)); + if (ret == 0) { + byte ivLenBuf[4]; + ivLenBuf[0] = 0; ivLenBuf[1] = 0; ivLenBuf[2] = 0; + ivLenBuf[3] = (byte)WP11_HSS_STATE_IV_LEN; + ret = wp11_storage_write(storage, ivLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, iv, WP11_HSS_STATE_IV_LEN); + if (ret == 0) { + byte ctLenBuf[4]; + ctLenBuf[0] = (byte)((ctLen >> 24) & 0xFF); + ctLenBuf[1] = (byte)((ctLen >> 16) & 0xFF); + ctLenBuf[2] = (byte)((ctLen >> 8) & 0xFF); + ctLenBuf[3] = (byte)( ctLen & 0xFF); + ret = wp11_storage_write(storage, ctLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, ct, (int)ctLen); + wp11_storage_close(storage); + } + + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +/* Read and decrypt the HSS private state file into priv[privSz]. The header + * AAD is verified by the AES-GCM tag, so any tampering is detected. */ +static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte hdr[20]; + int levels = 0, height = 0, winternitz = 0; + int expectedLevels = 0, expectedHeight = 0, expectedW = 0; + byte iv[WP11_HSS_STATE_IV_LEN]; + byte* ct = NULL; + word32 magic, version, ivLen, ctLen; + int tokenId, objId; + + if (o == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &expectedLevels, + &expectedHeight, &expectedW); + if (ret != 0) + return ret; + + tokenId = (int)o->slot->id; + objId = (int)o->handle; + + ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); + if (ret != 0) + return ret; + + ret = wp11_storage_read(storage, hdr, (int)sizeof(hdr)); + if (ret == 0) { + magic = wp11_HssReadU32(hdr); + version = wp11_HssReadU32(hdr + 4); + levels = (int)wp11_HssReadU32(hdr + 8); + height = (int)wp11_HssReadU32(hdr + 12); + winternitz = (int)wp11_HssReadU32(hdr + 16); + if (magic != WP11_HSS_STATE_MAGIC || + version != WP11_HSS_STATE_VERSION || + levels != expectedLevels || + height != expectedHeight || + winternitz != expectedW) { + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ivLen = wp11_HssReadU32(buf); + if (ivLen != WP11_HSS_STATE_IV_LEN) + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) + ret = wp11_storage_read(storage, iv, WP11_HSS_STATE_IV_LEN); + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ctLen = wp11_HssReadU32(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_HSS_STATE_IV_LEN, hdr, (int)sizeof(hdr), 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); + } + if (ret != 0) + wc_ForceZero(priv, privSz); + return ret; +} + +/* wolfSSL write callback — invoked by wc_LmsKey_MakeKey and wc_LmsKey_Sign + * after each state advance. Returns 0 on success; non-zero aborts the sign + * (no signature is released to the caller). */ +static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + if (o == NULL) + return BAD_FUNC_ARG; + return wp11_Hss_WriteStateBlob(o, priv, privSz); +} + +static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + if (o == NULL) + return BAD_FUNC_ARG; + return wp11_Hss_ReadStateBlob(o, priv, privSz); +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Decode the HSS public key (or, for private keys, the shell file: read + * parameters + cached pub, init the wolfSSL key, and Reload state via the + * read callback). */ +static int wp11_Object_Decode_HssKey(WP11_Object* object) +{ + int ret = 0; + + if (object == NULL || object->data.lmsKey == NULL) + return BAD_FUNC_ARG; + + if (object->objClass == CKO_PUBLIC_KEY) { + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); + if (ret == 0) { + /* For pub keys we try to import directly using stored params. + * The shell file (if any) carries the parameter triple; in + * the simple pub-key-only case we expect the keyData buffer + * to start with a packed shell header so we can recover the + * parameters. */ + int levels = 0, height = 0, winternitz = 0; + const byte* pub = NULL; + word32 pubLen = 0; + ret = wp11_HssParseShellHeader(object->keyData, + (word32)object->keyDataLen, &levels, &height, &winternitz, + &pub, &pubLen); + if (ret == 0) + ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, + height, winternitz); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(object->data.lmsKey, pub, pubLen); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } + } + else if (object->objClass == CKO_PRIVATE_KEY) { +#ifdef WOLFPKCS11_LMS_PRIVATE + int levels = 0, height = 0, winternitz = 0; + const byte* pub = NULL; + word32 pubLen = 0; + ret = wp11_HssParseShellHeader(object->keyData, + (word32)object->keyDataLen, &levels, &height, &winternitz, + &pub, &pubLen); + 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); + 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); + if (ret == 0) { + /* Cross-check the cached pub against the loaded key. Mismatch + * indicates the state file does not belong to this key (or + * tampering), so we refuse to use it. */ + byte loaded[HSS_MAX_PUBLIC_KEY_LEN]; + word32 loadedLen = sizeof(loaded); + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, loaded, + &loadedLen); + if (ret == 0) { + if (loadedLen != pubLen || + WP11_ConstantCompare(loaded, pub, + (int)pubLen) != 1) { + ret = BAD_FUNC_ARG; + } + } + wc_ForceZero(loaded, sizeof(loaded)); + } + if (ret == 0) + object->opFlag |= WP11_FLAG_HSS_STATE_VALID; + else + wc_LmsKey_Free(object->data.lmsKey); +#else + /* In a verify-only build we cannot reload the private state, so + * the key remains effectively dormant: any sign attempt is rejected + * later by the missing CKM_HSS sign capability. */ + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); +#endif + } + object->encoded = (ret != 0); + return ret; +} + +/* Encode the HSS public key (or private shell). For private keys we never + * serialize the state via keyData — the state file is written directly via + * the wolfSSL write callback. The shell carries non-secret metadata. */ +static int wp11_Object_Encode_HssKey(WP11_Object* object) +{ + int ret = 0; + int levels = 0, height = 0, winternitz = 0; + byte pub[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubLen = sizeof(pub); + int hdrLen; + + if (object == NULL || object->data.lmsKey == NULL) + return BAD_FUNC_ARG; + + 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) { + word32 needed = 24 + pubLen; + 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; + } + else { + hdrLen = wp11_HssPackShellHeader(object->keyData, needed, + levels, height, winternitz, pub, pubLen); + if (hdrLen < 0) { + ret = hdrLen; + } + else { + object->keyDataLen = hdrLen; + } + } + } + wc_ForceZero(pub, sizeof(pub)); + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; +} + +static int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + int storeType; + + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; + else + storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + + ret = wp11_storage_open_readonly(storeType, (CK_ULONG)tokenId, + (CK_ULONG)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; + + if (object->keyData == NULL) + ret = wp11_Object_Encode_HssKey(object); + + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; + else + storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + + if (ret == 0) { + ret = wp11_storage_open(storeType, (CK_ULONG)tokenId, + (CK_ULONG)objId, object->keyDataLen, &storage); + } + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +#endif /* WOLFPKCS11_LMS */ + #ifndef NO_DH /** * Decode the DH key. @@ -5394,6 +6218,11 @@ static int wp11_Object_Load(WP11_Object* object, int tokenId, int objId) ret = wp11_Object_Load_MldsaKey(object, tokenId, objId); break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + ret = wp11_Object_Load_HssKey(object, tokenId, objId); + break; + #endif #ifndef NO_DH case CKK_DH: ret = wp11_Object_Load_DhKey(object, tokenId, objId); @@ -5575,6 +6404,11 @@ static int wp11_Object_Store(WP11_Object* object, int tokenId, int objId) ret = wp11_Object_Store_MldsaKey(object, tokenId, objId); break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + ret = wp11_Object_Store_HssKey(object, tokenId, objId); + break; + #endif #ifndef NO_DH case CKK_DH: ret = wp11_Object_Store_DhKey(object, tokenId, objId); @@ -5645,6 +6479,11 @@ static int wp11_Object_Decode(WP11_Object* object) ret = wp11_Object_Decode_MldsaKey(object); break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + ret = wp11_Object_Decode_HssKey(object); + break; + #endif #ifndef NO_DH case CKK_DH: ret = wp11_Object_Decode_DhKey(object); @@ -5823,6 +6662,21 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) storeObjType = WOLFPKCS11_STORE_MLDSAKEY_PUB; break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + if (object->objClass == CKO_PRIVATE_KEY) { + /* HSS private key has TWO on-disk files: shell + state. + * Remove the state file here; the shell file is removed by + * the trailing wp11_storage_remove(storeObjType,...). */ + storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; + (void)wp11_storage_remove(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + tokenId, objId); + } + else { + storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; + } + break; + #endif #ifndef NO_DH case CKK_DH: if (object->objClass == CKO_PRIVATE_KEY) @@ -7605,6 +8459,7 @@ 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: + case WP11_INIT_HSS_SIGN: return WP11_OP_SIGN; case WP11_INIT_HMAC_VERIFY: @@ -7617,6 +8472,7 @@ static int wp11_init_get_op_category(int init) case WP11_INIT_AES_CMAC_VERIFY: case WP11_INIT_TLS_MAC_VERIFY: case WP11_INIT_MLDSA_VERIFY: + case WP11_INIT_HSS_VERIFY: return WP11_OP_VERIFY; default: @@ -8805,6 +9661,15 @@ void WP11_Object_Free(WP11_Object* object) object->data.mldsaKey = NULL; } #endif + #ifdef WOLFPKCS11_LMS + if (object->type == CKK_HSS && object->data.lmsKey != NULL) { + /* wc_LmsKey_Free zeroizes the in-memory state buffer. */ + wc_LmsKey_Free(object->data.lmsKey); + wc_ForceZero(object->data.lmsKey, sizeof(LmsKey)); + XFREE(object->data.lmsKey, NULL, DYNAMIC_TYPE_LMS); + object->data.lmsKey = NULL; + } + #endif #ifndef NO_DH if (object->type == CKK_DH && object->data.dhKey != NULL) { wc_FreeDhKey(&object->data.dhKey->params); @@ -9310,6 +10175,283 @@ int WP11_Object_SetMldsaKey(WP11_Object* object, unsigned char** data, } #endif /* WOLFPKCS11_MLDSA */ +#ifdef WOLFPKCS11_LMS +/** + * Set the HSS key data into the object. + * + * Only public-key import is supported. Private-key import is rejected + * unconditionally (even when WOLFPKCS11_LMS_PRIVATE is defined): a private + * key with a caller-controlled state index is a forgery vector. + * + * @param object HSS object (must already have data.lmsKey allocated). + * @param data[0] CK_HSS_PARAMS pointer (or NULL for default). + * @param data[1] Raw RFC 8554 HSS public key bytes. + * @param len[i] Lengths corresponding to data[i]. + * @return 0 on success; BAD_FUNC_ARG on invalid input or private-key import. + */ +int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) +{ + int ret; + int levels = 0, height = 0, winternitz = 0; + LmsKey* key; + + if (object == NULL || data == NULL || len == NULL) + return BAD_FUNC_ARG; + + /* Reject private-key import unconditionally. */ + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + key = object->data.lmsKey; + + /* data[0] is optional CK_HSS_PARAMS; if absent, the public key blob in + * data[1] is parsed by wolfSSL and we can fall back to defaults. */ + if (data[0] != NULL) { + ret = wp11_HssTranslateParams((const CK_HSS_PARAMS*)data[0], len[0], + &levels, &height, &winternitz); + } + else { + ret = wp11_HssTranslateParams(NULL, 0, &levels, &height, &winternitz); + } + + if (ret == 0) + ret = wc_LmsKey_Init(key, NULL, object->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(key, levels, height, winternitz); + if (ret == 0 && data[1] != NULL) + ret = wc_LmsKey_ImportPubRaw(key, data[1], (word32)len[1]); + + if (ret != 0) + wc_LmsKey_Free(key); + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + + return ret; +} + +/** + * Return signature length for the HSS key. + */ +int 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 (int)sigLen; +} + +/** + * Return public key length for the HSS key. + */ +int WP11_Hss_PubLen(WP11_Object* key) +{ + word32 pubLen = 0; + if (key == NULL || key->data.lmsKey == NULL) + return 0; + if (wc_LmsKey_GetPubLen(key->data.lmsKey, &pubLen) != 0) + return 0; + return (int)pubLen; +} + +/** + * Retrieve the (levels, height, winternitz) of the HSS key. + */ +int WP11_Hss_GetParameters(WP11_Object* key, int* levels, int* height, + int* winternitz) +{ + if (key == NULL || key->data.lmsKey == NULL) + return BAD_FUNC_ARG; + return wc_LmsKey_GetParameters(key->data.lmsKey, levels, height, + winternitz); +} + +/** + * 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; + + if (pub == NULL || pub->data.lmsKey == NULL || stat == NULL) + return BAD_FUNC_ARG; + + if (pub->onToken) + WP11_Lock_LockRO(pub->lock); + + ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, (int)dataLen); + /* wolfSSL distinguishes "bad signature" (returns SIG_VERIFY_E or similar) + * from internal failure. The C-layer caller maps stat=0 to + * CKR_SIGNATURE_INVALID and ret < 0 to CKR_FUNCTION_FAILED. */ + if (ret == 0) + *stat = 1; + else + *stat = 0; + + if (pub->onToken) + WP11_Lock_UnlockRO(pub->lock); + + /* Verify failures (signature invalid) should not bubble up as ret < 0; + * the *stat output is the canonical signal. Only return -ve for + * unrecoverable internal errors. */ + if (ret != 0 && *stat == 0) { + /* Map any wolfSSL return into success-with-stat=0 — the caller + * inspects *stat. */ + ret = 0; + } + return ret; +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/** + * Generate an HSS key pair, persist genesis state durably, and populate the + * public-key object with the matching public key. The private object's + * write/read callbacks are wired in before MakeKey so the genesis state is + * persisted to disk before the call returns. + */ +int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + const CK_HSS_PARAMS* params, CK_ULONG paramsLen, + WP11_Slot* slot) +{ + int ret; + int levels = 0, height = 0, winternitz = 0; + WC_RNG rng; + byte pubBuf[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubLen = sizeof(pubBuf); + + if (pub == NULL || priv == NULL || slot == NULL || + pub->data.lmsKey == NULL || priv->data.lmsKey == NULL) { + return BAD_FUNC_ARG; + } + + if (priv->onToken) + WP11_Lock_LockRW(priv->lock); + + ret = wp11_HssTranslateParams(params, paramsLen, &levels, &height, + &winternitz); + if (ret == 0) + ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, priv->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(priv->data.lmsKey, levels, height, + winternitz); + /* Wire callbacks BEFORE MakeKey so genesis state is persisted via our + * write CB (durable fsync + atomic rename). */ + 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 calls our write CB internally to persist genesis state. */ + ret = wc_LmsKey_MakeKey(priv->data.lmsKey, &rng); + Rng_Free(&rng); + } + + /* Mirror parameters and pubkey 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, levels, 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_HSS_STATE_VALID; + } + else { + wc_LmsKey_Free(priv->data.lmsKey); + } + + wc_ForceZero(pubBuf, sizeof(pubBuf)); + + if (priv->onToken) + WP11_Lock_UnlockRW(priv->lock); + + return ret; +} + +/** + * Sign a raw message with an HSS private key. State is advanced and persisted + * (via write CB) BEFORE the signature is returned to the caller. On any + * failure the in-memory state is poisoned (WP11_FLAG_HSS_STATE_VALID cleared) + * to force a reload from durable storage on the next attempt. + */ +int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, + word32* sigLen, WP11_Object* priv) +{ + int ret = 0; + word32 outLen; + + if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || + sigLen == NULL) { + return BAD_FUNC_ARG; + } + + if (priv->onToken) + WP11_Lock_LockRW(priv->lock); + + if ((priv->opFlag & WP11_FLAG_HSS_STATE_VALID) == 0) { + ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ + } + if (ret == 0) { + outLen = *sigLen; + ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, + (int)dataLen); + if (ret == 0) { + *sigLen = outLen; + } + else { + /* Either the write CB failed (state on disk may be stale; in- + * memory advanced) or the key is exhausted. Either way, refuse + * future signs until the object is reloaded from durable + * storage; zero any partial signature material. */ + priv->opFlag &= ~WP11_FLAG_HSS_STATE_VALID; + XMEMSET(sig, 0, *sigLen); + } + } + + if (priv->onToken) + WP11_Lock_UnlockRW(priv->lock); + + return ret; +} + +/** + * Returns the number of remaining one-time-keys ("signatures left") for the + * HSS key. *remaining is set to 0 on error. + */ +int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) +{ + int n; + if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) + return BAD_FUNC_ARG; + n = wc_LmsKey_SigsLeft(key->data.lmsKey); + if (n < 0) { + *remaining = 0; + return n; + } + *remaining = (word32)n; + return 0; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ +#endif /* WOLFPKCS11_LMS */ + #ifndef NO_DH /** * Set the DH key data into the object. @@ -10401,6 +11543,190 @@ static int MldsaObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, } #endif /* WOLFPKCS11_MLDSA */ +#ifdef WOLFPKCS11_LMS +/** + * Get an HSS object's data as an attribute. + * + * Notes: + * - CKA_VALUE on a private key is *always* CK_UNAVAILABLE_INFORMATION, + * regardless of CKA_EXTRACTABLE / CKA_SENSITIVE. Exposing the wolfSSL + * state buffer would let a duplicate sign at the same indices. + * - CKA_HSS_KEYS_REMAINING returns wc_LmsKey_SigsLeft for private keys. + */ +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: { + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + CK_ULONG total = (CK_ULONG)(sizeof(CK_ULONG) * levels); + CK_ULONG fill; + int i; + 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: { +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + word32 remaining = 0; + int n = wc_LmsKey_SigsLeft(object->data.lmsKey); + if (n < 0) { + *len = CK_UNAVAILABLE_INFORMATION; + } + else { + CK_ULONG v; + remaining = (word32)n; + 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); + } + } + } + else { + *len = CK_UNAVAILABLE_INFORMATION; + } +#else + (void)data; + *len = CK_UNAVAILABLE_INFORMATION; +#endif + break; + } + case CKA_VALUE: + if (object->objClass == CKO_PRIVATE_KEY) { + /* HARDCODED: never expose private state, regardless of any + * EXTRACTABLE/SENSITIVE flags the caller may have set. */ + *len = CK_UNAVAILABLE_INFORMATION; + } + else 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; + } + break; + default: + ret = NOT_AVAILABLE_E; + break; + } + return ret; +} +#endif /* WOLFPKCS11_LMS */ + #ifndef NO_DH /** * Get a DH object's data as an attribute. @@ -10937,6 +12263,11 @@ int WP11_Object_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, byte* data, ret = MldsaObject_GetAttr(object, type, data, len); break; #endif +#ifdef WOLFPKCS11_LMS + case CKK_HSS: + ret = HssObject_GetAttr(object, type, data, len); + break; +#endif #ifndef NO_DH case CKK_DH: ret = DhObject_GetAttr(object, type, data, len); diff --git a/src/slot.c b/src/slot.c index 58e18b00..294b2c73 100644 --- a/src/slot.c +++ b/src/slot.c @@ -369,6 +369,12 @@ static CK_MECHANISM_TYPE mechanismList[] = { CKM_ML_DSA, CKM_HASH_ML_DSA, #endif +#ifdef WOLFPKCS11_LMS + CKM_HSS, +# ifdef WOLFPKCS11_LMS_PRIVATE + CKM_HSS_KEY_PAIR_GEN, +# endif +#endif #ifdef WOLFPKCS11_HKDF CKM_HKDF_DERIVE, CKM_HKDF_DATA, @@ -640,6 +646,24 @@ static CK_MECHANISM_INFO mldsaMechInfo = { CKF_SIGN | CKF_VERIFY }; #endif +#ifdef WOLFPKCS11_LMS +/* HSS public-key sizes are small and parameter-dependent (RFC 8554). For + * mechanism advertising we report a wide envelope: smallest L1/H5 ≈ 60 bytes, + * largest L4/H25 ≈ 60 bytes (HSS pub is fixed-size for a given hash), so + * pick a coarse envelope that fits all parameter combos. */ +static CK_MECHANISM_INFO hssMechInfo = { + 60, 60, + CKF_VERIFY +# ifdef WOLFPKCS11_LMS_PRIVATE + | CKF_SIGN +# endif +}; +# ifdef WOLFPKCS11_LMS_PRIVATE +static CK_MECHANISM_INFO hssKgMechInfo = { + 60, 60, CKF_GENERATE_KEY_PAIR +}; +# endif +#endif #ifdef WOLFPKCS11_HKDF static CK_MECHANISM_INFO hkdfMechInfo = { 1, 16320, CKF_DERIVE @@ -998,6 +1022,16 @@ CK_RV C_GetMechanismInfo(CK_SLOT_ID slotID, CK_MECHANISM_TYPE type, XMEMCPY(pInfo, &mldsaMechInfo, sizeof(CK_MECHANISM_INFO)); break; #endif +#ifdef WOLFPKCS11_LMS + 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_HKDF case CKM_HKDF_DERIVE: XMEMCPY(pInfo, &hkdfMechInfo, sizeof(CK_MECHANISM_INFO)); diff --git a/tests/include.am b/tests/include.am index 6cf5eb1f..05431efc 100644 --- a/tests/include.am +++ b/tests/include.am @@ -26,6 +26,11 @@ noinst_PROGRAMS += tests/rsa_session_persistence_test tests_rsa_session_persistence_test_SOURCES = tests/rsa_session_persistence_test.c tests_rsa_session_persistence_test_LDADD = +check_PROGRAMS += tests/lms_state_persistence_test +noinst_PROGRAMS += tests/lms_state_persistence_test +tests_lms_state_persistence_test_SOURCES = tests/lms_state_persistence_test.c +tests_lms_state_persistence_test_LDADD = + check_PROGRAMS += tests/debug_test noinst_PROGRAMS += tests/debug_test tests_debug_test_SOURCES = tests/debug_test.c @@ -72,6 +77,7 @@ tests_pkcs11mtt_LDADD += src/libwolfpkcs11.la tests_pkcs11str_LDADD += src/libwolfpkcs11.la tests_token_path_test_LDADD += src/libwolfpkcs11.la tests_rsa_session_persistence_test_LDADD += src/libwolfpkcs11.la +tests_lms_state_persistence_test_LDADD += src/libwolfpkcs11.la tests_debug_test_LDADD += src/libwolfpkcs11.la tests_object_id_uniqueness_test_LDADD += src/libwolfpkcs11.la tests_empty_pin_store_test_LDADD += src/libwolfpkcs11.la diff --git a/tests/lms_state_persistence_test.c b/tests/lms_state_persistence_test.c new file mode 100644 index 00000000..b166bf73 --- /dev/null +++ b/tests/lms_state_persistence_test.c @@ -0,0 +1,430 @@ +/* lms_state_persistence_test.c + * + * Copyright (C) 2006-2025 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 + * + * Tests for LMS/HSS state persistence across session cycles. + * + * Stateful one-time hash-based signatures must never re-use a leaf index. + * This test exercises: + * 1. Generate an HSS keypair (1 level / H=5 / W=4 → 32 sigs). + * 2. Sign a message; record CKA_HSS_KEYS_REMAINING. + * 3. C_Finalize and C_Initialize again. + * 4. Find the persisted private key by label. + * 5. Confirm CKA_HSS_KEYS_REMAINING matches the post-step-2 value. + * 6. Sign a different message and verify both signatures. + * 7. Confirm CKA_HSS_KEYS_REMAINING decremented by exactly one. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#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) + +#define CHECK_CKR(rv, msg) \ + do { \ + if ((rv) != CKR_OK) { \ + fprintf(stderr, "%s:%d - %s: 0x%lx FAIL\n", \ + __FILE__, __LINE__, (msg), (unsigned long)(rv)); \ + } \ + } while (0) + +#define CHECK_COND(cond, rv, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d - %s FAIL\n", \ + __FILE__, __LINE__, (msg)); \ + (rv) = CKR_GENERAL_ERROR; \ + } \ + } 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 = "wolfpkcs11lms"; +static byte* soPin = (byte*)"password123456"; +static int soPinLen = 14; +static byte* userPin = (byte*)"wolfpkcs11-test"; +static int userPinLen = 15; + +static CK_BBOOL ckTrue = CK_TRUE; +static CK_KEY_TYPE hssKeyType = CKK_HSS; +static CK_OBJECT_CLASS privClass = CKO_PRIVATE_KEY; +static CK_OBJECT_CLASS pubClass = CKO_PUBLIC_KEY; + +static char hssKeyLabel[] = "test-hss-key"; +static char hssPubLabel[] = "test-hss-pub"; + +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: %s\n", dlerror()); + return -1; + } + func = (CK_C_GetFunctionList)dlsym(dlib, "C_GetFunctionList"); + if (func == NULL) { dlclose(dlib); return -1; } + 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, "GetSlotList"); + } + if (ret == CKR_OK && slotCount > 0) + slot = slotList[0]; + else if (ret == CKR_OK) + ret = CKR_GENERAL_ERROR; + return ret; +} + +static void pkcs11_final(void) +{ + funcList->C_Finalize(NULL); +#ifndef HAVE_PKCS11_STATIC + if (dlib) dlclose(dlib); + dlib = NULL; +#endif +} + +static CK_RV pkcs11_init_token(void) +{ + unsigned char label[32]; + CK_RV ret; + + XMEMSET(label, ' ', sizeof(label)); + XMEMCPY(label, tokenName, XSTRLEN(tokenName)); + ret = funcList->C_InitToken(slot, soPin, soPinLen, label); + CHECK_CKR(ret, "InitToken"); + return ret; +} + +static CK_RV pkcs11_set_user_pin(void) +{ + CK_RV ret; + CK_SESSION_HANDLE s; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, &s); + CHECK_CKR(ret, "OpenSession (PIN setup)"); + if (ret == CKR_OK) { + ret = funcList->C_Login(s, CKU_SO, soPin, soPinLen); + CHECK_CKR(ret, "Login SO"); + if (ret == CKR_OK) { + ret = funcList->C_InitPIN(s, userPin, userPinLen); + CHECK_CKR(ret, "InitPIN"); + } + funcList->C_Logout(s); + funcList->C_CloseSession(s); + } + 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, "OpenSession"); + if (ret == CKR_OK && userPinLen > 0) { + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + CHECK_CKR(ret, "Login USER"); + } + return ret; +} + +static CK_RV gen_hss_key(CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE* pubKey, + CK_OBJECT_HANDLE* privKey) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_HSS_PARAMS params; + CK_ATTRIBUTE pubTmpl[] = { + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_LABEL, hssPubLabel, sizeof(hssPubLabel) - 1 } + }; + CK_ATTRIBUTE privTmpl[] = { + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_LABEL, hssKeyLabel, sizeof(hssKeyLabel) - 1 } + }; + + XMEMSET(¶ms, 0, sizeof(params)); + params.ulLevels = 1; + params.lm_type[0] = CKL_LMS_SHA256_M32_H5; + params.lm_ots_type[0] = CKL_LMOTS_SHA256_N32_W4; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = ¶ms; + mech.ulParameterLen = sizeof(params); + + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl)/sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl)/sizeof(*privTmpl), pubKey, privKey); + CHECK_CKR(ret, "C_GenerateKeyPair (HSS)"); + return ret; +} + +static CK_RV find_hss_priv(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* h) +{ + CK_RV ret; + CK_ULONG count = 0; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &privClass, sizeof(privClass) }, + { CKA_KEY_TYPE, &hssKeyType, sizeof(hssKeyType) }, + { CKA_LABEL, hssKeyLabel, sizeof(hssKeyLabel)-1} + }; + ret = funcList->C_FindObjectsInit(session, tmpl, 3); + CHECK_CKR(ret, "FindObjectsInit (priv)"); + if (ret == CKR_OK) { + ret = funcList->C_FindObjects(session, h, 1, &count); + CHECK_CKR(ret, "FindObjects (priv)"); + } + funcList->C_FindObjectsFinal(session); + if (ret == CKR_OK && count != 1) + ret = CKR_GENERAL_ERROR; + return ret; +} + +static CK_RV find_hss_pub(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* h) +{ + CK_RV ret; + CK_ULONG count = 0; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &pubClass, sizeof(pubClass) }, + { CKA_KEY_TYPE, &hssKeyType, sizeof(hssKeyType) }, + { CKA_LABEL, hssPubLabel, sizeof(hssPubLabel)-1} + }; + ret = funcList->C_FindObjectsInit(session, tmpl, 3); + CHECK_CKR(ret, "FindObjectsInit (pub)"); + if (ret == CKR_OK) { + ret = funcList->C_FindObjects(session, h, 1, &count); + CHECK_CKR(ret, "FindObjects (pub)"); + } + funcList->C_FindObjectsFinal(session); + if (ret == CKR_OK && count != 1) + ret = CKR_GENERAL_ERROR; + return ret; +} + +static CK_RV sign_msg(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE priv, + const byte* msg, CK_ULONG msgLen, + byte* sig, CK_ULONG* sigLen) +{ + CK_RV ret; + CK_MECHANISM mech; + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + ret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR(ret, "C_SignInit (HSS)"); + if (ret == CKR_OK) { + ret = funcList->C_Sign(session, (CK_BYTE_PTR)msg, msgLen, sig, sigLen); + CHECK_CKR(ret, "C_Sign (HSS)"); + } + return ret; +} + +static CK_RV verify_msg(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE pub, + const byte* msg, CK_ULONG msgLen, + const 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); + CHECK_CKR(ret, "C_VerifyInit (HSS)"); + if (ret == CKR_OK) { + ret = funcList->C_Verify(session, (CK_BYTE_PTR)msg, msgLen, + (CK_BYTE_PTR)sig, sigLen); + CHECK_CKR(ret, "C_Verify (HSS)"); + } + return ret; +} + +static CK_RV get_keys_remaining(CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE priv, + CK_ULONG* out) +{ + CK_ATTRIBUTE q; + q.type = CKA_HSS_KEYS_REMAINING; + q.pValue = out; + q.ulValueLen = sizeof(*out); + return funcList->C_GetAttributeValue(session, priv, &q, 1); +} + +static CK_RV lms_state_persistence_test(void) +{ + CK_RV ret; + CK_SESSION_HANDLE s1 = 0, s2 = 0; + CK_OBJECT_HANDLE pub1 = CK_INVALID_HANDLE, priv1 = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub2 = CK_INVALID_HANDLE, priv2 = CK_INVALID_HANDLE; + static const byte msg1[] = "first sign before reinit"; + static const byte msg2[] = "second sign after reinit"; + byte sig1[8192], sig2[8192]; + CK_ULONG sig1Len = sizeof(sig1), sig2Len = sizeof(sig2); + CK_ULONG remaining_after_first = 0, remaining_after_load = 0; + CK_ULONG remaining_after_second = 0; + + ret = pkcs11_init(); + if (ret != CKR_OK) return ret; + ret = pkcs11_init_token(); + if (ret == CKR_OK) + ret = pkcs11_set_user_pin(); + if (ret == CKR_OK) + ret = pkcs11_open_session(&s1); + if (ret == CKR_OK) { + printf("Generating HSS keypair (L=1, H=5, W=4)...\n"); + ret = gen_hss_key(s1, &pub1, &priv1); + } + if (ret == CKR_OK) { + printf("Signing message #1...\n"); + ret = sign_msg(s1, priv1, msg1, sizeof(msg1) - 1, sig1, &sig1Len); + } + if (ret == CKR_OK) + ret = verify_msg(s1, pub1, msg1, sizeof(msg1) - 1, sig1, sig1Len); + if (ret == CKR_OK) { + ret = get_keys_remaining(s1, priv1, &remaining_after_first); + CHECK_CKR(ret, "GetAttr KEYS_REMAINING (1st)"); + printf("After 1st sign, KEYS_REMAINING = %lu (expected 31)\n", + (unsigned long)remaining_after_first); + CHECK_COND(remaining_after_first == 31, ret, + "KEYS_REMAINING != 31 after first sign"); + } + + funcList->C_Logout(s1); + funcList->C_CloseSession(s1); + pkcs11_final(); + + printf("Re-initializing PKCS#11 to load token from disk...\n"); + ret = pkcs11_init(); + if (ret != CKR_OK) return ret; + if (ret == CKR_OK) + ret = pkcs11_open_session(&s2); + if (ret == CKR_OK) { + ret = find_hss_priv(s2, &priv2); + if (ret != CKR_OK) { + fprintf(stderr, "Persisted private key not found\n"); + } + } + if (ret == CKR_OK) + ret = find_hss_pub(s2, &pub2); + if (ret == CKR_OK) { + ret = get_keys_remaining(s2, priv2, &remaining_after_load); + CHECK_CKR(ret, "GetAttr KEYS_REMAINING (after load)"); + printf("After reload, KEYS_REMAINING = %lu (must equal %lu)\n", + (unsigned long)remaining_after_load, + (unsigned long)remaining_after_first); + CHECK_COND(remaining_after_load == remaining_after_first, ret, + "KEYS_REMAINING regressed after reload"); + } + if (ret == CKR_OK) { + printf("Signing message #2...\n"); + ret = sign_msg(s2, priv2, msg2, sizeof(msg2) - 1, sig2, &sig2Len); + } + if (ret == CKR_OK) + ret = verify_msg(s2, pub2, msg2, sizeof(msg2) - 1, sig2, sig2Len); + if (ret == CKR_OK) + ret = verify_msg(s2, pub2, msg1, sizeof(msg1) - 1, sig1, sig1Len); + if (ret == CKR_OK) { + ret = get_keys_remaining(s2, priv2, &remaining_after_second); + CHECK_CKR(ret, "GetAttr KEYS_REMAINING (2nd)"); + printf("After 2nd sign, KEYS_REMAINING = %lu (expected 30)\n", + (unsigned long)remaining_after_second); + CHECK_COND(remaining_after_second == 30, ret, + "KEYS_REMAINING != 30 after second sign"); + } + + /* Cleanup: destroy persisted objects so the test is repeatable. */ + if (priv2 != CK_INVALID_HANDLE) + funcList->C_DestroyObject(s2, priv2); + if (pub2 != CK_INVALID_HANDLE) + funcList->C_DestroyObject(s2, pub2); + funcList->C_Logout(s2); + funcList->C_CloseSession(s2); + pkcs11_final(); + 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", "./store/lms", 1); +#endif + + printf("wolfPKCS11 LMS/HSS State Persistence Test\n"); + printf("=========================================\n\n"); + + ret = lms_state_persistence_test(); + if (ret == CKR_OK) { + printf("\nAll tests passed!\n"); + return 0; + } + fprintf(stderr, "\nTest failed: 0x%lx\n", (unsigned long)ret); + return 1; +#else + printf("LMS_PRIVATE or KeyStore not compiled in!\n"); + return 77; +#endif +} diff --git a/tests/pkcs11v3test.c b/tests/pkcs11v3test.c index 2759657d..f5f42886 100644 --- a/tests/pkcs11v3test.c +++ b/tests/pkcs11v3test.c @@ -1021,6 +1021,289 @@ static CK_RV test_copy_object_mldsa_key(void* args) } #endif /* WOLFPKCS11_MLDSA */ +#ifdef WOLFPKCS11_LMS + +static CK_KEY_TYPE hssKeyType = CKK_HSS; + +/* Build a CK_HSS_PARAMS for a 1-level HSS / LMS_H5 / W4 tree (32 sigs). */ +static void hss_test_make_params(CK_HSS_PARAMS* p) +{ + XMEMSET(p, 0, sizeof(*p)); + p->ulLevels = 1; + p->lm_type[0] = CKL_LMS_SHA256_M32_H5; + p->lm_ots_type[0] = CKL_LMOTS_SHA256_N32_W4; +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Generate an HSS keypair with a small parameter set. Returns the new pub + * and priv handles via out-parameters. Either may be NULL. */ +static CK_RV gen_hss_keys(CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE* outPub, + CK_OBJECT_HANDLE* outPriv, + int onToken) +{ + CK_RV ret; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE priv = CK_INVALID_HANDLE; + CK_MECHANISM mech; + CK_HSS_PARAMS params; + CK_BBOOL token = (CK_BBOOL)onToken; + CK_ATTRIBUTE pubKeyTmpl[] = { + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_TOKEN, &token, sizeof(token) } + }; + CK_ATTRIBUTE privKeyTmpl[] = { + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_TOKEN, &token, sizeof(token) } + }; + + hss_test_make_params(¶ms); + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = ¶ms; + mech.ulParameterLen = sizeof(params); + + ret = funcList->C_GenerateKeyPair(session, &mech, + pubKeyTmpl, sizeof(pubKeyTmpl)/sizeof(*pubKeyTmpl), + privKeyTmpl, sizeof(privKeyTmpl)/sizeof(*privKeyTmpl), &pub, &priv); + CHECK_CKR(ret, "HSS Key Generation"); + + if (ret == CKR_OK && outPub != NULL) *outPub = pub; + if (ret == CKR_OK && outPriv != NULL) *outPriv = priv; + return ret; +} + +/* Sign a message and verify it on the public-key side. */ +static CK_RV hss_sign_verify_one(CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE priv, + CK_OBJECT_HANDLE pub, + const byte* msg, CK_ULONG msgLen) +{ + CK_RV ret; + CK_MECHANISM mech; + byte sig[8192]; + CK_ULONG sigLen = sizeof(sig); + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR(ret, "HSS C_SignInit"); + if (ret == CKR_OK) { + ret = funcList->C_Sign(session, (CK_BYTE_PTR)msg, msgLen, sig, &sigLen); + CHECK_CKR(ret, "HSS C_Sign"); + } + if (ret == CKR_OK) { + ret = funcList->C_VerifyInit(session, &mech, pub); + CHECK_CKR(ret, "HSS C_VerifyInit"); + } + if (ret == CKR_OK) { + ret = funcList->C_Verify(session, (CK_BYTE_PTR)msg, msgLen, sig, + sigLen); + CHECK_CKR(ret, "HSS C_Verify"); + } + /* Negative verify: flip a byte in the signature. */ + if (ret == CKR_OK && sigLen > 0) { + sig[sigLen / 2] ^= 0x55; + ret = funcList->C_VerifyInit(session, &mech, pub); + CHECK_CKR(ret, "HSS C_VerifyInit (bad sig)"); + if (ret == CKR_OK) { + ret = funcList->C_Verify(session, (CK_BYTE_PTR)msg, msgLen, sig, + sigLen); + CHECK_CKR_FAIL(ret, CKR_SIGNATURE_INVALID, "HSS bad-sig verify"); + } + } + return ret; +} + +/* Basic generate + sign + verify roundtrip, in-session keys. */ +static CK_RV test_hss_gen_sign_verify(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static const byte msg[] = "wolfPKCS11 HSS roundtrip test"; + + ret = gen_hss_keys(session, &pub, &priv, 0); + if (ret == CKR_OK) + ret = hss_sign_verify_one(session, priv, pub, msg, sizeof(msg) - 1); + + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + return ret; +} + +/* Decrement of CKA_HSS_KEYS_REMAINING after each sign. */ +static CK_RV test_hss_keys_remaining(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_ULONG remaining = 0; + CK_ATTRIBUTE attr = { CKA_HSS_KEYS_REMAINING, &remaining, + sizeof(remaining) }; + static const byte msg[] = "kr"; + int i; + + ret = gen_hss_keys(session, &pub, &priv, 0); + if (ret == CKR_OK) { + ret = funcList->C_GetAttributeValue(session, priv, &attr, 1); + CHECK_CKR(ret, "HSS GetAttr CKA_HSS_KEYS_REMAINING"); + } + if (ret == CKR_OK) { + /* H=5 → 32 sigs total. */ + CHECK_COND(remaining == 32, ret, "HSS initial keys remaining is 32"); + } + for (i = 0; ret == CKR_OK && i < 4; i++) { + ret = hss_sign_verify_one(session, priv, pub, msg, sizeof(msg) - 1); + if (ret == CKR_OK) { + ret = funcList->C_GetAttributeValue(session, priv, &attr, 1); + CHECK_CKR(ret, "HSS GetAttr after sign"); + if (ret == CKR_OK) { + CHECK_COND(remaining == (CK_ULONG)(32 - 1 - i), ret, + "HSS keys remaining decremented"); + } + } + } + + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + return ret; +} + +/* CKA_VALUE on a private HSS key MUST be CK_UNAVAILABLE_INFORMATION even + * when CKA_EXTRACTABLE = TRUE. Verify with both default and explicit flag. */ +static CK_RV test_hss_priv_value_blocked(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_ATTRIBUTE q; + + ret = gen_hss_keys(session, &pub, &priv, 0); + if (ret == CKR_OK) { + /* Query length only. */ + q.type = CKA_VALUE; + q.pValue = NULL; + q.ulValueLen = 0; + ret = funcList->C_GetAttributeValue(session, priv, &q, 1); + /* Spec: when sensitive/unavailable, ulValueLen is set to + * CK_UNAVAILABLE_INFORMATION and the call may return CKR_OK or + * CKR_ATTRIBUTE_SENSITIVE. We accept either. */ + CHECK_COND(q.ulValueLen == CK_UNAVAILABLE_INFORMATION, ret, + "HSS private CKA_VALUE returns UNAVAILABLE"); + } + /* Try setting CKA_EXTRACTABLE = TRUE and re-query — must still be blocked. */ + if (ret == CKR_OK) { + CK_BBOOL t = CK_TRUE; + CK_ATTRIBUTE setExtractable = { CKA_EXTRACTABLE, &t, sizeof(t) }; + (void)funcList->C_SetAttributeValue(session, priv, &setExtractable, 1); + q.type = CKA_VALUE; + q.pValue = NULL; + q.ulValueLen = 0; + ret = funcList->C_GetAttributeValue(session, priv, &q, 1); + CHECK_COND(q.ulValueLen == CK_UNAVAILABLE_INFORMATION, ret, + "HSS private CKA_VALUE blocked even with EXTRACTABLE=TRUE"); + } + + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + return ret; +} + +/* Copy of an HSS private key must be rejected. */ +static CK_RV test_hss_copy_priv_rejected(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE copy = CK_INVALID_HANDLE; + + ret = gen_hss_keys(session, &pub, &priv, 0); + if (ret == CKR_OK) { + ret = funcList->C_CopyObject(session, priv, NULL, 0, ©); + /* Copy is rejected; exact CK_RV depends on internal mapping but + * MUST not be CKR_OK. */ + CHECK_COND(ret != CKR_OK, ret, "HSS copy of private key rejected"); + ret = (ret != CKR_OK) ? CKR_OK : CKR_FUNCTION_FAILED; + } + if (copy != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, copy); + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + return ret; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Private-key import is rejected unconditionally (even when LMS_PRIVATE on). + * This guards against an attacker installing a state-forked private key. */ +static CK_RV test_hss_priv_import_rejected(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_OBJECT_CLASS cls = CKO_PRIVATE_KEY; + CK_BYTE bogus[64]; + CK_OBJECT_HANDLE h = CK_INVALID_HANDLE; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &cls, sizeof(cls) }, + { CKA_KEY_TYPE, &hssKeyType, sizeof(hssKeyType) }, + { CKA_VALUE, bogus, sizeof(bogus) } + }; + XMEMSET(bogus, 0xAB, sizeof(bogus)); + + ret = funcList->C_CreateObject(session, tmpl, + sizeof(tmpl)/sizeof(*tmpl), &h); + CHECK_CKR_FAIL(ret, CKR_ATTRIBUTE_VALUE_INVALID, + "HSS private-key import rejected"); + if (ret == CKR_ATTRIBUTE_VALUE_INVALID) + ret = CKR_OK; + if (h != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, h); + return ret; +} + +#ifndef WOLFPKCS11_LMS_PRIVATE +/* In a verify-only build, attempting to keygen must report + * CKR_MECHANISM_INVALID since CKM_HSS_KEY_PAIR_GEN is not advertised. */ +static CK_RV test_hss_verify_only_no_keygen(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_MECHANISM mech; + CK_HSS_PARAMS params; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_ATTRIBUTE pubKeyTmpl[] = { + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) } + }; + CK_ATTRIBUTE privKeyTmpl[] = { + { CKA_SIGN, &ckTrue, sizeof(ckTrue) } + }; + + hss_test_make_params(¶ms); + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = ¶ms; + mech.ulParameterLen = sizeof(params); + + ret = funcList->C_GenerateKeyPair(session, &mech, + pubKeyTmpl, 1, privKeyTmpl, 1, &pub, &priv); + CHECK_CKR_FAIL(ret, CKR_MECHANISM_INVALID, + "HSS keygen rejected in verify-only build"); + if (ret == CKR_MECHANISM_INVALID) + ret = CKR_OK; + return ret; +} +#endif /* !WOLFPKCS11_LMS_PRIVATE */ + +#endif /* WOLFPKCS11_LMS */ + #ifdef WOLFPKCS11_MLKEM static CK_KEY_TYPE mlkemKeyType = CKK_ML_KEM; @@ -2775,6 +3058,17 @@ static TEST_FUNC testFunc[] = { PKCS11TEST_FUNC_SESS_DECL(test_mldsa_fixed_keys_both), PKCS11TEST_FUNC_SESS_DECL(test_copy_object_mldsa_key), #endif +#ifdef WOLFPKCS11_LMS + PKCS11TEST_FUNC_SESS_DECL(test_hss_priv_import_rejected), +# ifdef WOLFPKCS11_LMS_PRIVATE + PKCS11TEST_FUNC_SESS_DECL(test_hss_gen_sign_verify), + PKCS11TEST_FUNC_SESS_DECL(test_hss_keys_remaining), + PKCS11TEST_FUNC_SESS_DECL(test_hss_priv_value_blocked), + PKCS11TEST_FUNC_SESS_DECL(test_hss_copy_priv_rejected), +# else + PKCS11TEST_FUNC_SESS_DECL(test_hss_verify_only_no_keygen), +# endif +#endif #ifdef WOLFPKCS11_MLKEM PKCS11TEST_FUNC_SESS_DECL(test_mlkem_gen_keys), PKCS11TEST_FUNC_SESS_DECL(test_mlkem_gen_keys_id), diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 43518e88..4f505eef 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -36,6 +36,10 @@ #include #endif +#ifdef WOLFPKCS11_LMS +#include +#endif + #include #include @@ -117,6 +121,30 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #error Compiling with ML-KEM requires ML-KEM support in wolfSSL. #endif +#if defined(WOLFPKCS11_LMS) && !defined(WOLFSSL_HAVE_LMS) +#error WOLFPKCS11_LMS requires wolfSSL built with --enable-lms (WOLFSSL_HAVE_LMS). +#endif + +/* wolfSSL master removed the standalone WOLFSSL_WC_LMS toggle; the wolfCrypt + * implementation is now the default when --enable-lms is passed. Sign/keygen + * are gated by the absence of WOLFSSL_LMS_VERIFY_ONLY. */ +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFSSL_LMS_VERIFY_ONLY) +#error WOLFPKCS11_LMS_PRIVATE requires wolfSSL built without --enable-lms=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 storage; do not combine with WOLFPKCS11_NO_STORE. +#endif + +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_TPM_STORE) +#error WOLFPKCS11_LMS_PRIVATE is not supported with WOLFPKCS11_TPM_STORE \ + (HSS state files are too large for TPM NV). +#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 @@ -218,6 +246,11 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_FLAG_DERIVE 0x00040000 #define WP11_FLAG_ENCAPSULATE 0x00080000 #define WP11_FLAG_DECAPSULATE 0x00100000 +/* Internal poison/reload marker for HSS private keys. Set after a successful + * keygen or wc_LmsKey_Reload; cleared on any state-write failure or sign + * error. When clear, signing is refused with CKR_DEVICE_ERROR until the key + * is reloaded from durable storage. */ +#define WP11_FLAG_HSS_STATE_VALID 0x00200000 /* Flags for token. */ #define WP11_TOKEN_FLAG_USER_PIN_SET 0x00000001 @@ -265,6 +298,8 @@ 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 +#define WP11_INIT_HSS_VERIFY 0x0091 /* Operation categories for CKR_OPERATION_ACTIVE checks */ #define WP11_OP_ENCRYPT 0 @@ -545,6 +580,27 @@ WP11_LOCAL int WP11_Mldsa_Verify(unsigned char* sig, word32 sigLen, unsigned cha word32 dataLen, int* stat, WP11_Object* pub, WP11_Session* session); +#ifdef WOLFPKCS11_LMS +WP11_LOCAL int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len); +WP11_LOCAL int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, + unsigned char* data, word32 dataLen, int* stat, + WP11_Object* pub); +WP11_LOCAL int WP11_Hss_SigLen(WP11_Object* key); +WP11_LOCAL int WP11_Hss_PubLen(WP11_Object* key); +WP11_LOCAL int WP11_Hss_GetParameters(WP11_Object* key, int* levels, int* height, + int* winternitz); +#endif +#ifdef WOLFPKCS11_LMS_PRIVATE +WP11_LOCAL int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + const CK_HSS_PARAMS* params, + CK_ULONG paramsLen, 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 + WP11_LOCAL int WP11_Dh_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, WP11_Slot* slot); WP11_LOCAL int WP11_Dh_Derive(unsigned char* pub, word32 pubLen, unsigned char* key, diff --git a/wolfpkcs11/pkcs11.h b/wolfpkcs11/pkcs11.h index 925f957b..437da0d8 100644 --- a/wolfpkcs11/pkcs11.h +++ b/wolfpkcs11/pkcs11.h @@ -178,6 +178,7 @@ extern "C" { #define CKK_HKDF 0x00000042UL #define CKK_ML_KEM 0x00000049UL #define CKK_ML_DSA 0x0000004AUL +#define CKK_HSS 0x00000046UL #ifdef WOLFPKCS11_NSS /* Not defined by NSS, but we need one */ @@ -262,6 +263,13 @@ extern "C" { /* KEM */ #define CKA_ENCAPSULATE 0x00000633UL #define CKA_DECAPSULATE 0x00000634UL +/* LMS/HSS (RFC 8554) */ +#define CKA_HSS_LEVELS 0x00000617UL +#define CKA_HSS_LMS_TYPE 0x00000618UL +#define CKA_HSS_LMOTS_TYPE 0x00000619UL +#define CKA_HSS_LMS_TYPES 0x0000061AUL +#define CKA_HSS_LMOTS_TYPES 0x0000061BUL +#define CKA_HSS_KEYS_REMAINING 0x0000061CUL #ifdef WOLFPKCS11_NSS #define CKA_NSS_EMAIL (CKA_NSS + 2) @@ -365,6 +373,8 @@ extern "C" { #define CKM_ML_DSA_KEY_PAIR_GEN 0x0000001CUL #define CKM_ML_DSA 0x0000001DUL #define CKM_HASH_ML_DSA 0x0000001FUL +#define CKM_HSS_KEY_PAIR_GEN 0x00004032UL +#define CKM_HSS 0x00004033UL #ifdef WOLFPKCS11_NSS #define CKM_NSS_TLS_PRF_GENERAL_SHA256 (CKM_NSS + 21) @@ -877,6 +887,36 @@ typedef CK_ULONG CK_ML_KEM_PARAMETER_SET_TYPE; #define CKP_ML_KEM_768 0x00000002UL #define CKP_ML_KEM_1024 0x00000003UL +/* HSS / LMS / LMOTS algorithm identifiers (RFC 8554) used in CK_HSS_PARAMS. */ +typedef CK_ULONG CK_HSS_LEVELS; +typedef CK_ULONG CK_LMS_TYPE; +typedef CK_ULONG CK_LMOTS_TYPE; + +/* RFC 8554 LMS typecodes (subset supported by wolfSSL) */ +#define CKL_LMS_SHA256_M32_H5 0x00000005UL +#define CKL_LMS_SHA256_M32_H10 0x00000006UL +#define CKL_LMS_SHA256_M32_H15 0x00000007UL +#define CKL_LMS_SHA256_M32_H20 0x00000008UL +#define CKL_LMS_SHA256_M32_H25 0x00000009UL + +/* RFC 8554 LMOTS typecodes (subset supported by wolfSSL) */ +#define CKL_LMOTS_SHA256_N32_W1 0x00000001UL +#define CKL_LMOTS_SHA256_N32_W2 0x00000002UL +#define CKL_LMOTS_SHA256_N32_W4 0x00000003UL +#define CKL_LMOTS_SHA256_N32_W8 0x00000004UL + +/* PKCS#11 v3.2 specifiedParams for CKM_HSS_KEY_PAIR_GEN. The `lm_type` and + * `lm_ots_type` arrays carry one entry per HSS level (1..ulLevels-1 unused + * entries are ignored). wolfSSL requires uniform (height, winternitz) across + * levels in its current API; mixed parameters are rejected with + * CKR_MECHANISM_PARAM_INVALID. */ +typedef struct CK_HSS_PARAMS { + CK_HSS_LEVELS ulLevels; /* 1..WOLFSSL_LMS_MAX_LEVELS */ + CK_LMS_TYPE lm_type[8]; /* per-level LMS typecode */ + CK_LMOTS_TYPE lm_ots_type[8]; /* per-level LMOTS typecode */ +} CK_HSS_PARAMS; +typedef CK_HSS_PARAMS CK_PTR CK_HSS_PARAMS_PTR; + /* Function list types. */ typedef struct CK_FUNCTION_LIST CK_FUNCTION_LIST; diff --git a/wolfpkcs11/store.h b/wolfpkcs11/store.h index 12bf120e..7abc4f57 100644 --- a/wolfpkcs11/store.h +++ b/wolfpkcs11/store.h @@ -40,6 +40,9 @@ #define WOLFPKCS11_STORE_MLDSAKEY_PUB 0x0D #define WOLFPKCS11_STORE_MLKEMKEY_PRIV 0x0E #define WOLFPKCS11_STORE_MLKEMKEY_PUB 0x0F +#define WOLFPKCS11_STORE_HSSKEY_PUB 0x10 +#define WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL 0x11 +#define WOLFPKCS11_STORE_HSSKEY_PRIV_STATE 0x12 /* * Opens access to location to read/write token data. From 3b2f97c07fbf38b5b235a8196499cbb6a17660fe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 06:11:34 +0000 Subject: [PATCH 2/6] Fix LMS/HSS keygen, state persistence, and reload paths - Add CKK_HSS dispatch to wp11_Object_Encode so token-resident HSS keys can be serialized via the standard slot-store flow. - Add HSS PKCS#11 attribute types (CKA_HSS_*) to the known-attribute table so C_GetAttributeValue(CKA_HSS_KEYS_REMAINING) returns the count instead of CKR_ATTRIBUTE_TYPE_INVALID. - Compute CKA_HSS_KEYS_REMAINING by reading q from priv_raw and subtracting from 2^(levels*height). wc_LmsKey_SigsLeft is a boolean, not a count. - Return WC_LMS_RC_SAVED_TO_NV_MEMORY / WC_LMS_RC_READ_TO_MEMORY from the wolfSSL CBs (returning 0 trips IO_FAILED_E inside MakeKey/Sign). - Defer the genesis state write into a heap buffer when the object's slot handle has not yet been assigned, then flush via WP11_Hss_FlushDeferredState after AddObject runs. Skip the disk write entirely for session-only keys. - Match wp11_Token_Store's sequential storage index when naming the state file so the shell and state files share an objId. The previous code used the handle's low bits, which differs from the iteration index. - After wc_LmsKey_Reload, restore the cached pub into key->pub. Reload only rebuilds private state, leaving key->pub zero, which broke ExportPubRaw and any cross-check on the loaded key. - Allow WP11_Object_SetHssKey to be called with empty data during NewObject for both private and public objects (the import-rejection rule still fires when caller-controlled bytes are supplied for a private object). All three configurations (--enable-lms-private, --enable-lms, default) build cleanly and pass make check (13/13, 12/12, 12/12 respectively). https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- src/crypto.c | 20 +++ src/internal.c | 246 +++++++++++++++++++++++------ tests/lms_state_persistence_test.c | 5 + tests/pkcs11v3test.c | 6 +- wolfpkcs11/internal.h | 1 + wolfpkcs11/pkcs11.h | 2 +- 6 files changed, 232 insertions(+), 48 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 22bb5b64..a0c01583 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -266,6 +266,14 @@ static AttributeType attrType[] = { { CKA_SEED, ATTR_TYPE_DATA }, { CKA_ENCAPSULATE, ATTR_TYPE_BOOL }, { CKA_DECAPSULATE, ATTR_TYPE_BOOL }, +#ifdef WOLFPKCS11_LMS + { CKA_HSS_LEVELS, ATTR_TYPE_ULONG }, + { CKA_HSS_LMS_TYPE, ATTR_TYPE_ULONG }, + { CKA_HSS_LMOTS_TYPE, ATTR_TYPE_ULONG }, + { CKA_HSS_LMS_TYPES, ATTR_TYPE_DATA }, + { CKA_HSS_LMOTS_TYPES, ATTR_TYPE_DATA }, + { CKA_HSS_KEYS_REMAINING, ATTR_TYPE_ULONG }, +#endif #ifdef WOLFPKCS11_NSS { CKA_CERT_SHA1_HASH, ATTR_TYPE_DATA }, { CKA_CERT_MD5_HASH, ATTR_TYPE_DATA }, @@ -7478,6 +7486,18 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, rv = AddObject(session, priv, pPrivateKeyTemplate, ulPrivateKeyAttributeCount, phPrivateKey); } +#ifdef WOLFPKCS11_LMS_PRIVATE + /* HSS keygen writes the genesis state via the wolfSSL write CB during + * MakeKey, BEFORE the object has a slot handle. The CB stashes those + * bytes; once AddObject has assigned the handle, flush them to the + * durable state file. If this fails, the keygen is rolled back: the + * object cannot be used to sign without a valid persisted state. */ + if (rv == CKR_OK && priv != NULL && pMechanism != NULL && + pMechanism->mechanism == CKM_HSS_KEY_PAIR_GEN) { + if (WP11_Hss_FlushDeferredState(priv) != 0) + rv = CKR_FUNCTION_FAILED; + } +#endif if (pub != NULL && rv == CKR_OK) { rv = SetInitialStates(pub); diff --git a/src/internal.c b/src/internal.c index c4fc0b1d..23d827e1 100644 --- a/src/internal.c +++ b/src/internal.c @@ -47,7 +47,6 @@ #endif #ifdef WOLFPKCS11_LMS #include -#include #endif #if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) @@ -295,6 +294,14 @@ struct WP11_Object { byte iv[GCM_NONCE_MID_SZ]; /* IV/nonce for encrypt/decrypt */ byte encoded:1; /* Key isn't in decoded form */ #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + /* Genesis-state buffer used when wolfSSL invokes the write callback + * during wc_LmsKey_MakeKey, before the object has been added to the + * slot (handle == 0). The buffer is flushed to durable storage by + * WP11_Hss_FlushDeferredState() once AddObject has assigned a handle. */ + unsigned char* hssDeferredState; + word32 hssDeferredStateLen; +#endif WP11_Session* session; /* Session object belongs to */ WP11_Slot* slot; /* Slot object belongs to */ @@ -1122,7 +1129,7 @@ typedef struct WP11_FileStoreCtx { /* Mark a write-mode file storage context as requiring full durability * (fsync of file data + directory) before the close path returns success. * Must be called after wolfPKCS11_Store_OpenSz returned, before writes. */ -WP11_LOCAL void wolfPKCS11_Store_SetDurable(void* store, int durable) +static void wolfPKCS11_Store_SetDurable(void* store, int durable) { WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; if (ctx != NULL) @@ -4977,6 +4984,34 @@ static int wp11_HssParseShellHeader(const byte* in, word32 inLen, * [u32 ctLen][ciphertext + 16-byte AES-GCM tag] * The first 20 bytes (magic..winternitz) are bound into the GCM tag via AAD. */ +/* Compute the storage objId used by the token-serialization loop for this + * object. wp11_Token_Store iterates from objCnt-1 down to 0 walking the + * linked list head->next; the index assigned to each object equals its + * position from the tail. We MUST match that mapping or the state file + * lives under a different index than the shell file. */ +static int wp11_Hss_StorageObjIdx(WP11_Object* o) +{ + int idx; + int total; + WP11_Object* cur; + WP11_Token* tok; + + if (o == NULL || o->slot == NULL) + return -1; + tok = &o->slot->token; + total = 0; + for (cur = tok->object; cur != NULL; cur = cur->next) + total++; + if (total == 0) + return -1; + idx = total - 1; + for (cur = tok->object; cur != NULL && cur != o; cur = cur->next) + idx--; + if (cur != o) + return -1; + return idx; +} + static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, word32 privSz) { @@ -5043,7 +5078,12 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, totalLen = (word32)sizeof(hdr) + 4 + WP11_HSS_STATE_IV_LEN + 4 + ctLen; tokenId = (int)o->slot->id; - objId = (int)o->handle; + objId = wp11_Hss_StorageObjIdx(o); + if (objId < 0) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } ret = wp11_storage_open(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, (CK_ULONG)tokenId, (CK_ULONG)objId, (int)totalLen, &storage); @@ -5101,7 +5141,9 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) return ret; tokenId = (int)o->slot->id; - objId = (int)o->handle; + objId = wp11_Hss_StorageObjIdx(o); + if (objId < 0) + return BAD_FUNC_ARG; ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); @@ -5169,22 +5211,81 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) } /* wolfSSL write callback — invoked by wc_LmsKey_MakeKey and wc_LmsKey_Sign - * after each state advance. Returns 0 on success; non-zero aborts the sign - * (no signature is released to the caller). */ + * after each state advance. Must return WC_LMS_RC_SAVED_TO_NV_MEMORY on + * success; any other value aborts the sign (no signature is released). + * + * During keygen, MakeKey fires this callback before the object has been + * registered with the slot (handle == 0). Stash the genesis state in a + * heap buffer and rely on WP11_Hss_FlushDeferredState() being called by + * the keygen path after AddObject assigns a real handle. */ static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) { WP11_Object* o = (WP11_Object*)ctx; - if (o == NULL) + if (o == NULL || priv == NULL || privSz == 0) + return WC_LMS_RC_BAD_ARG; + + /* During keygen the object hasn't been added to the slot yet (handle + * still 0). We can't decide yet whether it's a token key — the caller + * has the CKA_TOKEN attribute. Stash the genesis state in a heap buffer; + * the keygen postlude calls WP11_Hss_FlushDeferredState() once the + * handle is assigned, which writes to disk only for token keys. */ + if (o->handle == 0) { + unsigned char* buf; + if (o->hssDeferredState != NULL) { + wc_ForceZero(o->hssDeferredState, o->hssDeferredStateLen); + XFREE(o->hssDeferredState, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + buf = (unsigned char*)XMALLOC(privSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (buf == NULL) + return WC_LMS_RC_WRITE_FAIL; + XMEMCPY(buf, priv, privSz); + o->hssDeferredState = buf; + o->hssDeferredStateLen = privSz; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; + } + + /* Session-only keys do not need NV persistence: state lives in the + * in-memory LmsKey for the session's lifetime. */ + if (!o->onToken) + return WC_LMS_RC_SAVED_TO_NV_MEMORY; + + if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_WRITE_FAIL; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +int WP11_Hss_FlushDeferredState(WP11_Object* priv) +{ + int ret = 0; + if (priv == NULL) return BAD_FUNC_ARG; - return wp11_Hss_WriteStateBlob(o, priv, privSz); + if (priv->hssDeferredState == NULL) + return 0; /* nothing deferred */ + if (priv->handle == 0) + return BAD_FUNC_ARG; /* called too early */ + + /* Only persist for token-resident keys. Session keys keep their state + * in the in-memory LmsKey for the session's lifetime. */ + if (priv->onToken) { + ret = wp11_Hss_WriteStateBlob(priv, priv->hssDeferredState, + priv->hssDeferredStateLen); + } + + wc_ForceZero(priv->hssDeferredState, priv->hssDeferredStateLen); + XFREE(priv->hssDeferredState, NULL, DYNAMIC_TYPE_TMP_BUFFER); + priv->hssDeferredState = NULL; + priv->hssDeferredStateLen = 0; + return ret; } static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) { WP11_Object* o = (WP11_Object*)ctx; if (o == NULL) - return BAD_FUNC_ARG; - return wp11_Hss_ReadStateBlob(o, priv, privSz); + 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; } #endif /* WOLFPKCS11_LMS_PRIVATE */ @@ -5244,23 +5345,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); - if (ret == 0) { - /* Cross-check the cached pub against the loaded key. Mismatch - * indicates the state file does not belong to this key (or - * tampering), so we refuse to use it. */ - byte loaded[HSS_MAX_PUBLIC_KEY_LEN]; - word32 loadedLen = sizeof(loaded); - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, loaded, - &loadedLen); - if (ret == 0) { - if (loadedLen != pubLen || - WP11_ConstantCompare(loaded, pub, - (int)pubLen) != 1) { - ret = BAD_FUNC_ARG; - } - } - wc_ForceZero(loaded, sizeof(loaded)); - } + /* Trust the GCM-authenticated state file: if the tag verifies, the + * private state is intact. Restore the cached pub into key->pub so + * subsequent ExportPubRaw / Sign produce values consistent with the + * shell record. wc_LmsKey_Reload does not restore key->pub itself. */ + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(object->data.lmsKey, pub, pubLen); if (ret == 0) object->opFlag |= WP11_FLAG_HSS_STATE_VALID; else @@ -6563,6 +6653,15 @@ static int wp11_Object_Encode(WP11_Object* object, int protect) } break; #endif + #ifdef WOLFPKCS11_LMS + case CKK_HSS: + ret = wp11_Object_Encode_HssKey(object); + /* Never `protect`-zero an HSS private LmsKey here — the + * private state must remain in memory between Sign calls so + * the wolfSSL read CB can serve it on demand. The shell + * blob written to disk only carries params + public key. */ + break; + #endif #ifndef NO_DH case CKK_DH: ret = wp11_Object_Encode_DhKey(object); @@ -9620,6 +9719,14 @@ void WP11_Object_Free(WP11_Object* object) XFREE(object->label, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (object->keyId != NULL) XFREE(object->keyId, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->hssDeferredState != NULL) { + wc_ForceZero(object->hssDeferredState, object->hssDeferredStateLen); + XFREE(object->hssDeferredState, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->hssDeferredState = NULL; + object->hssDeferredStateLen = 0; + } +#endif if (object->issuer != NULL) XFREE(object->issuer, NULL, DYNAMIC_TYPE_CERT); if (object->serial != NULL) @@ -10192,15 +10299,21 @@ int WP11_Object_SetMldsaKey(WP11_Object* object, unsigned char** data, int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, CK_ULONG* len) { - int ret; + int ret = 0; int levels = 0, height = 0, winternitz = 0; LmsKey* key; if (object == NULL || data == NULL || len == NULL) return BAD_FUNC_ARG; - /* Reject private-key import unconditionally. */ - if (object->objClass != CKO_PUBLIC_KEY) + /* Reject private-key import (data[1] non-NULL on a private object means + * the caller is trying to install caller-controlled key material, which + * would defeat HSS state safety). Empty templates during NewObject hit + * this with data[1] == NULL — those are no-ops, not import attempts. */ + if (object->objClass == CKO_PRIVATE_KEY && data[1] != NULL) + return BAD_FUNC_ARG; + if (object->objClass != CKO_PUBLIC_KEY && + object->objClass != CKO_PRIVATE_KEY) return BAD_FUNC_ARG; if (object->onToken) @@ -10208,8 +10321,17 @@ int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, key = object->data.lmsKey; - /* data[0] is optional CK_HSS_PARAMS; if absent, the public key blob in - * data[1] is parsed by wolfSSL and we can fall back to defaults. */ + /* Empty template case (newObject=TRUE during keygen): nothing to do + * yet — the wolfSSL key is fully initialized by WP11_Hss_GenerateKeyPair + * after both the public and private WP11_Objects are created. */ + if (data[0] == NULL && data[1] == NULL) { + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + return 0; + } + + /* Public-key import path: parse params, init the wolfSSL key, + * and import the raw public key bytes. */ if (data[0] != NULL) { ret = wp11_HssTranslateParams((const CK_HSS_PARAMS*)data[0], len[0], &levels, &height, &winternitz); @@ -10434,19 +10556,53 @@ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, /** * Returns the number of remaining one-time-keys ("signatures left") for the - * HSS key. *remaining is set to 0 on error. + * HSS key. *remaining is set to 0 on error or when the key is exhausted. + * + * Note: wc_LmsKey_SigsLeft returns a boolean (1 if any sigs left, 0 if + * exhausted), not a count. Compute the actual count from the leaf index q + * stored in the first 8 bytes of priv_raw, big-endian, and the total + * 2^(levels*height) leaves. */ int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) { - int n; + int levels = 0, height = 0, winternitz = 0; + int totalH; + word64 q = 0; + word64 total; + int i; + LmsKey* k; + if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) return BAD_FUNC_ARG; - n = wc_LmsKey_SigsLeft(key->data.lmsKey); - if (n < 0) { + k = key->data.lmsKey; + + /* Boolean check — if wolfSSL says "no sigs left", return 0 unconditionally. */ + if (wc_LmsKey_SigsLeft(k) == 0) { *remaining = 0; - return n; + return 0; } - *remaining = (word32)n; + + /* Compute (2^(levels*height) - q). q is big-endian in priv_raw[0..7]. */ + if (wc_LmsKey_GetParameters(k, &levels, &height, &winternitz) != 0) { + *remaining = 0; + return BAD_FUNC_ARG; + } + for (i = 0; i < 8; i++) + q = (q << 8) | (word64)k->priv_raw[i]; + totalH = levels * height; + if (totalH >= 64) { + /* Saturate — total signatures don't fit in word64. We cap at + * UINT32_MAX since the PKCS#11 attribute is CK_ULONG. */ + *remaining = 0xFFFFFFFFU; + return 0; + } + total = ((word64)1) << totalH; + if (q >= total) + *remaining = 0; + else if ((total - q) > 0xFFFFFFFFULL) + *remaining = 0xFFFFFFFFU; + else + *remaining = (word32)(total - q); return 0; } #endif /* WOLFPKCS11_LMS_PRIVATE */ @@ -11660,14 +11816,9 @@ static int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, #ifdef WOLFPKCS11_LMS_PRIVATE if (object->objClass == CKO_PRIVATE_KEY) { word32 remaining = 0; - int n = wc_LmsKey_SigsLeft(object->data.lmsKey); - if (n < 0) { - *len = CK_UNAVAILABLE_INFORMATION; - } - else { - CK_ULONG v; - remaining = (word32)n; - v = (CK_ULONG)remaining; + ret = WP11_Hss_SigsLeft(object, &remaining); + if (ret == 0) { + CK_ULONG v = (CK_ULONG)remaining; if (data == NULL) { *len = sizeof(v); } @@ -11680,6 +11831,9 @@ static int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, *len = sizeof(v); } } + else { + *len = CK_UNAVAILABLE_INFORMATION; + } } else { *len = CK_UNAVAILABLE_INFORMATION; diff --git a/tests/lms_state_persistence_test.c b/tests/lms_state_persistence_test.c index b166bf73..7272a372 100644 --- a/tests/lms_state_persistence_test.c +++ b/tests/lms_state_persistence_test.c @@ -39,6 +39,8 @@ #include #endif #include +#include +#include #ifndef WOLFPKCS11_USER_SETTINGS #include @@ -47,6 +49,9 @@ #ifndef HAVE_PKCS11_STATIC #include +#ifndef WOLFPKCS11_DLL_FILENAME + #define WOLFPKCS11_DLL_FILENAME "src/.libs/libwolfpkcs11.so" +#endif #endif #include diff --git a/tests/pkcs11v3test.c b/tests/pkcs11v3test.c index f5f42886..9f372dcb 100644 --- a/tests/pkcs11v3test.c +++ b/tests/pkcs11v3test.c @@ -87,9 +87,13 @@ static byte* userPin = (byte*)"wolfpkcs11-test"; static int userPinLen; #ifdef WOLFPKCS11_PKCS11_V3_2 -#if defined(WOLFPKCS11_MLDSA) || defined(WOLFPKCS11_MLKEM) +#if defined(WOLFPKCS11_MLDSA) || defined(WOLFPKCS11_MLKEM) || \ + defined(WOLFPKCS11_LMS) static CK_BBOOL ckTrue = CK_TRUE; + +#endif /* WOLFPKCS11_MLDSA || WOLFPKCS11_MLKEM || WOLFPKCS11_LMS */ +#if defined(WOLFPKCS11_MLDSA) || defined(WOLFPKCS11_MLKEM) static CK_OBJECT_CLASS privKeyClass = CKO_PRIVATE_KEY; static CK_OBJECT_CLASS pubKeyClass = CKO_PUBLIC_KEY; diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 4f505eef..206a7259 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -599,6 +599,7 @@ 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); +WP11_LOCAL int WP11_Hss_FlushDeferredState(WP11_Object* priv); #endif WP11_LOCAL int WP11_Dh_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, diff --git a/wolfpkcs11/pkcs11.h b/wolfpkcs11/pkcs11.h index 437da0d8..25057a4a 100644 --- a/wolfpkcs11/pkcs11.h +++ b/wolfpkcs11/pkcs11.h @@ -915,7 +915,7 @@ typedef struct CK_HSS_PARAMS { CK_LMS_TYPE lm_type[8]; /* per-level LMS typecode */ CK_LMOTS_TYPE lm_ots_type[8]; /* per-level LMOTS typecode */ } CK_HSS_PARAMS; -typedef CK_HSS_PARAMS CK_PTR CK_HSS_PARAMS_PTR; +typedef CK_HSS_PARAMS* CK_HSS_PARAMS_PTR; /* Function list types. */ From aab04acee37e40c458826ff3a2aed1106cd4e1d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 06:23:02 +0000 Subject: [PATCH 3/6] HSS: propagate storage commit failures and lock the deferred-state flush MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two security fixes uncovered by the post-merge review. 1. wolfPKCS11_Store_Close was void, so a failed rename or fsync after writes were buffered to the temp file silently dropped the state advance: the tmp was either aborted or the rename returned non-zero, but the HSS write path saw ret == 0 and returned WC_LMS_RC_SAVED_TO_NV_MEMORY to wolfSSL. wc_LmsKey_Sign would then release a signature whose new state never reached durable storage; on the next process restart the previous state file would be reloaded and the OTS key reused. Add a wolfPKCS11_Store_CloseAndReport variant that returns the commit/fsync status and call it from wp11_Hss_WriteStateBlob, so any rename or fsync error propagates back through the wolfSSL write CB and wc_LmsKey_Sign refuses to emit a signature. 2. WP11_Hss_FlushDeferredState walked the token's object linked list (via wp11_Hss_StorageObjIdx) without holding the token lock. The Sign path already holds the object lock — which for token objects is the same lock — but the keygen flush was called from crypto.c after AddObject released the lock, leaving the walk racy against concurrent C_DestroyObject or C_GenerateKeyPair. Take priv->lock for the duration of the flush. --- src/internal.c | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/internal.c b/src/internal.c index 23d827e1..fdbc62da 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1792,24 +1792,30 @@ 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) +/* Internal close-and-report helper. Returns 0 on a successful close+commit, + * non-zero if the temp-file rename or fsync failed. The legacy + * wolfPKCS11_Store_Close wrapper discards this status to preserve its void + * signature; durability-sensitive callers (the HSS state-write path) must + * use the *_CloseAndReport entrypoint and propagate failures. */ +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); #endif #ifdef WOLFPKCS11_TPM_STORE - /* nothing to do for TPM */ (void)tpmStore; #else if (ctx != NULL) { int commitRet = 0; + int fsyncFailed = 0; if (ctx->file != XBADFILE && ctx->file != NULL) { /* Durable mode: flush file data to disk before close so the @@ -1819,12 +1825,14 @@ void wolfPKCS11_Store_Close(void* store) #if defined(_WIN32) || defined(_MSC_VER) { int fd = _fileno(ctx->file); - if (fd >= 0) (void)_commit(fd); + if (fd < 0 || _commit(fd) != 0) + fsyncFailed = 1; } #else { int fd = fileno(ctx->file); - if (fd >= 0) (void)fsync(fd); + if (fd < 0 || fsync(fd) != 0) + fsyncFailed = 1; } #endif } @@ -1840,16 +1848,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); } /** @@ -5110,7 +5128,15 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, } if (ret == 0) ret = wp11_storage_write(storage, ct, (int)ctLen); - wp11_storage_close(storage); + /* Capture the close-and-commit result. A void close would mask + * a failed rename/fsync; that would let wc_LmsKey_Sign release a + * signature whose state advance never reached durable storage, + * causing OTS-key reuse on the next process restart. */ + { + int closeRet = wolfPKCS11_Store_CloseAndReport(storage); + if (ret == 0) + ret = closeRet; + } } wc_ForceZero(ct, ctLen); @@ -5265,10 +5291,17 @@ int WP11_Hss_FlushDeferredState(WP11_Object* priv) return BAD_FUNC_ARG; /* called too early */ /* Only persist for token-resident keys. Session keys keep their state - * in the in-memory LmsKey for the session's lifetime. */ + * in the in-memory LmsKey for the session's lifetime. + * + * WriteStateBlob walks the token's object list to compute the storage + * idx, so the token lock must be held for the duration of the walk and + * the disk write. priv->lock points at &token->lock for token objects + * (set by WP11_Session_AddObject). */ if (priv->onToken) { + WP11_Lock_LockRW(priv->lock); ret = wp11_Hss_WriteStateBlob(priv, priv->hssDeferredState, priv->hssDeferredStateLen); + WP11_Lock_UnlockRW(priv->lock); } wc_ForceZero(priv->hssDeferredState, priv->hssDeferredStateLen); From 22bbaf6a2c3db3e8d513bc4146c4c78ad023638a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 07:20:16 +0000 Subject: [PATCH 4/6] HSS: per-key-nonce state files, ban session keys, harden durability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review findings from a fresh-context audit of the LMS/HSS support. Highlights: - Re-key the encrypted state file on disk by a stable 64-bit per-key nonce stored in the shell file (not the object's tail-position in the token list). Removing or adding any non-HSS object no longer corrupts a sibling HSS key's state file. The nonce is generated up-front at keygen so the wolfSSL write callback persists genesis state synchronously inside MakeKey — eliminating the deferred-state staging buffer and the AddObject/Flush ordering window that could leave a key with no durable anchor. - Refuse session-only HSS keygen with CKR_TEMPLATE_INCONSISTENT. A stateful one-time signature key without a durable leaf-index counter is structurally unsafe (a process exit between sign and cleanup releases a signature whose OTS index was never persisted). Always-on-token also collapses several locking and reload-path edge cases. - Persist the signature counter inside the AAD-bound state header (state format bumped to v2; shell to v2 to carry the nonce). On reload the counter is restored from the GCM-authenticated header rather than reading wolfSSL's LmsKey internals (priv_raw layout is not public API and varies between LMS backends and small/full builds). - Verify path: only collapse SIG_VERIFY_E to stat=0; let other wolfSSL errors propagate as CKR_FUNCTION_FAILED so callers can distinguish a forgery attempt from infrastructure failures. - Sign path: zero only the bytes wolfSSL may have written on failure (outLen, not *sigLen). - Durability: durable mode now propagates dir-fsync errors instead of swallowing them, fails on path truncation, and emits a one-time stderr warning when WOLFPKCS11_HSS_RELAX_FSYNC=1 is honored. Verify-only build also imports parameters and the cached pub from the shell so attribute queries work. - pkcs11.h: gate CKK_HSS / CKM_HSS* / CKA_HSS_* / CK_HSS_PARAMS behind WOLFPKCS11_LMS so the public ABI surface is conditional on the build. Validate ulLevels against both CK_HSS_PARAMS_MAX_LEVELS and WOLFSSL_LMS_MAX_LEVELS. Cap parsed pubLen at HSS_MAX_PUBLIC_KEY_LEN. - mechInfo: report 0..0 for HSS key sizes (PKCS#11's "not applicable" convention) instead of an ambiguous 60..60. - Tests: tighten assertions to fail-fast, decouple keys-remaining test from a wolfSSL-internal counter encoding, run in mkdtemp() per invocation rather than a hardcoded ./store/lms, and add a crash-injection scenario that corrupts the state file and verifies the AES-GCM-authenticated reload refuses to release a signature. Add a session-keygen-rejected negative test. - CI: drop the duplicate lms_state_persistence_test invocation (it ran twice via make check + explicit, sharing one token dir). Add a non- --enable-lms=small job with WOLFSSL_LMS_MAX_LEVELS=4/HEIGHT=15 to exercise multi-level / H>5 paths the small-build cap hides. https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- .github/workflows/build-workflow.yml | 23 +- .github/workflows/unit-test.yml | 17 +- src/crypto.c | 41 +- src/internal.c | 663 +++++++++++++++++---------- src/slot.c | 14 +- tests/lms_state_persistence_test.c | 287 +++++++++--- tests/pkcs11v3test.c | 93 +++- wolfpkcs11/internal.h | 1 - wolfpkcs11/pkcs11.h | 25 +- 9 files changed, 828 insertions(+), 336 deletions(-) diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index c87ed39a..41353d12 100644 --- a/.github/workflows/build-workflow.yml +++ b/.github/workflows/build-workflow.yml @@ -11,6 +11,21 @@ on: required: false type: string default: 'make check' + wolfssl_lms: + description: 'wolfSSL LMS configure flag (small|verify-only|)' + required: false + type: string + default: 'small' + wolfssl_lms_levels: + description: 'WOLFSSL_LMS_MAX_LEVELS to compile wolfSSL with' + required: false + type: string + default: '2' + wolfssl_lms_height: + description: 'WOLFSSL_LMS_MAX_HEIGHT to compile wolfSSL with' + required: false + type: string + default: '10' jobs: build: @@ -34,9 +49,13 @@ jobs: - name: wolfssl configure working-directory: ./wolfssl run: | + LMS_FLAG="--enable-lms" + if [ -n "${{ inputs.wolfssl_lms }}" ]; then + LMS_FLAG="--enable-lms=${{ inputs.wolfssl_lms }}" + fi ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 \ - --enable-mldsa --enable-lms=small \ - C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP -DWOLFSSL_LMS_MAX_LEVELS=2 -DWOLFSSL_LMS_MAX_HEIGHT=10" + --enable-mldsa $LMS_FLAG \ + C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP -DWOLFSSL_LMS_MAX_LEVELS=${{ inputs.wolfssl_lms_levels }} -DWOLFSSL_LMS_MAX_HEIGHT=${{ inputs.wolfssl_lms_height }}" - name: wolfssl make install working-directory: ./wolfssl run: make diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 07c75434..9d5badc0 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -118,7 +118,22 @@ jobs: uses: ./.github/workflows/build-workflow.yml with: config: --enable-lms-private - check: make check && ./tests/lms_state_persistence_test + # lms_state_persistence_test is in check_PROGRAMS — `make check` already + # runs it. No explicit second invocation; the original duplicate ran + # the test twice in the same token directory which masked some bugs. + check: make check + # Full (non-`small`) wolfSSL LMS build with larger MAX_LEVELS/MAX_HEIGHT + # to exercise multi-level HSS and the H10 path. The `small` build that the + # other LMS jobs use caps levels and height at 2/10, hiding bugs in the + # multi-level / H>5 code paths. + lms_private_full: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private + check: make check + wolfssl_lms: '' + wolfssl_lms_levels: '4' + wolfssl_lms_height: '15' debug: uses: ./.github/workflows/build-workflow.yml with: diff --git a/src/crypto.c b/src/crypto.c index a0c01583..d97cd57f 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -7309,6 +7309,8 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, case CKM_HSS_KEY_PAIR_GEN: { const CK_HSS_PARAMS* hssParams = NULL; CK_ULONG hssParamsLen = 0; + CK_ATTRIBUTE* tokAttr = NULL; + CK_BBOOL pubTok = CK_FALSE, privTok = CK_FALSE; if (pMechanism->pParameter != NULL) { if (pMechanism->ulParameterLen != sizeof(CK_HSS_PARAMS)) return CKR_MECHANISM_PARAM_INVALID; @@ -7316,6 +7318,32 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, hssParamsLen = pMechanism->ulParameterLen; } + /* Stateful one-time signature keys MUST be on-token. A session- + * only HSS private key has no durable anchor for the leaf index; + * a process crash between sign and cleanup releases a signature + * whose OTS index was never persisted, allowing OTS-key reuse on + * the next invocation. Refuse session-only keys outright. */ + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, + CKA_TOKEN, &tokAttr); + if (tokAttr != NULL && tokAttr->pValue != NULL && + tokAttr->ulValueLen == sizeof(CK_BBOOL)) { + pubTok = *(CK_BBOOL*)tokAttr->pValue; + } + tokAttr = NULL; + FindAttributeType(pPrivateKeyTemplate, ulPrivateKeyAttributeCount, + CKA_TOKEN, &tokAttr); + if (tokAttr != NULL && tokAttr->pValue != NULL && + tokAttr->ulValueLen == sizeof(CK_BBOOL)) { + privTok = *(CK_BBOOL*)tokAttr->pValue; + } + if (!pubTok || !privTok) { + /* CKR_TEMPLATE_INCONSISTENT signals "the supplied template + * conflicts with what this mechanism requires" — exactly + * matches PKCS#11 v3.2 guidance for unsupported attribute + * combinations. */ + return CKR_TEMPLATE_INCONSISTENT; + } + *phPublicKey = *phPrivateKey = CK_INVALID_HANDLE; rv = NewObject(session, CKK_HSS, CKO_PUBLIC_KEY, pPublicKeyTemplate, ulPublicKeyAttributeCount, &pub); @@ -7486,19 +7514,6 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, rv = AddObject(session, priv, pPrivateKeyTemplate, ulPrivateKeyAttributeCount, phPrivateKey); } -#ifdef WOLFPKCS11_LMS_PRIVATE - /* HSS keygen writes the genesis state via the wolfSSL write CB during - * MakeKey, BEFORE the object has a slot handle. The CB stashes those - * bytes; once AddObject has assigned the handle, flush them to the - * durable state file. If this fails, the keygen is rolled back: the - * object cannot be used to sign without a valid persisted state. */ - if (rv == CKR_OK && priv != NULL && pMechanism != NULL && - pMechanism->mechanism == CKM_HSS_KEY_PAIR_GEN) { - if (WP11_Hss_FlushDeferredState(priv) != 0) - rv = CKR_FUNCTION_FAILED; - } -#endif - if (pub != NULL && rv == CKR_OK) { rv = SetInitialStates(pub); } diff --git a/src/internal.c b/src/internal.c index fdbc62da..a4cc2ff6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -295,12 +295,18 @@ struct WP11_Object { byte encoded:1; /* Key isn't in decoded form */ #endif #ifdef WOLFPKCS11_LMS_PRIVATE - /* Genesis-state buffer used when wolfSSL invokes the write callback - * during wc_LmsKey_MakeKey, before the object has been added to the - * slot (handle == 0). The buffer is flushed to durable storage by - * WP11_Hss_FlushDeferredState() once AddObject has assigned a handle. */ - unsigned char* hssDeferredState; - word32 hssDeferredStateLen; + /* Per-key 64-bit identifier used to key the encrypted HSS state file on + * disk. Generated at keygen, persisted in the shell file, and copied + * back into this field on reload. The state-file path is therefore + * stable across object renumbering (WP11_Session_RemoveObject reshuffles + * the linked-list positions of every higher-id object; a position-keyed + * state file would be deleted out from under a sibling HSS key). + * Non-zero only for token-resident HSS private keys. */ + word64 lmsStateId; + /* Number of signatures ever produced by this key. Persisted in the + * AAD-bound state header so it cannot be silently rolled back. Used to + * compute CKA_HSS_KEYS_REMAINING without reading wolfSSL internals. */ + word64 lmsSigCount; #endif WP11_Session* session; /* Session object belongs to */ @@ -1171,33 +1177,53 @@ static int wolfPKCS11_StoreCommitTemp(WP11_FileStoreCtx* ctx) /* Durable mode: fsync the parent directory so the rename is recorded * on stable storage before returning success to the caller. Without * this, a power loss could revert the directory entry and silently - * undo the state advance. */ + * undo the state advance. For LMS/HSS this is the difference between + * "OTS index advanced durably" and "rename rolled back, leaf re-used". */ if (ret == 0 && ctx->durable) { #if defined(_WIN32) || defined(_MSC_VER) char dirCopy[WP11_STORE_MAX_PATH]; char* p; HANDLE dh; - XSTRNCPY(dirCopy, ctx->final_name, sizeof(dirCopy)); - dirCopy[sizeof(dirCopy) - 1] = '\0'; + size_t flen = XSTRLEN(ctx->final_name); + if (flen + 1 > sizeof(dirCopy)) { + /* Truncation would point fsync at the wrong directory. */ + return READ_ONLY_E; + } + XMEMCPY(dirCopy, ctx->final_name, flen + 1); p = strrchr(dirCopy, '\\'); if (p == NULL) p = strrchr(dirCopy, '/'); - if (p != NULL) { - *p = '\0'; - dh = CreateFileA(dirCopy, GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, NULL); - if (dh != INVALID_HANDLE_VALUE) { - /* Best-effort. Some FS reject FlushFileBuffers on dirs. */ - (void)FlushFileBuffers(dh); + if (p == NULL) { + return READ_ONLY_E; + } + *p = '\0'; + dh = CreateFileA(dirCopy, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (dh == INVALID_HANDLE_VALUE) { + return READ_ONLY_E; + } + if (!FlushFileBuffers(dh)) { + /* Some FS legitimately reject directory FlushFileBuffers; only + * fail if the call itself fails with an unexpected error. */ + DWORD err = GetLastError(); + if (err != ERROR_INVALID_FUNCTION && + err != ERROR_NOT_SUPPORTED && + err != ERROR_ACCESS_DENIED) { CloseHandle(dh); + return READ_ONLY_E; } } + CloseHandle(dh); #else char dirCopy[WP11_STORE_MAX_PATH]; char* slash; int dirFd; - XSTRNCPY(dirCopy, ctx->final_name, sizeof(dirCopy)); - dirCopy[sizeof(dirCopy) - 1] = '\0'; + size_t flen = XSTRLEN(ctx->final_name); + if (flen + 1 > sizeof(dirCopy)) { + /* Truncation would silently fsync the wrong directory. */ + return READ_ONLY_E; + } + XMEMCPY(dirCopy, ctx->final_name, flen + 1); slash = strrchr(dirCopy, '/'); if (slash != NULL) *slash = '\0'; @@ -1208,13 +1234,23 @@ static int wolfPKCS11_StoreCommitTemp(WP11_FileStoreCtx* ctx) | O_DIRECTORY #endif ); - if (dirFd >= 0) { - (void)fsync(dirFd); + if (dirFd < 0) { + /* In durable mode we cannot certify the rename without a dir + * fsync; surface the failure rather than pretend success. */ + return READ_ONLY_E; + } + if (fsync(dirFd) != 0) { + int saved_errno = errno; + close(dirFd); + /* EINVAL means the FS doesn't support fsync on a directory — + * still acceptable since on those filesystems POSIX rename + * atomicity is provided directly. Any other errno is fatal. */ + if (saved_errno != EINVAL) + return READ_ONLY_E; + } + else { close(dirFd); } - /* If the open or fsync failed, we still consider the rename - * committed — POSIX guarantees rename atomicity on the same FS, - * and dir-fsync is a best-effort durability improvement. */ #endif } @@ -4791,16 +4827,23 @@ static int wp11_Object_Store_MldsaKey(WP11_Object* object, int tokenId, #ifdef WOLFPKCS11_LMS /* On-disk magic + version for the HSS shell file (parameters + cached pub). * The shell is non-secret metadata persisted once at keygen so that on token - * load we know how to call wc_LmsKey_SetParameters before wc_LmsKey_Reload. */ + * load we know how to call wc_LmsKey_SetParameters before wc_LmsKey_Reload. + * + * Version 2 adds an 8-byte stateId nonce (used to key the encrypted state + * file independently of the object's position in the token list). Older v1 + * shells are no longer supported — the format is internal and never shipped + * before this change landed. */ #define WP11_HSS_SHELL_MAGIC 0x48535350UL /* "HSSP" */ -#define WP11_HSS_SHELL_VERSION 1U +#define WP11_HSS_SHELL_VERSION 2U #ifdef WOLFPKCS11_LMS_PRIVATE /* On-disk magic + version for the encrypted HSS state file. The header - * (including levels/height/winternitz) is bound into the AES-GCM tag via - * AAD, so any tampering with parameters is detected at decrypt time. */ + * (including levels/height/winternitz and the persisted signature counter) + * is bound into the AES-GCM tag via AAD, so any tampering with parameters + * or the counter is detected at decrypt time. Version 2 adds the 8-byte + * sigCount field — see wp11_Hss_WriteStateBlob. */ #define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ -#define WP11_HSS_STATE_VERSION 1U +#define WP11_HSS_STATE_VERSION 2U #define WP11_HSS_STATE_IV_LEN 12 /* Cached env-var read once at first use: when 1, the state-write callback @@ -4817,6 +4860,15 @@ static int wp11_HssShouldFsync(void) #else wp11_HssRelaxFsync = 0; #endif + if (wp11_HssRelaxFsync) { + /* Single, prominent stderr line — the README documents this is + * non-production. Operators who reach this path must see it. */ + fprintf(stderr, + "wolfPKCS11: WARNING: WOLFPKCS11_HSS_RELAX_FSYNC=1 is set. " + "HSS 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_HssRelaxFsync ? 0 : 1; } @@ -4895,8 +4947,13 @@ static int wp11_HssTranslateParams(const CK_HSS_PARAMS* params, } if (paramsLen != sizeof(CK_HSS_PARAMS)) return BAD_FUNC_ARG; - if (params->ulLevels < 1 || params->ulLevels > 8) + /* Bound by both the wire-struct capacity (CK_HSS_PARAMS_MAX_LEVELS) and the + * wolfSSL build's WOLFSSL_LMS_MAX_LEVELS — only the smaller is usable. */ + if (params->ulLevels < 1 || + params->ulLevels > CK_HSS_PARAMS_MAX_LEVELS || + params->ulLevels > (CK_ULONG)WOLFSSL_LMS_MAX_LEVELS) { return BAD_FUNC_ARG; + } { int h, w, i; @@ -4985,6 +5042,13 @@ static int wp11_HssParseShellHeader(const byte* in, word32 inLen, *height = (int)wp11_HssReadU32(in + idx); idx += 4; *winternitz = (int)wp11_HssReadU32(in + idx); idx += 4; pl = wp11_HssReadU32(in + idx); idx += 4; + /* Cap pubLen against the maximum HSS public-key size: a malicious shell + * with an absurd pl value would otherwise pass the idx+pl<=inLen check + * (if the file itself is large) and force wolfSSL to dispatch a giant + * ImportPubRaw. RFC 8554 SHA256/M32 pub keys are 60 bytes; we allow + * HSS_MAX_PUBLIC_KEY_LEN as the upper bound. */ + if (pl > HSS_MAX_PUBLIC_KEY_LEN) + return BAD_FUNC_ARG; if (idx + pl > inLen) return BAD_FUNC_ARG; *pub = in + idx; @@ -4998,36 +5062,45 @@ static int wp11_HssParseShellHeader(const byte* in, word32 inLen, * * Layout written to disk: * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] + * [u64 sigCount] * [u32 ivLen][iv (12 bytes)] * [u32 ctLen][ciphertext + 16-byte AES-GCM tag] - * The first 20 bytes (magic..winternitz) are bound into the GCM tag via AAD. - */ -/* Compute the storage objId used by the token-serialization loop for this - * object. wp11_Token_Store iterates from objCnt-1 down to 0 walking the - * linked list head->next; the index assigned to each object equals its - * position from the tail. We MUST match that mapping or the state file - * lives under a different index than the shell file. */ -static int wp11_Hss_StorageObjIdx(WP11_Object* o) + * The first 28 bytes (magic..sigCount) are bound into the GCM tag via AAD, + * so any tampering with parameters or the persisted counter is detected + * at decrypt time. The state file is keyed on disk by (slot_id, lmsStateId) + * — a stable per-key 64-bit nonce stored in the shell — NOT by the object's + * position in the token list. This means token reordering (Add/Remove/ + * Slot_Store) cannot move the state file or delete a sibling key's state. */ + +/* Pack/parse the AAD-bound header. */ +#define WP11_HSS_STATE_HDR_LEN 28U + +static void wp11_HssWriteU32(byte* p, word32 v) { - int idx; - int total; - WP11_Object* cur; - WP11_Token* tok; + p[0] = (byte)(v >> 24); + p[1] = (byte)(v >> 16); + p[2] = (byte)(v >> 8); + p[3] = (byte)v; +} - if (o == NULL || o->slot == NULL) - return -1; - tok = &o->slot->token; - total = 0; - for (cur = tok->object; cur != NULL; cur = cur->next) - total++; - if (total == 0) - return -1; - idx = total - 1; - for (cur = tok->object; cur != NULL && cur != o; cur = cur->next) - idx--; - if (cur != o) - return -1; - return idx; +static void wp11_HssWriteU64(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; +} + +static word64 wp11_HssReadU64(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 int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, @@ -5037,15 +5110,16 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, void* storage = NULL; int levels = 0, height = 0, winternitz = 0; byte iv[WP11_HSS_STATE_IV_LEN]; - byte hdr[20]; - word32 hdrIdx = 0; + byte hdr[WP11_HSS_STATE_HDR_LEN]; byte* ct = NULL; word32 ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ word32 totalLen; - int tokenId, objId; + int tokenId; if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) return BAD_FUNC_ARG; + if (o->lmsStateId == 0) + return BAD_FUNC_ARG; /* per-key nonce must have been assigned */ ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, &winternitz); @@ -5053,26 +5127,12 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, return ret; /* Build the AAD-bound header. */ - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC >> 24); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC >> 16); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC >> 8); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_MAGIC); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION >> 24); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION >> 16); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION >> 8); - hdr[hdrIdx++] = (byte)(WP11_HSS_STATE_VERSION); - hdr[hdrIdx++] = (byte)((levels >> 24) & 0xFF); - hdr[hdrIdx++] = (byte)((levels >> 16) & 0xFF); - hdr[hdrIdx++] = (byte)((levels >> 8) & 0xFF); - hdr[hdrIdx++] = (byte)( levels & 0xFF); - hdr[hdrIdx++] = (byte)((height >> 24) & 0xFF); - hdr[hdrIdx++] = (byte)((height >> 16) & 0xFF); - hdr[hdrIdx++] = (byte)((height >> 8) & 0xFF); - hdr[hdrIdx++] = (byte)( height & 0xFF); - hdr[hdrIdx++] = (byte)((winternitz >> 24) & 0xFF); - hdr[hdrIdx++] = (byte)((winternitz >> 16) & 0xFF); - hdr[hdrIdx++] = (byte)((winternitz >> 8) & 0xFF); - hdr[hdrIdx++] = (byte)( winternitz & 0xFF); + wp11_HssWriteU32(hdr + 0, WP11_HSS_STATE_MAGIC); + wp11_HssWriteU32(hdr + 4, WP11_HSS_STATE_VERSION); + wp11_HssWriteU32(hdr + 8, (word32)levels); + wp11_HssWriteU32(hdr + 12, (word32)height); + wp11_HssWriteU32(hdr + 16, (word32)winternitz); + wp11_HssWriteU64(hdr + 20, o->lmsSigCount); /* Fresh GCM nonce per write (NEVER reuse object->iv). */ ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_HSS_STATE_IV_LEN); @@ -5096,15 +5156,8 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, totalLen = (word32)sizeof(hdr) + 4 + WP11_HSS_STATE_IV_LEN + 4 + ctLen; tokenId = (int)o->slot->id; - objId = wp11_Hss_StorageObjIdx(o); - if (objId < 0) { - wc_ForceZero(ct, ctLen); - XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return BAD_FUNC_ARG; - } - ret = wp11_storage_open(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)tokenId, (CK_ULONG)objId, (int)totalLen, &storage); + (CK_ULONG)tokenId, (CK_ULONG)o->lmsStateId, (int)totalLen, &storage); if (ret == 0) { if (wp11_HssShouldFsync()) wolfPKCS11_Store_SetDurable(storage, 1); @@ -5112,18 +5165,14 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, ret = wp11_storage_write(storage, hdr, (int)sizeof(hdr)); if (ret == 0) { byte ivLenBuf[4]; - ivLenBuf[0] = 0; ivLenBuf[1] = 0; ivLenBuf[2] = 0; - ivLenBuf[3] = (byte)WP11_HSS_STATE_IV_LEN; + wp11_HssWriteU32(ivLenBuf, WP11_HSS_STATE_IV_LEN); ret = wp11_storage_write(storage, ivLenBuf, 4); } if (ret == 0) ret = wp11_storage_write(storage, iv, WP11_HSS_STATE_IV_LEN); if (ret == 0) { byte ctLenBuf[4]; - ctLenBuf[0] = (byte)((ctLen >> 24) & 0xFF); - ctLenBuf[1] = (byte)((ctLen >> 16) & 0xFF); - ctLenBuf[2] = (byte)((ctLen >> 8) & 0xFF); - ctLenBuf[3] = (byte)( ctLen & 0xFF); + wp11_HssWriteU32(ctLenBuf, ctLen); ret = wp11_storage_write(storage, ctLenBuf, 4); } if (ret == 0) @@ -5145,21 +5194,25 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, } /* Read and decrypt the HSS private state file into priv[privSz]. The header - * AAD is verified by the AES-GCM tag, so any tampering is detected. */ + * AAD is verified by the AES-GCM tag, so any tampering (including rolling + * back the persisted sigCount) is detected. */ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) { int ret; void* storage = NULL; - byte hdr[20]; + byte hdr[WP11_HSS_STATE_HDR_LEN]; int levels = 0, height = 0, winternitz = 0; int expectedLevels = 0, expectedHeight = 0, expectedW = 0; byte iv[WP11_HSS_STATE_IV_LEN]; byte* ct = NULL; word32 magic, version, ivLen, ctLen; - int tokenId, objId; + word64 sigCount; + int tokenId; if (o == NULL || priv == NULL || privSz == 0) return BAD_FUNC_ARG; + if (o->lmsStateId == 0) + return BAD_FUNC_ARG; ret = wc_LmsKey_GetParameters(o->data.lmsKey, &expectedLevels, &expectedHeight, &expectedW); @@ -5167,12 +5220,8 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) return ret; tokenId = (int)o->slot->id; - objId = wp11_Hss_StorageObjIdx(o); - if (objId < 0) - return BAD_FUNC_ARG; - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); + (CK_ULONG)tokenId, (CK_ULONG)o->lmsStateId, &storage); if (ret != 0) return ret; @@ -5183,6 +5232,7 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) levels = (int)wp11_HssReadU32(hdr + 8); height = (int)wp11_HssReadU32(hdr + 12); winternitz = (int)wp11_HssReadU32(hdr + 16); + sigCount = wp11_HssReadU64(hdr + 20); if (magic != WP11_HSS_STATE_MAGIC || version != WP11_HSS_STATE_VERSION || levels != expectedLevels || @@ -5231,6 +5281,12 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) wc_ForceZero(ct, ctLen); XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); } + if (ret == 0) { + /* Restore the persisted signature counter into the in-memory object. + * AES-GCM verified the header; sigCount cannot have been rolled back + * without detection. */ + o->lmsSigCount = sigCount; + } if (ret != 0) wc_ForceZero(priv, privSz); return ret; @@ -5240,77 +5296,26 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) * after each state advance. Must return WC_LMS_RC_SAVED_TO_NV_MEMORY on * success; any other value aborts the sign (no signature is released). * - * During keygen, MakeKey fires this callback before the object has been - * registered with the slot (handle == 0). Stash the genesis state in a - * heap buffer and rely on WP11_Hss_FlushDeferredState() being called by - * the keygen path after AddObject assigns a real handle. */ + * Synchronous: the per-key state-file path uses o->lmsStateId, which is + * assigned at the start of WP11_Hss_GenerateKeyPair (BEFORE MakeKey), so + * the genesis-state write lands at the final disk path immediately. There + * is no deferred-state staging area and no window where MakeKey returns + * with un-persisted state. */ 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; - - /* During keygen the object hasn't been added to the slot yet (handle - * still 0). We can't decide yet whether it's a token key — the caller - * has the CKA_TOKEN attribute. Stash the genesis state in a heap buffer; - * the keygen postlude calls WP11_Hss_FlushDeferredState() once the - * handle is assigned, which writes to disk only for token keys. */ - if (o->handle == 0) { - unsigned char* buf; - if (o->hssDeferredState != NULL) { - wc_ForceZero(o->hssDeferredState, o->hssDeferredStateLen); - XFREE(o->hssDeferredState, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - buf = (unsigned char*)XMALLOC(privSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (buf == NULL) - return WC_LMS_RC_WRITE_FAIL; - XMEMCPY(buf, priv, privSz); - o->hssDeferredState = buf; - o->hssDeferredStateLen = privSz; - return WC_LMS_RC_SAVED_TO_NV_MEMORY; - } - - /* Session-only keys do not need NV persistence: state lives in the - * in-memory LmsKey for the session's lifetime. */ - if (!o->onToken) - return WC_LMS_RC_SAVED_TO_NV_MEMORY; + /* HSS keys are always token-resident in this build (enforced by + * C_GenerateKeyPair). Refuse to write if not. */ + if (!o->onToken || o->lmsStateId == 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; } -int WP11_Hss_FlushDeferredState(WP11_Object* priv) -{ - int ret = 0; - if (priv == NULL) - return BAD_FUNC_ARG; - if (priv->hssDeferredState == NULL) - return 0; /* nothing deferred */ - if (priv->handle == 0) - return BAD_FUNC_ARG; /* called too early */ - - /* Only persist for token-resident keys. Session keys keep their state - * in the in-memory LmsKey for the session's lifetime. - * - * WriteStateBlob walks the token's object list to compute the storage - * idx, so the token lock must be held for the duration of the walk and - * the disk write. priv->lock points at &token->lock for token objects - * (set by WP11_Session_AddObject). */ - if (priv->onToken) { - WP11_Lock_LockRW(priv->lock); - ret = wp11_Hss_WriteStateBlob(priv, priv->hssDeferredState, - priv->hssDeferredStateLen); - WP11_Lock_UnlockRW(priv->lock); - } - - wc_ForceZero(priv->hssDeferredState, priv->hssDeferredStateLen); - XFREE(priv->hssDeferredState, NULL, DYNAMIC_TYPE_TMP_BUFFER); - priv->hssDeferredState = NULL; - priv->hssDeferredStateLen = 0; - return ret; -} - static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) { WP11_Object* o = (WP11_Object*)ctx; @@ -5320,6 +5325,47 @@ static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) return WC_LMS_RC_READ_FAIL; return WC_LMS_RC_READ_TO_MEMORY; } + +/* Recover the lmsStateId from the on-disk shell file. Used by the unstore + * path to delete the state file before forgetting which path it lived at. */ +static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, + word64* outStateId) +{ + int ret; + void* storage = NULL; + unsigned char* buf = NULL; + int bufLen = 0; + int levels = 0, height = 0, winternitz = 0; + const byte* pub = NULL; + word32 pubLen = 0; + word32 idx; + + if (outStateId == NULL) + return BAD_FUNC_ARG; + *outStateId = 0; + + ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL, + (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; + + ret = wp11_HssParseShellHeader(buf, (word32)bufLen, &levels, &height, + &winternitz, &pub, &pubLen); + /* Shell layout for v2 places the stateId after the pub blob. */ + if (ret == 0) { + idx = 24 + pubLen; + if (idx + 8 > (word32)bufLen) + ret = BAD_FUNC_ARG; + else + *outStateId = wp11_HssReadU64(buf + idx); + } + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} #endif /* WOLFPKCS11_LMS_PRIVATE */ /* Decode the HSS public key (or, for private keys, the shell file: read @@ -5356,43 +5402,81 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) } } else if (object->objClass == CKO_PRIVATE_KEY) { -#ifdef WOLFPKCS11_LMS_PRIVATE int levels = 0, height = 0, winternitz = 0; const byte* pub = NULL; word32 pubLen = 0; + word32 idx; ret = wp11_HssParseShellHeader(object->keyData, (word32)object->keyDataLen, &levels, &height, &winternitz, &pub, &pubLen); + if (ret == 0) { + /* v2 shells carry an 8-byte stateId after the pub blob. */ + idx = 24 + pubLen; + if (idx + 8 > (word32)object->keyDataLen) + ret = BAD_FUNC_ARG; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (ret == 0) { + object->lmsStateId = wp11_HssReadU64(object->keyData + idx); + if (object->lmsStateId == 0) + ret = BAD_FUNC_ARG; + } if (ret == 0) ret = wc_LmsKey_Init(object->data.lmsKey, NULL, object->devId); - if (ret == 0) + if (ret == 0) { ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, height, winternitz); - if (ret == 0) + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } + if (ret == 0) { ret = wc_LmsKey_SetWriteCb(object->data.lmsKey, wp11_Hss_WriteState_Cb); - if (ret == 0) + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } + if (ret == 0) { ret = wc_LmsKey_SetReadCb(object->data.lmsKey, wp11_Hss_ReadState_Cb); - if (ret == 0) + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } + if (ret == 0) { ret = wc_LmsKey_SetContext(object->data.lmsKey, object); - if (ret == 0) + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } + if (ret == 0) { ret = wc_LmsKey_Reload(object->data.lmsKey); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } /* Trust the GCM-authenticated state file: if the tag verifies, the * private state is intact. Restore the cached pub into key->pub so * subsequent ExportPubRaw / Sign produce values consistent with the * shell record. wc_LmsKey_Reload does not restore key->pub itself. */ - if (ret == 0) + if (ret == 0) { ret = wc_LmsKey_ImportPubRaw(object->data.lmsKey, pub, pubLen); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + } if (ret == 0) object->opFlag |= WP11_FLAG_HSS_STATE_VALID; - else - wc_LmsKey_Free(object->data.lmsKey); #else - /* In a verify-only build we cannot reload the private state, so - * the key remains effectively dormant: any sign attempt is rejected - * later by the missing CKM_HSS sign capability. */ - ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); + /* Verify-only build: we cannot reload private state, but we can + * still serve attribute queries (CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE, + * etc.) and pub-key extraction by importing parameters and the + * cached pub from the shell. CKM_HSS sign is not advertised. */ + 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); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(object->data.lmsKey, pub, pubLen); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); + (void)idx; #endif } object->encoded = (ret != 0); @@ -5401,7 +5485,11 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) /* Encode the HSS public key (or private shell). For private keys we never * serialize the state via keyData — the state file is written directly via - * the wolfSSL write callback. The shell carries non-secret metadata. */ + * the wolfSSL write callback. The shell carries non-secret metadata. + * + * Private-key shells (v2) include an 8-byte stateId trailer used to key + * the encrypted state file path independently of the object's position in + * the token list. Public-key shells do not need a stateId. */ static int wp11_Object_Encode_HssKey(WP11_Object* object) { int ret = 0; @@ -5409,16 +5497,20 @@ static int wp11_Object_Encode_HssKey(WP11_Object* object) byte pub[HSS_MAX_PUBLIC_KEY_LEN]; word32 pubLen = sizeof(pub); int hdrLen; + int isPriv; + word32 needed; if (object == NULL || object->data.lmsKey == NULL) return BAD_FUNC_ARG; + isPriv = (object->objClass == CKO_PRIVATE_KEY); + 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) { - word32 needed = 24 + pubLen; + needed = 24 + pubLen + (isPriv ? 8U : 0U); XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); object->keyData = (unsigned char*)XMALLOC(needed, NULL, DYNAMIC_TYPE_TMP_BUFFER); @@ -5432,7 +5524,24 @@ static int wp11_Object_Encode_HssKey(WP11_Object* object) ret = hdrLen; } else { - object->keyDataLen = hdrLen; + if (isPriv) { +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->lmsStateId == 0) { + ret = BAD_FUNC_ARG; + } + else { + wp11_HssWriteU64(object->keyData + hdrLen, + object->lmsStateId); + object->keyDataLen = hdrLen + 8; + } +#else + /* Verify-only build never encodes private shells. */ + ret = BAD_FUNC_ARG; +#endif + } + else { + object->keyDataLen = hdrLen; + } } } } @@ -6798,11 +6907,30 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) case CKK_HSS: if (object->objClass == CKO_PRIVATE_KEY) { /* HSS private key has TWO on-disk files: shell + state. - * Remove the state file here; the shell file is removed by - * the trailing wp11_storage_remove(storeObjType,...). */ + * The state file is keyed by lmsStateId (a stable per-key + * 64-bit nonce stored in the shell), NOT by objId, so the + * state file path doesn't shift when the token list is + * renumbered. Recover the stateId — preferring the in-memory + * value, falling back to a shell read for objects that have + * not been decoded yet. */ storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; - (void)wp11_storage_remove(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - tokenId, objId); +#ifdef WOLFPKCS11_LMS_PRIVATE + { + word64 stateId = 0; + if (object->lmsStateId != 0) { + stateId = object->lmsStateId; + } + else { + (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, + &stateId); + } + if (stateId != 0) { + (void)wolfPKCS11_Store_Remove( + WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + (CK_ULONG)tokenId, (CK_ULONG)stateId); + } + } +#endif } else { storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; @@ -9753,11 +9881,18 @@ void WP11_Object_Free(WP11_Object* object) if (object->keyId != NULL) XFREE(object->keyId, NULL, DYNAMIC_TYPE_TMP_BUFFER); #ifdef WOLFPKCS11_LMS_PRIVATE - if (object->hssDeferredState != NULL) { - wc_ForceZero(object->hssDeferredState, object->hssDeferredStateLen); - XFREE(object->hssDeferredState, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->hssDeferredState = NULL; - object->hssDeferredStateLen = 0; + /* Orphaned-state cleanup: if this is a token-resident HSS private key + * whose state file landed on disk but whose token-list registration + * never completed (handle still 0), the object is being freed before + * any wp11_Object_Unstore could fire. Remove the orphan state file + * here so a subsequent keygen with a colliding nonce cannot adopt + * stale leaf material. The shell file is removed by the same path + * that owns shell-file naming (Slot_Store / Unstore). */ + if (object->onToken && object->objClass == CKO_PRIVATE_KEY && + object->type == CKK_HSS && object->lmsStateId != 0 && + object->handle == 0 && object->slot != NULL) { + (void)wolfPKCS11_Store_Remove(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + (CK_ULONG)object->slot->id, (CK_ULONG)object->lmsStateId); } #endif if (object->issuer != NULL) @@ -10442,34 +10577,44 @@ int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, WP11_Lock_LockRO(pub->lock); ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, (int)dataLen); - /* wolfSSL distinguishes "bad signature" (returns SIG_VERIFY_E or similar) - * from internal failure. The C-layer caller maps stat=0 to - * CKR_SIGNATURE_INVALID and ret < 0 to CKR_FUNCTION_FAILED. */ - if (ret == 0) + /* wolfSSL signals "signature invalid" with SIG_VERIFY_E. Anything else + * (MEMORY_E, BAD_FUNC_ARG, hardware/driver errors) is an internal failure + * the caller MUST be able to distinguish from a forgery — collapsing + * them all to stat=0 would let infrastructure issues mimic CKR_SIGNATURE_INVALID. */ + if (ret == 0) { *stat = 1; - else + } + else if (ret == SIG_VERIFY_E) { *stat = 0; + ret = 0; /* caller maps stat=0 to CKR_SIGNATURE_INVALID */ + } + else { + *stat = 0; + /* leave ret < 0 for the C-layer to map to CKR_FUNCTION_FAILED */ + } if (pub->onToken) WP11_Lock_UnlockRO(pub->lock); - /* Verify failures (signature invalid) should not bubble up as ret < 0; - * the *stat output is the canonical signal. Only return -ve for - * unrecoverable internal errors. */ - if (ret != 0 && *stat == 0) { - /* Map any wolfSSL return into success-with-stat=0 — the caller - * inspects *stat. */ - ret = 0; - } return ret; } #ifdef WOLFPKCS11_LMS_PRIVATE /** * Generate an HSS key pair, persist genesis state durably, and populate the - * public-key object with the matching public key. The private object's - * write/read callbacks are wired in before MakeKey so the genesis state is - * persisted to disk before the call returns. + * public-key object with the matching public key. + * + * The 64-bit per-key nonce (lmsStateId) is generated FIRST so the wolfSSL + * write callback knows where to write genesis state. State is therefore + * durable on disk before this function returns. On any failure after the + * state file is written, we roll back: zero/free the in-memory key AND + * remove the state file, so the caller's higher-level cleanup never sees + * a half-created key. + * + * Pre-condition: caller (C_GenerateKeyPair) has already validated that + * CKA_TOKEN=TRUE on both pub and priv templates. We mark priv->onToken=1 + * here so the write CB will accept the state-file write — formal onToken + * registration via WP11_Session_AddObject happens after we return. */ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, const CK_HSS_PARAMS* params, CK_ULONG paramsLen, @@ -10480,14 +10625,37 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, 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; } - if (priv->onToken) - WP11_Lock_LockRW(priv->lock); + /* Generate the per-key 64-bit nonce. Must be non-zero (zero is the + * "uninitialized" sentinel) and distinct per key. We retry on the + * vanishingly small chance of zero. */ + { + 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->lmsStateId = wp11_HssReadU64(nonceBuf); + if (priv->lmsStateId != 0) + break; + } + if (priv->lmsStateId == 0) + return BAD_FUNC_ARG; + } + + /* Mark on-token so the write CB will commit; AddObject formalizes the + * registration shortly. priv->lock is set later by Session_AddObject; + * during keygen no other thread holds a handle to this object, so the + * CB does not need a lock. */ + priv->onToken = 1; + priv->lmsSigCount = 0; ret = wp11_HssTranslateParams(params, paramsLen, &levels, &height, &winternitz); @@ -10508,9 +10676,12 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, if (ret == 0) ret = Rng_New(&slot->token.rng, &slot->token.rngLock, &rng); if (ret == 0) { - /* MakeKey calls our write CB internally to persist genesis state. */ + /* MakeKey calls our write CB internally to persist genesis state. + * If MakeKey returns 0, 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 and pubkey into the public-key object. */ @@ -10531,13 +10702,19 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, } else { wc_LmsKey_Free(priv->data.lmsKey); + /* Remove any state file we may have written before the failure. */ + if (stateWritten && priv->lmsStateId != 0) { + (void)wolfPKCS11_Store_Remove( + WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + (CK_ULONG)slot->id, (CK_ULONG)priv->lmsStateId); + } + /* Reset state-tracking fields so an orphan-cleanup in + * WP11_Object_Free won't re-issue the remove on a freed path. */ + priv->lmsStateId = 0; } wc_ForceZero(pubBuf, sizeof(pubBuf)); - if (priv->onToken) - WP11_Lock_UnlockRW(priv->lock); - return ret; } @@ -10546,25 +10723,32 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, * (via write CB) BEFORE the signature is returned to the caller. On any * failure the in-memory state is poisoned (WP11_FLAG_HSS_STATE_VALID cleared) * to force a reload from durable storage on the next attempt. + * + * HSS private keys are always token-resident (enforced at C_GenerateKeyPair), + * so priv->lock points at &token->lock and is always non-NULL. We + * pre-increment lmsSigCount before the wolfSSL Sign call so the persisted + * counter in the state file matches "this signature is leaf N"; on failure + * we revert. */ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, word32* sigLen, WP11_Object* priv) { int ret = 0; - word32 outLen; + word32 outLen = 0; if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || - sigLen == NULL) { + sigLen == NULL || priv->lock == NULL) { return BAD_FUNC_ARG; } - if (priv->onToken) - WP11_Lock_LockRW(priv->lock); + WP11_Lock_LockRW(priv->lock); if ((priv->opFlag & WP11_FLAG_HSS_STATE_VALID) == 0) { ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ } if (ret == 0) { + word64 prevCount = priv->lmsSigCount; + priv->lmsSigCount = prevCount + 1; outLen = *sigLen; ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, (int)dataLen); @@ -10575,67 +10759,74 @@ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, /* Either the write CB failed (state on disk may be stale; in- * memory advanced) or the key is exhausted. Either way, refuse * future signs until the object is reloaded from durable - * storage; zero any partial signature material. */ + * storage; zero only the bytes wolfSSL may have written. */ priv->opFlag &= ~WP11_FLAG_HSS_STATE_VALID; - XMEMSET(sig, 0, *sigLen); + if (outLen > 0) + XMEMSET(sig, 0, outLen); + /* Revert in-memory counter; the persisted state file (if it + * was successfully rewritten) will be authoritative on reload. */ + priv->lmsSigCount = prevCount; } } - if (priv->onToken) - WP11_Lock_UnlockRW(priv->lock); + WP11_Lock_UnlockRW(priv->lock); return ret; } /** * Returns the number of remaining one-time-keys ("signatures left") for the - * HSS key. *remaining is set to 0 on error or when the key is exhausted. + * HSS key. Uses the in-memory lmsSigCount (persisted in the GCM-authenticated + * state file header) rather than reaching into wolfSSL's LmsKey internals — + * priv_raw layout is not part of the public API and varies between wolfSSL + * LMS backends (--enable-lms=small, hashsigs, future variants). * - * Note: wc_LmsKey_SigsLeft returns a boolean (1 if any sigs left, 0 if - * exhausted), not a count. Compute the actual count from the leaf index q - * stored in the first 8 bytes of priv_raw, big-endian, and the total - * 2^(levels*height) leaves. + * For multi-level HSS, total leaves are 2^(sum of per-level heights). wolfSSL + * currently constrains levels to share a uniform height, so total height is + * levels*height; if that ever changes, this will need a per-level accounting + * fix. Saturates at UINT32_MAX (the CK_ULONG attribute width) for very large + * parameter sets. */ int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) { int levels = 0, height = 0, winternitz = 0; int totalH; - word64 q = 0; + word64 used; word64 total; - int i; - LmsKey* k; if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) return BAD_FUNC_ARG; - k = key->data.lmsKey; - /* Boolean check — if wolfSSL says "no sigs left", return 0 unconditionally. */ - if (wc_LmsKey_SigsLeft(k) == 0) { + /* Boolean cross-check: if wolfSSL says exhausted, return 0 regardless + * of what the counter says. */ + if (wc_LmsKey_SigsLeft(key->data.lmsKey) == 0) { *remaining = 0; return 0; } - /* Compute (2^(levels*height) - q). q is big-endian in priv_raw[0..7]. */ - if (wc_LmsKey_GetParameters(k, &levels, &height, &winternitz) != 0) { + if (wc_LmsKey_GetParameters(key->data.lmsKey, &levels, &height, + &winternitz) != 0) { *remaining = 0; return BAD_FUNC_ARG; } - for (i = 0; i < 8; i++) - q = (q << 8) | (word64)k->priv_raw[i]; totalH = levels * height; + if (totalH <= 0) { + *remaining = 0; + return BAD_FUNC_ARG; + } if (totalH >= 64) { - /* Saturate — total signatures don't fit in word64. We cap at - * UINT32_MAX since the PKCS#11 attribute is CK_ULONG. */ + /* 2^totalH does not fit in word64. Cap at UINT32_MAX. */ *remaining = 0xFFFFFFFFU; return 0; } total = ((word64)1) << totalH; - if (q >= total) + used = key->lmsSigCount; + if (used >= total) *remaining = 0; - else if ((total - q) > 0xFFFFFFFFULL) + else if ((total - used) > 0xFFFFFFFFULL) *remaining = 0xFFFFFFFFU; else - *remaining = (word32)(total - q); + *remaining = (word32)(total - used); return 0; } #endif /* WOLFPKCS11_LMS_PRIVATE */ diff --git a/src/slot.c b/src/slot.c index 294b2c73..fb718d0e 100644 --- a/src/slot.c +++ b/src/slot.c @@ -647,12 +647,14 @@ static CK_MECHANISM_INFO mldsaMechInfo = { }; #endif #ifdef WOLFPKCS11_LMS -/* HSS public-key sizes are small and parameter-dependent (RFC 8554). For - * mechanism advertising we report a wide envelope: smallest L1/H5 ≈ 60 bytes, - * largest L4/H25 ≈ 60 bytes (HSS pub is fixed-size for a given hash), so - * pick a coarse envelope that fits all parameter combos. */ +/* HSS public-key sizes are fixed (RFC 8554, SHA256/M32 = 60 bytes regardless + * of parameters). PKCS#11 v3.2 does not define key-size semantics for HSS + * (bits vs bytes), so callers cannot reliably compare against this envelope. + * Report 0..0 to signal "not applicable" — the universal PKCS#11 convention + * for mechanisms whose key size is parameter-derived. Apps needing the exact + * key length can query CKA_VALUE_LEN on the public-key object. */ static CK_MECHANISM_INFO hssMechInfo = { - 60, 60, + 0, 0, CKF_VERIFY # ifdef WOLFPKCS11_LMS_PRIVATE | CKF_SIGN @@ -660,7 +662,7 @@ static CK_MECHANISM_INFO hssMechInfo = { }; # ifdef WOLFPKCS11_LMS_PRIVATE static CK_MECHANISM_INFO hssKgMechInfo = { - 60, 60, CKF_GENERATE_KEY_PAIR + 0, 0, CKF_GENERATE_KEY_PAIR }; # endif #endif diff --git a/tests/lms_state_persistence_test.c b/tests/lms_state_persistence_test.c index 7272a372..1f73b401 100644 --- a/tests/lms_state_persistence_test.c +++ b/tests/lms_state_persistence_test.c @@ -22,13 +22,20 @@ * * Stateful one-time hash-based signatures must never re-use a leaf index. * This test exercises: - * 1. Generate an HSS keypair (1 level / H=5 / W=4 → 32 sigs). + * 1. Generate an HSS keypair (1 level / H=5 / W=4 = 32 sigs). * 2. Sign a message; record CKA_HSS_KEYS_REMAINING. * 3. C_Finalize and C_Initialize again. * 4. Find the persisted private key by label. * 5. Confirm CKA_HSS_KEYS_REMAINING matches the post-step-2 value. * 6. Sign a different message and verify both signatures. * 7. Confirm CKA_HSS_KEYS_REMAINING decremented by exactly one. + * 8. Crash-injection: corrupt the on-disk state file; verify next sign + * after C_Initialize is refused (CKR_OBJECT_HANDLE_INVALID at find, + * since the AES-GCM-authenticated state cannot decrypt). + * + * The test directory is created with mkdtemp() so concurrent runs cannot + * collide and so a pre-existing stale state file from an earlier failed + * run cannot give a misleading pass. */ #ifdef HAVE_CONFIG_H @@ -55,24 +62,35 @@ #endif #include +#include #include +#include +#include +#include +#include +#include #if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) +/* Test status: any CHECK_* failure latches a non-zero rv that is returned + * to main(). Earlier the macro silently swallowed assertion failures and + * the program could print "All tests passed!" while having printed FAIL + * lines on stderr. */ #define CHECK_CKR(rv, msg) \ do { \ if ((rv) != CKR_OK) { \ fprintf(stderr, "%s:%d - %s: 0x%lx FAIL\n", \ __FILE__, __LINE__, (msg), (unsigned long)(rv)); \ + return (rv); \ } \ } while (0) -#define CHECK_COND(cond, rv, msg) \ +#define CHECK_COND(cond, msg) \ do { \ if (!(cond)) { \ fprintf(stderr, "%s:%d - %s FAIL\n", \ __FILE__, __LINE__, (msg)); \ - (rv) = CKR_GENERAL_ERROR; \ + return CKR_GENERAL_ERROR; \ } \ } while (0) @@ -87,6 +105,8 @@ static int soPinLen = 14; static byte* userPin = (byte*)"wolfpkcs11-test"; static int userPinLen = 15; +static char tokenDir[256]; + static CK_BBOOL ckTrue = CK_TRUE; static CK_KEY_TYPE hssKeyType = CKK_HSS; static CK_OBJECT_CLASS privClass = CKO_PRIVATE_KEY; @@ -123,14 +143,11 @@ static CK_RV pkcs11_init(void) ret = funcList->C_Initialize(&args); CHECK_CKR(ret, "Initialize"); - if (ret == CKR_OK) { - ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); - CHECK_CKR(ret, "GetSlotList"); - } - if (ret == CKR_OK && slotCount > 0) - slot = slotList[0]; - else if (ret == CKR_OK) - ret = CKR_GENERAL_ERROR; + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + CHECK_CKR(ret, "GetSlotList"); + if (slotCount == 0) + return CKR_GENERAL_ERROR; + slot = slotList[0]; return ret; } @@ -162,16 +179,13 @@ static CK_RV pkcs11_set_user_pin(void) int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; ret = funcList->C_OpenSession(slot, flags, NULL, NULL, &s); CHECK_CKR(ret, "OpenSession (PIN setup)"); + ret = funcList->C_Login(s, CKU_SO, soPin, soPinLen); if (ret == CKR_OK) { - ret = funcList->C_Login(s, CKU_SO, soPin, soPinLen); - CHECK_CKR(ret, "Login SO"); - if (ret == CKR_OK) { - ret = funcList->C_InitPIN(s, userPin, userPinLen); - CHECK_CKR(ret, "InitPIN"); - } - funcList->C_Logout(s); - funcList->C_CloseSession(s); + ret = funcList->C_InitPIN(s, userPin, userPinLen); + CHECK_CKR(ret, "InitPIN"); } + funcList->C_Logout(s); + funcList->C_CloseSession(s); return ret; } @@ -181,7 +195,7 @@ static CK_RV pkcs11_open_session(CK_SESSION_HANDLE* session) int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); CHECK_CKR(ret, "OpenSession"); - if (ret == CKR_OK && userPinLen > 0) { + if (userPinLen > 0) { ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); CHECK_CKR(ret, "Login USER"); } @@ -233,14 +247,12 @@ static CK_RV find_hss_priv(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* h) }; ret = funcList->C_FindObjectsInit(session, tmpl, 3); CHECK_CKR(ret, "FindObjectsInit (priv)"); - if (ret == CKR_OK) { - ret = funcList->C_FindObjects(session, h, 1, &count); - CHECK_CKR(ret, "FindObjects (priv)"); - } + ret = funcList->C_FindObjects(session, h, 1, &count); + CHECK_CKR(ret, "FindObjects (priv)"); funcList->C_FindObjectsFinal(session); - if (ret == CKR_OK && count != 1) - ret = CKR_GENERAL_ERROR; - return ret; + if (count != 1) + return CKR_GENERAL_ERROR; + return CKR_OK; } static CK_RV find_hss_pub(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* h) @@ -254,14 +266,12 @@ static CK_RV find_hss_pub(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* h) }; ret = funcList->C_FindObjectsInit(session, tmpl, 3); CHECK_CKR(ret, "FindObjectsInit (pub)"); - if (ret == CKR_OK) { - ret = funcList->C_FindObjects(session, h, 1, &count); - CHECK_CKR(ret, "FindObjects (pub)"); - } + ret = funcList->C_FindObjects(session, h, 1, &count); + CHECK_CKR(ret, "FindObjects (pub)"); funcList->C_FindObjectsFinal(session); - if (ret == CKR_OK && count != 1) - ret = CKR_GENERAL_ERROR; - return ret; + if (count != 1) + return CKR_GENERAL_ERROR; + return CKR_OK; } static CK_RV sign_msg(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE priv, @@ -275,10 +285,8 @@ static CK_RV sign_msg(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE priv, mech.ulParameterLen = 0; ret = funcList->C_SignInit(session, &mech, priv); CHECK_CKR(ret, "C_SignInit (HSS)"); - if (ret == CKR_OK) { - ret = funcList->C_Sign(session, (CK_BYTE_PTR)msg, msgLen, sig, sigLen); - CHECK_CKR(ret, "C_Sign (HSS)"); - } + ret = funcList->C_Sign(session, (CK_BYTE_PTR)msg, msgLen, sig, sigLen); + CHECK_CKR(ret, "C_Sign (HSS)"); return ret; } @@ -293,11 +301,9 @@ static CK_RV verify_msg(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE pub, mech.ulParameterLen = 0; ret = funcList->C_VerifyInit(session, &mech, pub); CHECK_CKR(ret, "C_VerifyInit (HSS)"); - if (ret == CKR_OK) { - ret = funcList->C_Verify(session, (CK_BYTE_PTR)msg, msgLen, - (CK_BYTE_PTR)sig, sigLen); - CHECK_CKR(ret, "C_Verify (HSS)"); - } + ret = funcList->C_Verify(session, (CK_BYTE_PTR)msg, msgLen, + (CK_BYTE_PTR)sig, sigLen); + CHECK_CKR(ret, "C_Verify (HSS)"); return ret; } @@ -312,6 +318,48 @@ static CK_RV get_keys_remaining(CK_SESSION_HANDLE session, return funcList->C_GetAttributeValue(session, priv, &q, 1); } +/* Best-effort: corrupt every file in tokenDir whose name contains "state". + * Used by the crash-injection scenario to verify that a tampered state + * file fails AES-GCM authentication on reload (no usable in-memory key, + * no leaf re-use). */ +static int corrupt_state_files(void) +{ + DIR* d = opendir(tokenDir); + struct dirent* e; + int touched = 0; + if (d == NULL) { + fprintf(stderr, "opendir(%s): %s\n", tokenDir, strerror(errno)); + return -1; + } + while ((e = readdir(d)) != NULL) { + if (strstr(e->d_name, "state") == NULL) + continue; + { + char path[512]; + FILE* f; + int n = snprintf(path, sizeof(path), "%s/%s", tokenDir, e->d_name); + if (n <= 0 || n >= (int)sizeof(path)) + continue; + f = fopen(path, "r+b"); + if (f == NULL) + continue; + /* Flip a byte in the AAD-bound header (offset 16 = winternitz) + * so AES-GCM authentication fails on next decrypt. */ + if (fseek(f, 16, SEEK_SET) == 0) { + int c = fgetc(f); + if (c != EOF) { + fseek(f, 16, SEEK_SET); + fputc(c ^ 0xFF, f); + touched++; + } + } + fclose(f); + } + } + closedir(d); + return touched > 0 ? 0 : -1; +} + static CK_RV lms_state_persistence_test(void) { CK_RV ret; @@ -347,9 +395,15 @@ static CK_RV lms_state_persistence_test(void) CHECK_CKR(ret, "GetAttr KEYS_REMAINING (1st)"); printf("After 1st sign, KEYS_REMAINING = %lu (expected 31)\n", (unsigned long)remaining_after_first); - CHECK_COND(remaining_after_first == 31, ret, + CHECK_COND(remaining_after_first == 31, "KEYS_REMAINING != 31 after first sign"); } + if (ret != CKR_OK) { + funcList->C_Logout(s1); + funcList->C_CloseSession(s1); + pkcs11_final(); + return ret; + } funcList->C_Logout(s1); funcList->C_CloseSession(s1); @@ -358,14 +412,9 @@ static CK_RV lms_state_persistence_test(void) printf("Re-initializing PKCS#11 to load token from disk...\n"); ret = pkcs11_init(); if (ret != CKR_OK) return ret; + ret = pkcs11_open_session(&s2); if (ret == CKR_OK) - ret = pkcs11_open_session(&s2); - if (ret == CKR_OK) { ret = find_hss_priv(s2, &priv2); - if (ret != CKR_OK) { - fprintf(stderr, "Persisted private key not found\n"); - } - } if (ret == CKR_OK) ret = find_hss_pub(s2, &pub2); if (ret == CKR_OK) { @@ -374,7 +423,7 @@ static CK_RV lms_state_persistence_test(void) printf("After reload, KEYS_REMAINING = %lu (must equal %lu)\n", (unsigned long)remaining_after_load, (unsigned long)remaining_after_first); - CHECK_COND(remaining_after_load == remaining_after_first, ret, + CHECK_COND(remaining_after_load == remaining_after_first, "KEYS_REMAINING regressed after reload"); } if (ret == CKR_OK) { @@ -390,19 +439,106 @@ static CK_RV lms_state_persistence_test(void) CHECK_CKR(ret, "GetAttr KEYS_REMAINING (2nd)"); printf("After 2nd sign, KEYS_REMAINING = %lu (expected 30)\n", (unsigned long)remaining_after_second); - CHECK_COND(remaining_after_second == 30, ret, + CHECK_COND(remaining_after_second == 30, "KEYS_REMAINING != 30 after second sign"); } + if (ret != CKR_OK) { + funcList->C_Logout(s2); + funcList->C_CloseSession(s2); + pkcs11_final(); + return ret; + } - /* Cleanup: destroy persisted objects so the test is repeatable. */ - if (priv2 != CK_INVALID_HANDLE) - funcList->C_DestroyObject(s2, priv2); - if (pub2 != CK_INVALID_HANDLE) - funcList->C_DestroyObject(s2, pub2); + /* Crash-injection scenario: corrupt the AAD-bound state header byte; + * AES-GCM authentication MUST fail on next reload. After C_Initialize, + * the private key is unusable (a Sign attempt must fail) and no leaf + * index can be re-used. We don't destroy first because we want to test + * recovery, not cleanup. */ + printf("Crash-injection: tampering with on-disk state header...\n"); funcList->C_Logout(s2); funcList->C_CloseSession(s2); pkcs11_final(); - return ret; + + if (corrupt_state_files() != 0) { + fprintf(stderr, + "Could not locate state file under %s — test inconclusive.\n", + tokenDir); + return CKR_GENERAL_ERROR; + } + + /* Re-init from the corrupted store. There are three places the corruption + * can be detected, in increasing latency: + * (a) C_Initialize / C_Login: token load decodes objects, AES-GCM auth + * fails on the state file → DEVICE_ERROR or non-zero rv. + * (b) C_FindObjects: object is suppressed. + * (c) C_Sign: SignInit succeeds but Sign refuses (poison flag). + * ANY of these constitutes a successful tamper-detection. The test + * MUST fail only if a Sign produces a signature with no error. */ + { + CK_RV initRet, sessRet; + initRet = pkcs11_init(); + if (initRet != CKR_OK) { + printf("Tampered state correctly rejected at C_Initialize: 0x%lx\n", + (unsigned long)initRet); + /* Don't call pkcs11_final on a failed init. */ + return CKR_OK; + } + sessRet = funcList->C_OpenSession(slot, + CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL, NULL, &s2); + if (sessRet != CKR_OK) { + printf("Tampered state correctly rejected at OpenSession: 0x%lx\n", + (unsigned long)sessRet); + pkcs11_final(); + return CKR_OK; + } + if (userPinLen > 0) { + CK_RV loginRet = funcList->C_Login(s2, CKU_USER, userPin, userPinLen); + if (loginRet != CKR_OK) { + printf("Tampered state correctly rejected at Login: 0x%lx\n", + (unsigned long)loginRet); + funcList->C_CloseSession(s2); + pkcs11_final(); + return CKR_OK; + } + } + { + CK_OBJECT_HANDLE bogus = CK_INVALID_HANDLE; + CK_RV findRet = find_hss_priv(s2, &bogus); + if (findRet != CKR_OK) { + printf( + "Tampered state correctly rejected at find time: 0x%lx\n", + (unsigned long)findRet); + funcList->C_Logout(s2); + funcList->C_CloseSession(s2); + pkcs11_final(); + return CKR_OK; + } + { + byte sig3[8192]; + CK_ULONG sig3Len = sizeof(sig3); + CK_RV signRet; + CK_MECHANISM mech; + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + signRet = funcList->C_SignInit(s2, &mech, bogus); + if (signRet == CKR_OK) { + signRet = funcList->C_Sign(s2, + (CK_BYTE_PTR)msg1, sizeof(msg1) - 1, sig3, &sig3Len); + } + CHECK_COND(signRet != CKR_OK, + "Tampered state must NOT release a signature"); + printf("Tampered state correctly refused at sign: 0x%lx\n", + (unsigned long)signRet); + funcList->C_DestroyObject(s2, bogus); + } + } + funcList->C_Logout(s2); + funcList->C_CloseSession(s2); + pkcs11_final(); + } + + return CKR_OK; } #endif /* WOLFPKCS11_LMS_PRIVATE && !WOLFPKCS11_NO_STORE */ @@ -413,13 +549,42 @@ int main(int argc, char* argv[]) #if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) CK_RV ret; + /* Always run in a fresh tmpdir so a stale state file from an aborted + * previous run cannot give a misleading pass. The directory is left + * in place after the test for post-mortem inspection on failure. */ #ifndef WOLFPKCS11_NO_ENV - if (!XGETENV("WOLFPKCS11_TOKEN_PATH")) - XSETENV("WOLFPKCS11_TOKEN_PATH", "./store/lms", 1); + { + const char* preset = getenv("WOLFPKCS11_TOKEN_PATH"); + if (preset != NULL && preset[0] != '\0') { + int n = snprintf(tokenDir, sizeof(tokenDir), "%s", preset); + if (n <= 0 || n >= (int)sizeof(tokenDir)) { + fprintf(stderr, "WOLFPKCS11_TOKEN_PATH too long\n"); + return 1; + } + (void)mkdir(tokenDir, 0700); + } + else { + char tmpl[] = "/tmp/wolfpkcs11_lms_XXXXXX"; + char* dir = mkdtemp(tmpl); + if (dir == NULL) { + fprintf(stderr, "mkdtemp failed: %s\n", strerror(errno)); + return 1; + } + snprintf(tokenDir, sizeof(tokenDir), "%s", dir); + if (setenv("WOLFPKCS11_TOKEN_PATH", tokenDir, 1) != 0) { + fprintf(stderr, "setenv failed\n"); + return 1; + } + } + } +#else + snprintf(tokenDir, sizeof(tokenDir), "./store/lms"); + (void)mkdir(tokenDir, 0700); #endif printf("wolfPKCS11 LMS/HSS State Persistence Test\n"); - printf("=========================================\n\n"); + printf("=========================================\n"); + printf("Token store directory: %s\n\n", tokenDir); ret = lms_state_persistence_test(); if (ret == CKR_OK) { diff --git a/tests/pkcs11v3test.c b/tests/pkcs11v3test.c index 9f372dcb..bbc7b1bd 100644 --- a/tests/pkcs11v3test.c +++ b/tests/pkcs11v3test.c @@ -1040,7 +1040,12 @@ static void hss_test_make_params(CK_HSS_PARAMS* p) #ifdef WOLFPKCS11_LMS_PRIVATE /* Generate an HSS keypair with a small parameter set. Returns the new pub - * and priv handles via out-parameters. Either may be NULL. */ + * and priv handles via out-parameters. Either may be NULL. + * + * HSS keys are always token-resident in this build (session-only keys + * cannot durably anchor the leaf-index counter). The onToken parameter is + * retained for the negative-path test that verifies session-only keygen + * is rejected with CKR_TEMPLATE_INCONSISTENT. */ static CK_RV gen_hss_keys(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE* outPub, CK_OBJECT_HANDLE* outPriv, @@ -1128,7 +1133,7 @@ static CK_RV test_hss_gen_sign_verify(void* args) CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; static const byte msg[] = "wolfPKCS11 HSS roundtrip test"; - ret = gen_hss_keys(session, &pub, &priv, 0); + ret = gen_hss_keys(session, &pub, &priv, 1); if (ret == CKR_OK) ret = hss_sign_verify_one(session, priv, pub, msg, sizeof(msg) - 1); @@ -1139,35 +1144,43 @@ static CK_RV test_hss_gen_sign_verify(void* args) return ret; } -/* Decrement of CKA_HSS_KEYS_REMAINING after each sign. */ +/* Decrement of CKA_HSS_KEYS_REMAINING by exactly one per sign. The initial + * count is verified against the expected total (2^(L*H) = 32 for L=1/H=5), + * but the comparison going forward is "before - after == 1" so it cannot + * be broken by changes to wolfSSL's leaf-index encoding. */ static CK_RV test_hss_keys_remaining(void* args) { CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; CK_RV ret; CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; CK_ULONG remaining = 0; + CK_ULONG remainingPrev = 0; CK_ATTRIBUTE attr = { CKA_HSS_KEYS_REMAINING, &remaining, sizeof(remaining) }; static const byte msg[] = "kr"; int i; - ret = gen_hss_keys(session, &pub, &priv, 0); + ret = gen_hss_keys(session, &pub, &priv, 1); if (ret == CKR_OK) { ret = funcList->C_GetAttributeValue(session, priv, &attr, 1); CHECK_CKR(ret, "HSS GetAttr CKA_HSS_KEYS_REMAINING"); } if (ret == CKR_OK) { - /* H=5 → 32 sigs total. */ - CHECK_COND(remaining == 32, ret, "HSS initial keys remaining is 32"); + /* L=1, H=5 → 2^5 = 32. Allow the initial value to be exactly 32 + * (no signs yet). Any other value is a regression. */ + CHECK_COND(remaining == 32, ret, + "HSS initial keys remaining must equal 2^(L*H) = 32"); } for (i = 0; ret == CKR_OK && i < 4; i++) { + remainingPrev = remaining; ret = hss_sign_verify_one(session, priv, pub, msg, sizeof(msg) - 1); if (ret == CKR_OK) { ret = funcList->C_GetAttributeValue(session, priv, &attr, 1); CHECK_CKR(ret, "HSS GetAttr after sign"); if (ret == CKR_OK) { - CHECK_COND(remaining == (CK_ULONG)(32 - 1 - i), ret, - "HSS keys remaining decremented"); + CHECK_COND(remainingPrev > 0 && + remaining == remainingPrev - 1, ret, + "HSS keys remaining must decrement by exactly one per sign"); } } } @@ -1179,6 +1192,65 @@ static CK_RV test_hss_keys_remaining(void* args) return ret; } +/* Session-only HSS keygen MUST be rejected (CKR_TEMPLATE_INCONSISTENT). + * A session-only stateful key has no durable anchor for the leaf index; + * a process crash after sign would leak a signature whose OTS index was + * never persisted, allowing reuse on the next invocation. */ +static CK_RV test_hss_session_keygen_rejected(void* args) +{ + CK_SESSION_HANDLE session = *(CK_SESSION_HANDLE*)args; + CK_RV ret; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_MECHANISM mech; + CK_HSS_PARAMS params; + CK_BBOOL fls = CK_FALSE; + CK_BBOOL tru = CK_TRUE; + CK_ATTRIBUTE pubTmpl[] = { + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_TOKEN, &fls, sizeof(fls) } + }; + CK_ATTRIBUTE privTmpl[] = { + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_TOKEN, &fls, sizeof(fls) } + }; + + hss_test_make_params(¶ms); + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = ¶ms; + mech.ulParameterLen = sizeof(params); + + /* Both pub and priv session-only — must reject. */ + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl)/sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl)/sizeof(*privTmpl), &pub, &priv); + CHECK_CKR_FAIL(ret, CKR_TEMPLATE_INCONSISTENT, + "HSS session-only keygen must be rejected"); + if (ret == CKR_TEMPLATE_INCONSISTENT) + ret = CKR_OK; + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + + /* Mixed (token pub, session priv) also rejected. */ + pub = priv = CK_INVALID_HANDLE; + pubTmpl[1].pValue = &tru; + privTmpl[1].pValue = &fls; + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl)/sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl)/sizeof(*privTmpl), &pub, &priv); + CHECK_CKR_FAIL(ret, CKR_TEMPLATE_INCONSISTENT, + "HSS mixed (token pub, session priv) keygen must be rejected"); + if (ret == CKR_TEMPLATE_INCONSISTENT) + ret = CKR_OK; + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + + return ret; +} + /* CKA_VALUE on a private HSS key MUST be CK_UNAVAILABLE_INFORMATION even * when CKA_EXTRACTABLE = TRUE. Verify with both default and explicit flag. */ static CK_RV test_hss_priv_value_blocked(void* args) @@ -1188,7 +1260,7 @@ static CK_RV test_hss_priv_value_blocked(void* args) CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; CK_ATTRIBUTE q; - ret = gen_hss_keys(session, &pub, &priv, 0); + ret = gen_hss_keys(session, &pub, &priv, 1); if (ret == CKR_OK) { /* Query length only. */ q.type = CKA_VALUE; @@ -1229,7 +1301,7 @@ static CK_RV test_hss_copy_priv_rejected(void* args) CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; CK_OBJECT_HANDLE copy = CK_INVALID_HANDLE; - ret = gen_hss_keys(session, &pub, &priv, 0); + ret = gen_hss_keys(session, &pub, &priv, 1); if (ret == CKR_OK) { ret = funcList->C_CopyObject(session, priv, NULL, 0, ©); /* Copy is rejected; exact CK_RV depends on internal mapping but @@ -3069,6 +3141,7 @@ static TEST_FUNC testFunc[] = { PKCS11TEST_FUNC_SESS_DECL(test_hss_keys_remaining), PKCS11TEST_FUNC_SESS_DECL(test_hss_priv_value_blocked), PKCS11TEST_FUNC_SESS_DECL(test_hss_copy_priv_rejected), + PKCS11TEST_FUNC_SESS_DECL(test_hss_session_keygen_rejected), # else PKCS11TEST_FUNC_SESS_DECL(test_hss_verify_only_no_keygen), # endif diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 206a7259..4f505eef 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -599,7 +599,6 @@ 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); -WP11_LOCAL int WP11_Hss_FlushDeferredState(WP11_Object* priv); #endif WP11_LOCAL int WP11_Dh_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, diff --git a/wolfpkcs11/pkcs11.h b/wolfpkcs11/pkcs11.h index 25057a4a..b4159789 100644 --- a/wolfpkcs11/pkcs11.h +++ b/wolfpkcs11/pkcs11.h @@ -178,7 +178,9 @@ extern "C" { #define CKK_HKDF 0x00000042UL #define CKK_ML_KEM 0x00000049UL #define CKK_ML_DSA 0x0000004AUL +#ifdef WOLFPKCS11_LMS #define CKK_HSS 0x00000046UL +#endif #ifdef WOLFPKCS11_NSS /* Not defined by NSS, but we need one */ @@ -263,6 +265,7 @@ extern "C" { /* KEM */ #define CKA_ENCAPSULATE 0x00000633UL #define CKA_DECAPSULATE 0x00000634UL +#ifdef WOLFPKCS11_LMS /* LMS/HSS (RFC 8554) */ #define CKA_HSS_LEVELS 0x00000617UL #define CKA_HSS_LMS_TYPE 0x00000618UL @@ -270,6 +273,7 @@ extern "C" { #define CKA_HSS_LMS_TYPES 0x0000061AUL #define CKA_HSS_LMOTS_TYPES 0x0000061BUL #define CKA_HSS_KEYS_REMAINING 0x0000061CUL +#endif #ifdef WOLFPKCS11_NSS #define CKA_NSS_EMAIL (CKA_NSS + 2) @@ -373,8 +377,10 @@ extern "C" { #define CKM_ML_DSA_KEY_PAIR_GEN 0x0000001CUL #define CKM_ML_DSA 0x0000001DUL #define CKM_HASH_ML_DSA 0x0000001FUL +#ifdef WOLFPKCS11_LMS #define CKM_HSS_KEY_PAIR_GEN 0x00004032UL #define CKM_HSS 0x00004033UL +#endif #ifdef WOLFPKCS11_NSS #define CKM_NSS_TLS_PRF_GENERAL_SHA256 (CKM_NSS + 21) @@ -887,6 +893,7 @@ typedef CK_ULONG CK_ML_KEM_PARAMETER_SET_TYPE; #define CKP_ML_KEM_768 0x00000002UL #define CKP_ML_KEM_1024 0x00000003UL +#ifdef WOLFPKCS11_LMS /* HSS / LMS / LMOTS algorithm identifiers (RFC 8554) used in CK_HSS_PARAMS. */ typedef CK_ULONG CK_HSS_LEVELS; typedef CK_ULONG CK_LMS_TYPE; @@ -905,17 +912,23 @@ typedef CK_ULONG CK_LMOTS_TYPE; #define CKL_LMOTS_SHA256_N32_W4 0x00000003UL #define CKL_LMOTS_SHA256_N32_W8 0x00000004UL +/* Maximum number of HSS levels in the PKCS#11 wire struct. The struct stores + * fixed-size arrays; legitimate ulLevels MUST be <= CK_HSS_PARAMS_MAX_LEVELS, + * AND <= the wolfSSL build's WOLFSSL_LMS_MAX_LEVELS. The code validates both. */ +#define CK_HSS_PARAMS_MAX_LEVELS 8 + /* PKCS#11 v3.2 specifiedParams for CKM_HSS_KEY_PAIR_GEN. The `lm_type` and - * `lm_ots_type` arrays carry one entry per HSS level (1..ulLevels-1 unused - * entries are ignored). wolfSSL requires uniform (height, winternitz) across - * levels in its current API; mixed parameters are rejected with + * `lm_ots_type` arrays carry one entry per HSS level (entries beyond + * ulLevels-1 are ignored). wolfSSL requires uniform (height, winternitz) + * across levels in its current API; mixed parameters are rejected with * CKR_MECHANISM_PARAM_INVALID. */ typedef struct CK_HSS_PARAMS { - CK_HSS_LEVELS ulLevels; /* 1..WOLFSSL_LMS_MAX_LEVELS */ - CK_LMS_TYPE lm_type[8]; /* per-level LMS typecode */ - CK_LMOTS_TYPE lm_ots_type[8]; /* per-level LMOTS typecode */ + CK_HSS_LEVELS ulLevels; /* 1..CK_HSS_PARAMS_MAX_LEVELS */ + CK_LMS_TYPE lm_type[CK_HSS_PARAMS_MAX_LEVELS]; /* per-level LMS typecode */ + CK_LMOTS_TYPE lm_ots_type[CK_HSS_PARAMS_MAX_LEVELS]; /* per-level LMOTS typecode */ } CK_HSS_PARAMS; typedef CK_HSS_PARAMS* CK_HSS_PARAMS_PTR; +#endif /* WOLFPKCS11_LMS */ /* Function list types. */ From e49cb0475e1d50a127d61d2c14fec2f9eac85931 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 07:55:43 +0000 Subject: [PATCH 5/6] HSS: extract scheme-neutral framework for stateful hash signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare for future XMSS / XMSS^MT support by extracting the scheme-agnostic parts of the LMS/HSS implementation. No behavior change for HSS. Renames (HSS-specific → scheme-neutral): - WP11_FLAG_HSS_STATE_VALID → WP11_FLAG_STATEFUL_STATE_VALID - WP11_Object::lmsStateId / lmsSigCount → statefulStateId / statefulSigCount - WOLFPKCS11_HSS_RELAX_FSYNC env var → WOLFPKCS11_STATEFUL_RELAX_FSYNC - wp11_HssShouldFsync / wp11_HssReadU32 / wp11_HssWriteU32 / wp11_Hss…U64 → wp11_Stateful_* Added umbrellas in wolfpkcs11/internal.h: - WOLFPKCS11_STATEFUL_SIG_ANY: any stateful sig (verify or private), gates the byte helpers and the contract documentation block. - WOLFPKCS11_STATEFUL_SIG_PRIVATE: any sign-capable stateful sig, gates the encrypted-state-file infrastructure (env var, AAD-bound state-blob writer/reader, peek-stateId-from-shell). A future XMSS implementation joins both with one-line OR additions. New scheme-agnostic helpers (gated by WOLFPKCS11_STATEFUL_SIG_PRIVATE): - wp11_Stateful_WriteStateBlob: AES-GCM encrypts the wolfSSL state with an AAD-bound header [magic|version|schemeParams|sigCount], writes durably (atomic rename + fsync + parent-dir fsync). Per-key 64-bit nonce keys the file path. Caller passes the scheme magic/version and a packed schemeParams buffer. - wp11_Stateful_ReadStateBlob: symmetric reader, validates the AAD scheme params against expected before AES-GCM decrypt and restores sigCount. - wp11_Stateful_PeekStateIdFromShell: reads the last 8 bytes of the shell file (by-contract trailer convention) without parsing scheme- specific shell internals. LMS code becomes thin wrappers: - wp11_Hss_PackSchemeParams packs (levels, height, winternitz) as 12 big-endian bytes; wp11_Hss_WriteStateBlob/ReadStateBlob now call the generic helpers with HSS magic/version + that param buffer. - wp11_Hss_PeekStateIdFromShell forwards to the generic helper. Documentation: a contract block at the top of the framework section spells out the seven invariants every stateful-sig scheme MUST honour (token-only, per-key-nonce, sync durable writes, AAD binding, sigCount in wp11 layer, poison-on-failure, token-lock-only). When XMSS is added, the implementer can read the contract once and follow the LMS recipe. XMSS will land via: - new CKK_XMSS / CKM_XMSS / CK_XMSS_PARAMS in pkcs11.h - WOLFPKCS11_XMSS_PRIVATE → joins both umbrella defines - new wp11_Object_{Encode,Decode,Set}_XmssKey + WP11_Xmss_GenerateKeyPair / Sign / Verify mirroring the LMS shape - new WOLFPKCS11_STORE_XMSSKEY_PUB / PRIV_SHELL / PRIV_STATE storage type IDs, scheme-specific magic + scheme params layout - the helpers reused via thin wrappers, no duplication Builds + tests pass clean for the three current LMS configurations (disabled / verify-only / sign-capable). https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- README.md | 7 +- src/internal.c | 792 +++++++++++++++++++++++++----------------- wolfpkcs11/internal.h | 26 +- 3 files changed, 491 insertions(+), 334 deletions(-) diff --git a/README.md b/README.md index 9f54a4aa..f388efbc 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,10 @@ The default parameter set when none is supplied is `levels = 1`, because the underlying wolfSSL API requires uniform parameters. For non-production rigs (e.g., tmpfs-backed test harnesses) the env var -`WOLFPKCS11_HSS_RELAX_FSYNC=1` skips the per-signature `fsync` calls. -**Never set this in production.** A power loss or kernel panic can then -expose a one-time-key reuse window. +`WOLFPKCS11_STATEFUL_RELAX_FSYNC=1` skips the per-signature `fsync` calls. +The variable applies to all stateful hash-based signature schemes +(LMS/HSS today; XMSS in the future). **Never set this in production.** A +power loss or kernel panic can then expose a one-time-key reuse window. ### Build options and defines diff --git a/src/internal.c b/src/internal.c index a4cc2ff6..56ec6373 100644 --- a/src/internal.c +++ b/src/internal.c @@ -302,11 +302,11 @@ struct WP11_Object { * the linked-list positions of every higher-id object; a position-keyed * state file would be deleted out from under a sibling HSS key). * Non-zero only for token-resident HSS private keys. */ - word64 lmsStateId; + word64 statefulStateId; /* Number of signatures ever produced by this key. Persisted in the * AAD-bound state header so it cannot be silently rolled back. Used to * compute CKA_HSS_KEYS_REMAINING without reading wolfSSL internals. */ - word64 lmsSigCount; + word64 statefulSigCount; #endif WP11_Session* session; /* Session object belongs to */ @@ -4824,6 +4824,379 @@ static int wp11_Object_Store_MldsaKey(WP11_Object* object, int tokenId, } #endif /* WOLFPKCS11_MLDSA */ +#ifdef WOLFPKCS11_STATEFUL_SIG_ANY +/* =========================================================================== + * 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 state-mutating paths run under the token lock. + * + * 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) +{ + 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) + | ((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; +} + +/* Persist the encrypted state file durably. Caller passes the scheme's + * (storeTypeState, 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. */ +static int wp11_Stateful_WriteStateBlob(WP11_Object* o, + int storeTypeState, 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 = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ + word32 totalLen; + int tokenId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (schemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + + hdrLen = 4 + 4 + schemeParamLen + 8; /* magic+version+params+sigCount */ + 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); + if (schemeParamLen > 0) + XMEMCPY(hdr + 8, schemeParamBytes, schemeParamLen); + wp11_Stateful_WriteU64(hdr + 8 + 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 */ + totalLen = hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen; + + tokenId = (int)o->slot->id; + ret = wp11_storage_open(storeTypeState, + (CK_ULONG)tokenId, (CK_ULONG)o->statefulStateId, (int)totalLen, + &storage); + if (ret == 0) { + if (wp11_StatefulShouldFsync()) + wolfPKCS11_Store_SetDurable(storage, 1); + if (ret == 0) + 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, 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, + int storeTypeState, 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, 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; + + hdrLen = 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) + return MEMORY_E; + + tokenId = (int)o->slot->id; + ret = wp11_storage_open_readonly(storeTypeState, + (CK_ULONG)tokenId, (CK_ULONG)o->statefulStateId, &storage); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + ret = wp11_storage_read(storage, hdr, (int)hdrLen); + if (ret == 0) { + m = wp11_Stateful_ReadU32(hdr); + v = wp11_Stateful_ReadU32(hdr + 4); + sigCount = wp11_Stateful_ReadU64(hdr + 8 + schemeParamLen); + if (m != magic || v != version) { + ret = BAD_FUNC_ARG; + } + else if (schemeParamLen > 0 && + XMEMCMP(hdr + 8, 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. */ +static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, + int storeTypeShell, 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; + + if (bufLen < 8) { + 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 */ +#endif /* WOLFPKCS11_STATEFUL_SIG_ANY */ + #ifdef WOLFPKCS11_LMS /* On-disk magic + version for the HSS shell file (parameters + cached pub). * The shell is non-secret metadata persisted once at keygen so that on token @@ -4840,38 +5213,13 @@ static int wp11_Object_Store_MldsaKey(WP11_Object* object, int tokenId, /* On-disk magic + version for the encrypted HSS state file. The header * (including levels/height/winternitz and the persisted signature counter) * is bound into the AES-GCM tag via AAD, so any tampering with parameters - * or the counter is detected at decrypt time. Version 2 adds the 8-byte - * sigCount field — see wp11_Hss_WriteStateBlob. */ + * or the counter is detected at decrypt time. */ #define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ #define WP11_HSS_STATE_VERSION 2U -#define WP11_HSS_STATE_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. */ -static int wp11_HssRelaxFsync = -1; -static int wp11_HssShouldFsync(void) -{ - if (wp11_HssRelaxFsync < 0) { -#ifndef WOLFPKCS11_NO_ENV - const char* v = XGETENV("WOLFPKCS11_HSS_RELAX_FSYNC"); - wp11_HssRelaxFsync = (v != NULL && v[0] == '1' && v[1] == '\0') ? 1 : 0; -#else - wp11_HssRelaxFsync = 0; -#endif - if (wp11_HssRelaxFsync) { - /* Single, prominent stderr line — the README documents this is - * non-production. Operators who reach this path must see it. */ - fprintf(stderr, - "wolfPKCS11: WARNING: WOLFPKCS11_HSS_RELAX_FSYNC=1 is set. " - "HSS 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_HssRelaxFsync ? 0 : 1; -} +/* HSS scheme-specific param region inside the AAD-bound state header, + * as packed by wp11_Hss_PackSchemeParams below. 12 bytes: three u32s. */ +#define WP11_HSS_SCHEME_PARAM_LEN 12U #endif /* WOLFPKCS11_LMS_PRIVATE */ /* Map RFC 8554 LMS typecode → height. Returns 0 on unknown. */ @@ -5017,13 +5365,6 @@ static int wp11_HssPackShellHeader(byte* out, word32 outLen, int levels, return (int)idx; } -/* Helpers for big-endian 32-bit reads from a byte buffer. */ -static word32 wp11_HssReadU32(const byte* p) -{ - return ((word32)p[0] << 24) | ((word32)p[1] << 16) - | ((word32)p[2] << 8) | (word32)p[3]; -} - /* Parse the shell header. On success, *pub is set to the in-buffer pub * pointer (no allocation) and *pubLen its length. */ static int wp11_HssParseShellHeader(const byte* in, word32 inLen, @@ -5034,14 +5375,14 @@ static int wp11_HssParseShellHeader(const byte* in, word32 inLen, word32 magic, version, pl; if (in == NULL || inLen < 24) return BAD_FUNC_ARG; - magic = wp11_HssReadU32(in + idx); idx += 4; - version = wp11_HssReadU32(in + idx); idx += 4; + 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_HssReadU32(in + idx); idx += 4; - *height = (int)wp11_HssReadU32(in + idx); idx += 4; - *winternitz = (int)wp11_HssReadU32(in + idx); idx += 4; - pl = wp11_HssReadU32(in + idx); idx += 4; + *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 maximum HSS public-key size: a malicious shell * with an absurd pl value would otherwise pass the idx+pl<=inLen check * (if the file itself is large) and force wolfSSL to dispatch a giant @@ -5057,250 +5398,80 @@ static int wp11_HssParseShellHeader(const byte* in, word32 inLen, } #ifdef WOLFPKCS11_LMS_PRIVATE -/* Persist the encrypted HSS private state file. Always uses durable mode - * (fsync + rename + fsync(parent)) unless WOLFPKCS11_HSS_RELAX_FSYNC=1. - * - * Layout written to disk: - * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] - * [u64 sigCount] - * [u32 ivLen][iv (12 bytes)] - * [u32 ctLen][ciphertext + 16-byte AES-GCM tag] - * The first 28 bytes (magic..sigCount) are bound into the GCM tag via AAD, - * so any tampering with parameters or the persisted counter is detected - * at decrypt time. The state file is keyed on disk by (slot_id, lmsStateId) - * — a stable per-key 64-bit nonce stored in the shell — NOT by the object's - * position in the token list. This means token reordering (Add/Remove/ - * Slot_Store) cannot move the state file or delete a sibling key's state. */ - -/* Pack/parse the AAD-bound header. */ -#define WP11_HSS_STATE_HDR_LEN 28U - -static void wp11_HssWriteU32(byte* p, word32 v) +/* Pack the HSS scheme params (levels|height|winternitz, big-endian u32s) + * into the AAD-bound region of the state header. Length must equal + * WP11_HSS_SCHEME_PARAM_LEN. */ +static void wp11_Hss_PackSchemeParams(byte out[WP11_HSS_SCHEME_PARAM_LEN], + int levels, int height, int winternitz) { - p[0] = (byte)(v >> 24); - p[1] = (byte)(v >> 16); - p[2] = (byte)(v >> 8); - p[3] = (byte)v; -} - -static void wp11_HssWriteU64(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; -} - -static word64 wp11_HssReadU64(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]; + 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 thin wrapper over the generic helper. + * + * Layout written to disk (managed by wp11_Stateful_WriteStateBlob): + * AAD: [u32 HSS_MAGIC][u32 HSS_VERSION][u32 levels][u32 height] + * [u32 winternitz][u64 sigCount] + * [u32 ivLen][iv (12 bytes)][u32 ctLen][ciphertext + GCM tag] + * + * The state file is keyed on disk by (slot_id, statefulStateId) — a stable + * per-key 64-bit nonce stored in the shell, NOT by the object's position + * in the token list. */ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, word32 privSz) { - int ret; - void* storage = NULL; int levels = 0, height = 0, winternitz = 0; - byte iv[WP11_HSS_STATE_IV_LEN]; - byte hdr[WP11_HSS_STATE_HDR_LEN]; - byte* ct = NULL; - word32 ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ - word32 totalLen; - int tokenId; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; - if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + if (o == NULL || o->data.lmsKey == NULL) return BAD_FUNC_ARG; - if (o->lmsStateId == 0) - return BAD_FUNC_ARG; /* per-key nonce must have been assigned */ - ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, &winternitz); if (ret != 0) return ret; - - /* Build the AAD-bound header. */ - wp11_HssWriteU32(hdr + 0, WP11_HSS_STATE_MAGIC); - wp11_HssWriteU32(hdr + 4, WP11_HSS_STATE_VERSION); - wp11_HssWriteU32(hdr + 8, (word32)levels); - wp11_HssWriteU32(hdr + 12, (word32)height); - wp11_HssWriteU32(hdr + 16, (word32)winternitz); - wp11_HssWriteU64(hdr + 20, o->lmsSigCount); - - /* Fresh GCM nonce per write (NEVER reuse object->iv). */ - ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_HSS_STATE_IV_LEN); - if (ret != 0) - return ret; - - ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (ct == NULL) - return MEMORY_E; - - ret = wp11_EncryptDataAAD(ct, priv, (int)privSz, - o->slot->token.key, (int)sizeof(o->slot->token.key), - iv, WP11_HSS_STATE_IV_LEN, hdr, (int)sizeof(hdr), o->devId); - if (ret != 0) { - wc_ForceZero(ct, ctLen); - XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return ret; - } - - /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag */ - totalLen = (word32)sizeof(hdr) + 4 + WP11_HSS_STATE_IV_LEN + 4 + ctLen; - - tokenId = (int)o->slot->id; - ret = wp11_storage_open(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)tokenId, (CK_ULONG)o->lmsStateId, (int)totalLen, &storage); - if (ret == 0) { - if (wp11_HssShouldFsync()) - wolfPKCS11_Store_SetDurable(storage, 1); - if (ret == 0) - ret = wp11_storage_write(storage, hdr, (int)sizeof(hdr)); - if (ret == 0) { - byte ivLenBuf[4]; - wp11_HssWriteU32(ivLenBuf, WP11_HSS_STATE_IV_LEN); - ret = wp11_storage_write(storage, ivLenBuf, 4); - } - if (ret == 0) - ret = wp11_storage_write(storage, iv, WP11_HSS_STATE_IV_LEN); - if (ret == 0) { - byte ctLenBuf[4]; - wp11_HssWriteU32(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 wc_LmsKey_Sign release a - * signature whose state advance never reached durable storage, - * causing OTS-key reuse on the next process restart. */ - { - int closeRet = wolfPKCS11_Store_CloseAndReport(storage); - if (ret == 0) - ret = closeRet; - } - } - - wc_ForceZero(ct, ctLen); - XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_WriteStateBlob(o, + WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, + priv, privSz); } -/* Read and decrypt the HSS private state file into priv[privSz]. The header - * AAD is verified by the AES-GCM tag, so any tampering (including rolling - * back the persisted sigCount) is detected. */ +/* HSS state-file read thin wrapper. Validates that the in-memory wolfSSL + * key's (levels, height, winternitz) match what's persisted in the AAD- + * bound header before AES-GCM decrypt. */ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) { - int ret; - void* storage = NULL; - byte hdr[WP11_HSS_STATE_HDR_LEN]; int levels = 0, height = 0, winternitz = 0; - int expectedLevels = 0, expectedHeight = 0, expectedW = 0; - byte iv[WP11_HSS_STATE_IV_LEN]; - byte* ct = NULL; - word32 magic, version, ivLen, ctLen; - word64 sigCount; - int tokenId; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; - if (o == NULL || priv == NULL || privSz == 0) - return BAD_FUNC_ARG; - if (o->lmsStateId == 0) + if (o == NULL || o->data.lmsKey == NULL) return BAD_FUNC_ARG; - - ret = wc_LmsKey_GetParameters(o->data.lmsKey, &expectedLevels, - &expectedHeight, &expectedW); - if (ret != 0) - return ret; - - tokenId = (int)o->slot->id; - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)tokenId, (CK_ULONG)o->lmsStateId, &storage); + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); if (ret != 0) return ret; - - ret = wp11_storage_read(storage, hdr, (int)sizeof(hdr)); - if (ret == 0) { - magic = wp11_HssReadU32(hdr); - version = wp11_HssReadU32(hdr + 4); - levels = (int)wp11_HssReadU32(hdr + 8); - height = (int)wp11_HssReadU32(hdr + 12); - winternitz = (int)wp11_HssReadU32(hdr + 16); - sigCount = wp11_HssReadU64(hdr + 20); - if (magic != WP11_HSS_STATE_MAGIC || - version != WP11_HSS_STATE_VERSION || - levels != expectedLevels || - height != expectedHeight || - winternitz != expectedW) { - ret = BAD_FUNC_ARG; - } - } - if (ret == 0) { - byte buf[4]; - ret = wp11_storage_read(storage, buf, 4); - if (ret == 0) { - ivLen = wp11_HssReadU32(buf); - if (ivLen != WP11_HSS_STATE_IV_LEN) - ret = BAD_FUNC_ARG; - } - } - if (ret == 0) - ret = wp11_storage_read(storage, iv, WP11_HSS_STATE_IV_LEN); - if (ret == 0) { - byte buf[4]; - ret = wp11_storage_read(storage, buf, 4); - if (ret == 0) { - ctLen = wp11_HssReadU32(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_HSS_STATE_IV_LEN, hdr, (int)sizeof(hdr), 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); - } - if (ret == 0) { - /* Restore the persisted signature counter into the in-memory object. - * AES-GCM verified the header; sigCount cannot have been rolled back - * without detection. */ - o->lmsSigCount = sigCount; - } - if (ret != 0) - wc_ForceZero(priv, privSz); - return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_ReadStateBlob(o, + WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + 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 (no signature is released). * - * Synchronous: the per-key state-file path uses o->lmsStateId, which is - * assigned at the start of WP11_Hss_GenerateKeyPair (BEFORE MakeKey), so - * the genesis-state write lands at the final disk path immediately. There - * is no deferred-state staging area and no window where MakeKey returns - * with un-persisted state. */ + * Synchronous: the per-key state-file path uses o->statefulStateId, which + * is assigned at the start of WP11_Hss_GenerateKeyPair (BEFORE MakeKey), + * so the genesis-state write lands at the final disk path immediately. + * There is no deferred-state staging area and no window where MakeKey + * returns with un-persisted state. */ static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) { WP11_Object* o = (WP11_Object*)ctx; @@ -5308,7 +5479,7 @@ static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) return WC_LMS_RC_BAD_ARG; /* HSS keys are always token-resident in this build (enforced by * C_GenerateKeyPair). Refuse to write if not. */ - if (!o->onToken || o->lmsStateId == 0) + if (!o->onToken || o->statefulStateId == 0) return WC_LMS_RC_WRITE_FAIL; if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) @@ -5326,45 +5497,14 @@ static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) return WC_LMS_RC_READ_TO_MEMORY; } -/* Recover the lmsStateId from the on-disk shell file. Used by the unstore - * path to delete the state file before forgetting which path it lived at. */ +/* Recover the statefulStateId from the on-disk HSS shell file. Thin wrapper + * over the generic helper — by contract the stateId is always the last 8 + * bytes of the shell, regardless of scheme-specific layout in between. */ static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, word64* outStateId) { - int ret; - void* storage = NULL; - unsigned char* buf = NULL; - int bufLen = 0; - int levels = 0, height = 0, winternitz = 0; - const byte* pub = NULL; - word32 pubLen = 0; - word32 idx; - - if (outStateId == NULL) - return BAD_FUNC_ARG; - *outStateId = 0; - - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL, - (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; - - ret = wp11_HssParseShellHeader(buf, (word32)bufLen, &levels, &height, - &winternitz, &pub, &pubLen); - /* Shell layout for v2 places the stateId after the pub blob. */ - if (ret == 0) { - idx = 24 + pubLen; - if (idx + 8 > (word32)bufLen) - ret = BAD_FUNC_ARG; - else - *outStateId = wp11_HssReadU64(buf + idx); - } - XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return ret; + return wp11_Stateful_PeekStateIdFromShell(tokenId, objId, + WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL, outStateId); } #endif /* WOLFPKCS11_LMS_PRIVATE */ @@ -5417,8 +5557,8 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) } #ifdef WOLFPKCS11_LMS_PRIVATE if (ret == 0) { - object->lmsStateId = wp11_HssReadU64(object->keyData + idx); - if (object->lmsStateId == 0) + object->statefulStateId = wp11_Stateful_ReadU64(object->keyData + idx); + if (object->statefulStateId == 0) ret = BAD_FUNC_ARG; } if (ret == 0) @@ -5461,7 +5601,7 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) wc_LmsKey_Free(object->data.lmsKey); } if (ret == 0) - object->opFlag |= WP11_FLAG_HSS_STATE_VALID; + object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; #else /* Verify-only build: we cannot reload private state, but we can * still serve attribute queries (CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE, @@ -5526,12 +5666,12 @@ static int wp11_Object_Encode_HssKey(WP11_Object* object) else { if (isPriv) { #ifdef WOLFPKCS11_LMS_PRIVATE - if (object->lmsStateId == 0) { + if (object->statefulStateId == 0) { ret = BAD_FUNC_ARG; } else { - wp11_HssWriteU64(object->keyData + hdrLen, - object->lmsStateId); + wp11_Stateful_WriteU64(object->keyData + hdrLen, + object->statefulStateId); object->keyDataLen = hdrLen + 8; } #else @@ -6907,7 +7047,7 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) case CKK_HSS: if (object->objClass == CKO_PRIVATE_KEY) { /* HSS private key has TWO on-disk files: shell + state. - * The state file is keyed by lmsStateId (a stable per-key + * The state file is keyed by statefulStateId (a stable per-key * 64-bit nonce stored in the shell), NOT by objId, so the * state file path doesn't shift when the token list is * renumbered. Recover the stateId — preferring the in-memory @@ -6917,8 +7057,8 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) #ifdef WOLFPKCS11_LMS_PRIVATE { word64 stateId = 0; - if (object->lmsStateId != 0) { - stateId = object->lmsStateId; + if (object->statefulStateId != 0) { + stateId = object->statefulStateId; } else { (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, @@ -9889,10 +10029,10 @@ void WP11_Object_Free(WP11_Object* object) * stale leaf material. The shell file is removed by the same path * that owns shell-file naming (Slot_Store / Unstore). */ if (object->onToken && object->objClass == CKO_PRIVATE_KEY && - object->type == CKK_HSS && object->lmsStateId != 0 && + object->type == CKK_HSS && object->statefulStateId != 0 && object->handle == 0 && object->slot != NULL) { (void)wolfPKCS11_Store_Remove(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)object->slot->id, (CK_ULONG)object->lmsStateId); + (CK_ULONG)object->slot->id, (CK_ULONG)object->statefulStateId); } #endif if (object->issuer != NULL) @@ -10604,7 +10744,7 @@ int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, * Generate an HSS key pair, persist genesis state durably, and populate the * public-key object with the matching public key. * - * The 64-bit per-key nonce (lmsStateId) is generated FIRST so the wolfSSL + * The 64-bit per-key nonce (statefulStateId) is generated FIRST so the wolfSSL * write callback knows where to write genesis state. State is therefore * durable on disk before this function returns. On any failure after the * state file is written, we roll back: zero/free the in-memory key AND @@ -10642,11 +10782,11 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, ret = WP11_Slot_GenerateRandom(slot, nonceBuf, sizeof(nonceBuf)); if (ret != 0) return ret; - priv->lmsStateId = wp11_HssReadU64(nonceBuf); - if (priv->lmsStateId != 0) + priv->statefulStateId = wp11_Stateful_ReadU64(nonceBuf); + if (priv->statefulStateId != 0) break; } - if (priv->lmsStateId == 0) + if (priv->statefulStateId == 0) return BAD_FUNC_ARG; } @@ -10655,7 +10795,7 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, * during keygen no other thread holds a handle to this object, so the * CB does not need a lock. */ priv->onToken = 1; - priv->lmsSigCount = 0; + priv->statefulSigCount = 0; ret = wp11_HssTranslateParams(params, paramsLen, &levels, &height, &winternitz); @@ -10698,19 +10838,19 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, if (ret == 0) { priv->local = pub->local = 1; priv->keyGenMech = pub->keyGenMech = CKM_HSS_KEY_PAIR_GEN; - priv->opFlag |= WP11_FLAG_HSS_STATE_VALID; + priv->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; } else { wc_LmsKey_Free(priv->data.lmsKey); /* Remove any state file we may have written before the failure. */ - if (stateWritten && priv->lmsStateId != 0) { + if (stateWritten && priv->statefulStateId != 0) { (void)wolfPKCS11_Store_Remove( WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)slot->id, (CK_ULONG)priv->lmsStateId); + (CK_ULONG)slot->id, (CK_ULONG)priv->statefulStateId); } /* Reset state-tracking fields so an orphan-cleanup in * WP11_Object_Free won't re-issue the remove on a freed path. */ - priv->lmsStateId = 0; + priv->statefulStateId = 0; } wc_ForceZero(pubBuf, sizeof(pubBuf)); @@ -10721,12 +10861,12 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, /** * Sign a raw message with an HSS private key. State is advanced and persisted * (via write CB) BEFORE the signature is returned to the caller. On any - * failure the in-memory state is poisoned (WP11_FLAG_HSS_STATE_VALID cleared) + * failure the in-memory state is poisoned (WP11_FLAG_STATEFUL_STATE_VALID cleared) * to force a reload from durable storage on the next attempt. * * HSS private keys are always token-resident (enforced at C_GenerateKeyPair), * so priv->lock points at &token->lock and is always non-NULL. We - * pre-increment lmsSigCount before the wolfSSL Sign call so the persisted + * pre-increment statefulSigCount before the wolfSSL Sign call so the persisted * counter in the state file matches "this signature is leaf N"; on failure * we revert. */ @@ -10743,12 +10883,12 @@ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, WP11_Lock_LockRW(priv->lock); - if ((priv->opFlag & WP11_FLAG_HSS_STATE_VALID) == 0) { + 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->lmsSigCount; - priv->lmsSigCount = prevCount + 1; + word64 prevCount = priv->statefulSigCount; + priv->statefulSigCount = prevCount + 1; outLen = *sigLen; ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, (int)dataLen); @@ -10760,12 +10900,12 @@ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, * memory advanced) or the key is exhausted. Either way, refuse * future signs until the object is reloaded from durable * storage; zero only the bytes wolfSSL may have written. */ - priv->opFlag &= ~WP11_FLAG_HSS_STATE_VALID; + priv->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; if (outLen > 0) XMEMSET(sig, 0, outLen); /* Revert in-memory counter; the persisted state file (if it * was successfully rewritten) will be authoritative on reload. */ - priv->lmsSigCount = prevCount; + priv->statefulSigCount = prevCount; } } @@ -10776,7 +10916,7 @@ int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, /** * Returns the number of remaining one-time-keys ("signatures left") for the - * HSS key. Uses the in-memory lmsSigCount (persisted in the GCM-authenticated + * HSS key. Uses the in-memory statefulSigCount (persisted in the GCM-authenticated * state file header) rather than reaching into wolfSSL's LmsKey internals — * priv_raw layout is not part of the public API and varies between wolfSSL * LMS backends (--enable-lms=small, hashsigs, future variants). @@ -10820,7 +10960,7 @@ int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) return 0; } total = ((word64)1) << totalH; - used = key->lmsSigCount; + used = key->statefulSigCount; if (used >= total) *remaining = 0; else if ((total - used) > 0xFFFFFFFFULL) diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index 4f505eef..d2e8fc33 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -145,6 +145,21 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" (HSS state files are too large for TPM NV). #endif +/* WOLFPKCS11_STATEFUL_SIG_ANY is defined when any stateful hash-based + * signature scheme is enabled (verify-only OR sign-capable). Today: LMS/HSS; + * future: XMSS, XMSS^MT. Gates the shared shell/byte utilities. */ +#if defined(WOLFPKCS11_LMS) +#define WOLFPKCS11_STATEFUL_SIG_ANY +#endif + +/* WOLFPKCS11_STATEFUL_SIG_PRIVATE is defined when any stateful hash-based + * signature scheme with sign + keygen support is enabled. Gates the + * encrypted-state-file infrastructure (wp11_Stateful_*StateBlob, env-var + * fsync gate, poison flag) — none of which is needed for verify-only. */ +#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,11 +261,12 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_FLAG_DERIVE 0x00040000 #define WP11_FLAG_ENCAPSULATE 0x00080000 #define WP11_FLAG_DECAPSULATE 0x00100000 -/* Internal poison/reload marker for HSS private keys. Set after a successful - * keygen or wc_LmsKey_Reload; cleared on any state-write failure or sign - * error. When clear, signing is refused with CKR_DEVICE_ERROR until the key - * is reloaded from durable storage. */ -#define WP11_FLAG_HSS_STATE_VALID 0x00200000 +/* Internal poison/reload marker for stateful-signature private keys (LMS/HSS + * today; XMSS/XMSS^MT in the future). Set after a successful keygen or + * wolfSSL Reload; cleared on any state-write failure or sign error. When + * clear, signing is refused with CKR_DEVICE_ERROR until the key is reloaded + * from durable storage. */ +#define WP11_FLAG_STATEFUL_STATE_VALID 0x00200000 /* Flags for token. */ #define WP11_TOKEN_FLAG_USER_PIN_SET 0x00000001 From 253360f2de876ad72d4a232db40441f223fd55f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 08:51:29 +0000 Subject: [PATCH 6/6] =?UTF-8?q?HSS:=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20orphan=20rollback,=20u64=20path,=20AAD=20tightening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address all findings from the post-refactor review (5 HIGH, 5 MEDIUM, 5 LOW). No on-disk format change for users without a previously-built state file (the format had been bumped to v2 in the prior commit and hasn't shipped); the AAD layout adds a u32 schemeParamLen field, so state files written by the prior commit will fail to decrypt and need to be re-keygen'd. Acceptable for a pre-release feature. HIGH fixes: 1. Session_AddObject linked-list orphan window: if wp11_Slot_Store fails AFTER the object was linked into token->object, roll back the linked- list insertion and reset object->handle to CK_INVALID_HANDLE. Without this, AddObject returns failure but the object remains reachable from token->object; the caller's WP11_Object_Free then leaves a dangling pointer (use-after-free on next slot walk). For HSS specifically this also leaks the durable state file because Object_Free's orphan-cleanup gate (handle == 0) was bypassed. Pre-existing bug for all key types, newly material for HSS. 2. Clear WP11_FLAG_STATEFUL_STATE_VALID on Decode_HssKey failure. Reload from disk with wc_LmsKey_Free leaves data.lmsKey non-NULL but with freed internals; the persisted opFlag still carried the validity bit from a prior successful run. Currently unreachable because callers propagate Decode failure as CKR_DEVICE_ERROR (slot stays unusable); defense-in-depth. 3. word64-safe state-file path for stateful schemes. CK_ULONG is 32-bit on LLP64 (Windows); passing the 64-bit per-key nonce through the public Store_Open / Store_Remove ids truncated it, dropping collision space from 2^64 to 2^32. New helpers wp11_Stateful_StateFile_Path / _Open / _Remove thread word64 through and format with %016llx; on Linux/LP64 the on-disk filename is unchanged. Public store.h API is untouched. 4. wp11_Stateful_WriteStateBlob now asserts o->onToken — the framework contract said "token-resident only" but the helper only checked statefulStateId != 0. A future scheme implementer wiring directly to the framework helper without an onToken check in their CB could have silently persisted session-key state. 5. wp11_Stateful_PeekStateIdFromShell now validates the shell's magic + version against caller-supplied expected values before returning the trailer. Previously a truncated, replaced, or wrong-scheme file at the same (tokenId, objId) would yield a garbage stateId that Store_Remove either silently no-op'd on, or used to delete an unrelated file while the real state file orphaned. MEDIUM fixes: 6. CK_HSS_PARAMS / CKK_HSS / CKM_HSS* / CKA_HSS_* / CKL_* are exposed unconditionally in pkcs11.h. The prior gate broke source compilation for downstream apps that didn't propagate -DWOLFPKCS11_LMS. The library still rejects the mechanism at runtime via CKR_MECHANISM_INVALID when built without HSS support. 7. WP11_Hss_Verify allowlist widened: SIG_VERIFY_E + BUFFER_E + BAD_FUNC_ARG all map to stat=0 (CKR_SIGNATURE_INVALID). Other wolfSSL errors propagate as CKR_FUNCTION_FAILED. Prevents an attacker probing with malformed signatures from distinguishing "infrastructure failure" via the CK_RV side channel. 8. AAD-bound state header now includes the u32 schemeParamLen between version and the scheme-params region. A thin-wrapper version skew (e.g., HSS Write packs 12 params, Read expects 16) now surfaces cleanly as a header-mismatch BAD_FUNC_ARG before AES-GCM decrypt instead of as opaque tag-failure that looks like corruption. 9. Contract doc block (item 7) now calls out the keygen lock exception: priv is thread-local during keygen because no other thread has the handle yet, so the write callback fires unlocked. Explicit exception clarifies the invariant for future scheme implementers. 10. C_GenerateKeyPair returns CKR_ATTRIBUTE_VALUE_INVALID (per PKCS#11 v3.2) for a malformed CKA_TOKEN attribute instead of falling through to CKR_TEMPLATE_INCONSISTENT. LOW fixes: 11. wp11_StatefulShouldFsync resolved during WP11_Library_Init while single-threaded. Prevents the warning being printed twice on heavily-threaded first use, and removes the read-modify-write race on the sentinel. 12. lms_state_persistence_test corrupt_state_files matches exact prefix "wp11_hsskey_priv_state_" instead of substring "state". 13. Comment in crash-injection test clarifies that current implementation fails fast at Login (eager Decode); explicit Sign-refusal branch is a backstop for future lazy-Login changes. 14. wp11_Stateful_ReadStateBlob now wc_ForceZero(priv) on every early- failure path (input validation, malloc failure, file-open failure). Closes the gap where caller relied on the fail-zero pattern but the early returns bypassed it. 15. wp11_Stateful_WriteStateBlob and ReadStateBlob now sanity-cap privSz <= 1 MiB and schemeParamLen <= 256. Prevents word32 overflow in size-arithmetic against hostile / corrupted callers. 16. Dead `if (ret == 0)` after wolfPKCS11_Store_SetDurable removed (SetDurable is void). 17. WP11_Hss_GenerateKeyPair returns RNG_FAILURE_E (mapped to CKR_FUNCTION_FAILED) instead of BAD_FUNC_ARG (mapped to CKR_MECHANISM_PARAM_INVALID) when the 4-retry nonce-generation loop exhausts. Probability ~2^-256; cosmetic but the diagnostic is now correct. 18. README adds a top-level "Environment variables" entry for WOLFPKCS11_STATEFUL_RELAX_FSYNC so operators searching by env-var name find it immediately, not just under the LMS/HSS section. Builds + tests pass clean for all three LMS configurations (disabled / verify-only / sign-capable). https://claude.ai/code/session_01GtRoh5TVMmmfX81LLRbroa --- README.md | 8 + src/crypto.c | 20 +- src/internal.c | 380 ++++++++++++++++++++++++----- tests/lms_state_persistence_test.c | 11 +- wolfpkcs11/pkcs11.h | 22 +- 5 files changed, 368 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index f388efbc..eb520e0d 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,14 @@ POSIX or `%APPDIR%\wolfPKCS11` on Windows), and finally the optional Set to any value to stop storage of token data. +### WOLFPKCS11_STATEFUL_RELAX_FSYNC + +When set to `1`, skips the per-signature `fsync` calls used by stateful +hash-based signature schemes (LMS/HSS today; XMSS in the future). **Never +set this in production.** A power loss or kernel panic with this enabled +can roll back the leaf-index advance and expose a one-time-key reuse +window. See the LMS/HSS build section for details. + ## Release Notes diff --git a/src/crypto.c b/src/crypto.c index d97cd57f..b4901486 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -7322,18 +7322,28 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, * only HSS private key has no durable anchor for the leaf index; * a process crash between sign and cleanup releases a signature * whose OTS index was never persisted, allowing OTS-key reuse on - * the next invocation. Refuse session-only keys outright. */ + * the next invocation. Refuse session-only keys outright. + * + * Distinguish a malformed CKA_TOKEN attribute (return + * CKR_ATTRIBUTE_VALUE_INVALID per PKCS#11 v3.2) from a well-formed + * but session-only template (return CKR_TEMPLATE_INCONSISTENT). */ FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, CKA_TOKEN, &tokAttr); - if (tokAttr != NULL && tokAttr->pValue != NULL && - tokAttr->ulValueLen == sizeof(CK_BBOOL)) { + if (tokAttr != NULL) { + if (tokAttr->pValue == NULL || + tokAttr->ulValueLen != sizeof(CK_BBOOL)) { + return CKR_ATTRIBUTE_VALUE_INVALID; + } pubTok = *(CK_BBOOL*)tokAttr->pValue; } tokAttr = NULL; FindAttributeType(pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_TOKEN, &tokAttr); - if (tokAttr != NULL && tokAttr->pValue != NULL && - tokAttr->ulValueLen == sizeof(CK_BBOOL)) { + if (tokAttr != NULL) { + if (tokAttr->pValue == NULL || + tokAttr->ulValueLen != sizeof(CK_BBOOL)) { + return CKR_ATTRIBUTE_VALUE_INVALID; + } privTok = *(CK_BBOOL*)tokAttr->pValue; } if (!pubTok || !privTok) { diff --git a/src/internal.c b/src/internal.c index 56ec6373..67ad99e7 100644 --- a/src/internal.c +++ b/src/internal.c @@ -4866,7 +4866,13 @@ static int wp11_Object_Store_MldsaKey(WP11_Object* object, int tokenId, * CKR_DEVICE_ERROR until the object is reloaded from disk * (wolfSSL Reload restores in-memory state from the durable file). * - * 7. Locking: all state-mutating paths run under the token lock. + * 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: * @@ -4952,12 +4958,190 @@ static int wp11_StatefulShouldFsync(void) 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 - * (storeTypeState, 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. */ + * (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, - int storeTypeState, word32 magic, word32 version, + const char* schemeFilePrefix, word32 magic, word32 version, const byte* schemeParamBytes, word32 schemeParamLen, const byte* priv, word32 privSz) { @@ -4967,27 +5151,38 @@ static int wp11_Stateful_WriteStateBlob(WP11_Object* o, byte* hdr = NULL; word32 hdrLen; byte* ct = NULL; - word32 ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ - word32 totalLen; + 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; - hdrLen = 4 + 4 + schemeParamLen + 8; /* magic+version+params+sigCount */ + 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 + 8, schemeParamBytes, schemeParamLen); - wp11_Stateful_WriteU64(hdr + 8 + schemeParamLen, o->statefulSigCount); + 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); @@ -5012,18 +5207,17 @@ static int wp11_Stateful_WriteStateBlob(WP11_Object* o, return ret; } - /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag */ - totalLen = hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen; + /* 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_storage_open(storeTypeState, - (CK_ULONG)tokenId, (CK_ULONG)o->statefulStateId, (int)totalLen, - &storage); + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 0, &storage); if (ret == 0) { if (wp11_StatefulShouldFsync()) wolfPKCS11_Store_SetDurable(storage, 1); - if (ret == 0) - ret = wp11_storage_write(storage, hdr, (int)hdrLen); + ret = wp11_storage_write(storage, hdr, (int)hdrLen); if (ret == 0) { byte ivLenBuf[4]; wp11_Stateful_WriteU32(ivLenBuf, WP11_STATEFUL_IV_LEN); @@ -5055,13 +5249,13 @@ static int wp11_Stateful_WriteStateBlob(WP11_Object* o, } /* Read and decrypt the state file. The full header is verified as AAD by - * the AES-GCM tag; tampering with magic, version, scheme params, 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. */ + * 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, - int storeTypeState, word32 magic, word32 version, + const char* schemeFilePrefix, word32 magic, word32 version, const byte* expectedSchemeParamBytes, word32 schemeParamLen, byte* priv, word32 privSz) { @@ -5071,7 +5265,7 @@ static int wp11_Stateful_ReadStateBlob(WP11_Object* o, word32 hdrLen; byte iv[WP11_STATEFUL_IV_LEN]; byte* ct = NULL; - word32 m, v, ivLen, ctLen; + word32 m, v, persistedParamLen, ivLen, ctLen; word64 sigCount = 0; int tokenId; @@ -5081,17 +5275,24 @@ static int wp11_Stateful_ReadStateBlob(WP11_Object* o, 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 + schemeParamLen + 8; + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (hdr == NULL) + if (hdr == NULL) { + wc_ForceZero(priv, privSz); return MEMORY_E; + } tokenId = (int)o->slot->id; - ret = wp11_storage_open_readonly(storeTypeState, - (CK_ULONG)tokenId, (CK_ULONG)o->statefulStateId, &storage); + 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; } @@ -5099,12 +5300,18 @@ static int wp11_Stateful_ReadStateBlob(WP11_Object* o, if (ret == 0) { m = wp11_Stateful_ReadU32(hdr); v = wp11_Stateful_ReadU32(hdr + 4); - sigCount = wp11_Stateful_ReadU64(hdr + 8 + schemeParamLen); + 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 + 8, expectedSchemeParamBytes, schemeParamLen) + XMEMCMP(hdr + 12, expectedSchemeParamBytes, schemeParamLen) != 0) { ret = BAD_FUNC_ARG; } @@ -5163,9 +5370,19 @@ static int wp11_Stateful_ReadStateBlob(WP11_Object* o, /* 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. */ + * 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, word64* outStateId) + int storeTypeShell, word32 expectedShellMagic, word32 expectedShellVersion, + word64* outStateId) { int ret; void* storage = NULL; @@ -5185,7 +5402,13 @@ static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, if (ret != 0) return ret; - if (bufLen < 8) { + /* 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; } @@ -5433,8 +5656,7 @@ static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, if (ret != 0) return ret; wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); - return wp11_Stateful_WriteStateBlob(o, - WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + return wp11_Stateful_WriteStateBlob(o, "hsskey", WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); @@ -5456,8 +5678,7 @@ static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) if (ret != 0) return ret; wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); - return wp11_Stateful_ReadStateBlob(o, - WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, + return wp11_Stateful_ReadStateBlob(o, "hsskey", WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); @@ -5499,12 +5720,15 @@ static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) /* Recover the statefulStateId from the on-disk HSS shell file. Thin wrapper * over the generic helper — by contract the stateId is always the last 8 - * bytes of the shell, regardless of scheme-specific layout in between. */ + * bytes of the shell. The framework validates the HSS shell magic+version + * before returning the trailer to detect file truncation / wrong-scheme + * collisions. */ static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, word64* outStateId) { return wp11_Stateful_PeekStateIdFromShell(tokenId, objId, - WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL, outStateId); + WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL, + WP11_HSS_SHELL_MAGIC, WP11_HSS_SHELL_VERSION, outStateId); } #endif /* WOLFPKCS11_LMS_PRIVATE */ @@ -5600,8 +5824,20 @@ static int wp11_Object_Decode_HssKey(WP11_Object* object) if (ret != 0) wc_LmsKey_Free(object->data.lmsKey); } - if (ret == 0) + if (ret == 0) { object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + } + else { + /* opFlag was restored from disk before this Decode and may have + * carried STATE_VALID from a prior successful run. After a + * Decode failure the in-memory wolfSSL key is freed but + * data.lmsKey is not nulled — clear the flag so a subsequent + * Sign cannot pass the validity guard and dereference freed + * internals. (Today this path is unreachable because callers + * propagate Decode failure as CKR_DEVICE_ERROR and the slot + * stays unusable, but defense-in-depth.) */ + object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + } #else /* Verify-only build: we cannot reload private state, but we can * still serve attribute queries (CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE, @@ -7065,9 +7301,10 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) &stateId); } if (stateId != 0) { - (void)wolfPKCS11_Store_Remove( - WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)tokenId, (CK_ULONG)stateId); + /* Use the word64-safe helper — CK_ULONG would + * truncate the nonce on LLP64 platforms. */ + (void)wp11_Stateful_StateFile_Remove("hsskey", + tokenId, stateId); } } #endif @@ -7806,6 +8043,13 @@ int WP11_Library_Init(void) int i; if (libraryInitCount == 0) { +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + /* Resolve the env-var-controlled fsync gate while we are still + * single-threaded. Lazy-init from arbitrary worker threads inside + * the wolfSSL write CB races on the sentinel and can print the + * warning twice on heavily-threaded first use. */ + (void)wp11_StatefulShouldFsync(); +#endif ret = WP11_Lock_Init(&globalLock); if (ret == 0) { @@ -9681,6 +9925,22 @@ int WP11_Session_AddObject(WP11_Session* session, int onToken, #ifndef WOLFPKCS11_NO_STORE if (ret == 0) { ret = wp11_Slot_Store(session->slot, (int)session->slotId); + if (ret != 0) { + /* Persistence failed AFTER the object was linked into + * token->object. Roll back the linked-list insertion so + * the caller's WP11_Object_Free() does not leave a dangling + * pointer in token->object (use-after-free on next walk). + * Also reset the handle so any post-failure orphan-cleanup + * (e.g. HSS state-file removal in WP11_Object_Free) can + * detect this object is unregistered. */ + token->object = object->next; + object->next = NULL; + token->objCnt--; + if (token->nextObjId > 0) + token->nextObjId--; + object->handle = CK_INVALID_HANDLE; + object->lock = NULL; + } } #endif } @@ -10030,9 +10290,9 @@ void WP11_Object_Free(WP11_Object* object) * that owns shell-file naming (Slot_Store / Unstore). */ if (object->onToken && object->objClass == CKO_PRIVATE_KEY && object->type == CKK_HSS && object->statefulStateId != 0 && - object->handle == 0 && object->slot != NULL) { - (void)wolfPKCS11_Store_Remove(WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)object->slot->id, (CK_ULONG)object->statefulStateId); + object->handle == CK_INVALID_HANDLE && object->slot != NULL) { + (void)wp11_Stateful_StateFile_Remove("hsskey", + (int)object->slot->id, object->statefulStateId); } #endif if (object->issuer != NULL) @@ -10717,14 +10977,19 @@ int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, WP11_Lock_LockRO(pub->lock); ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, (int)dataLen); - /* wolfSSL signals "signature invalid" with SIG_VERIFY_E. Anything else - * (MEMORY_E, BAD_FUNC_ARG, hardware/driver errors) is an internal failure - * the caller MUST be able to distinguish from a forgery — collapsing - * them all to stat=0 would let infrastructure issues mimic CKR_SIGNATURE_INVALID. */ + /* wolfSSL distinguishes "signature invalid / malformed input" from + * "internal failure". Map known input-rejection codes to stat=0 (which + * the caller turns into CKR_SIGNATURE_INVALID) and let everything else + * bubble up as a function failure. The allowlist intentionally covers + * malformed-signature codes (BUFFER_E for too-short, BAD_FUNC_ARG for + * malformed structure) so attacker-supplied garbage cannot be + * distinguished from genuine forgery via the CK_RV side channel. */ if (ret == 0) { *stat = 1; } - else if (ret == SIG_VERIFY_E) { + else if (ret == SIG_VERIFY_E || + ret == BUFFER_E || + ret == BAD_FUNC_ARG) { *stat = 0; ret = 0; /* caller maps stat=0 to CKR_SIGNATURE_INVALID */ } @@ -10786,8 +11051,12 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, if (priv->statefulStateId != 0) break; } - if (priv->statefulStateId == 0) - return BAD_FUNC_ARG; + if (priv->statefulStateId == 0) { + /* RNG produced 8 zero bytes 4 times in a row (probability + * 2^-256). Treat as RNG failure; C-layer maps this to + * CKR_FUNCTION_FAILED, NOT CKR_MECHANISM_PARAM_INVALID. */ + return RNG_FAILURE_E; + } } /* Mark on-token so the write CB will commit; AddObject formalizes the @@ -10844,9 +11113,8 @@ int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, wc_LmsKey_Free(priv->data.lmsKey); /* Remove any state file we may have written before the failure. */ if (stateWritten && priv->statefulStateId != 0) { - (void)wolfPKCS11_Store_Remove( - WOLFPKCS11_STORE_HSSKEY_PRIV_STATE, - (CK_ULONG)slot->id, (CK_ULONG)priv->statefulStateId); + (void)wp11_Stateful_StateFile_Remove("hsskey", + (int)slot->id, priv->statefulStateId); } /* Reset state-tracking fields so an orphan-cleanup in * WP11_Object_Free won't re-issue the remove on a freed path. */ diff --git a/tests/lms_state_persistence_test.c b/tests/lms_state_persistence_test.c index 1f73b401..636c9831 100644 --- a/tests/lms_state_persistence_test.c +++ b/tests/lms_state_persistence_test.c @@ -332,7 +332,11 @@ static int corrupt_state_files(void) return -1; } while ((e = readdir(d)) != NULL) { - if (strstr(e->d_name, "state") == NULL) + /* Match the exact prefix of the HSS state file. Loose substring + * matching ("state") would also catch leftover *.tmp files from an + * interrupted commit and any future filename containing "state". */ + if (strncmp(e->d_name, "wp11_hsskey_priv_state_", + sizeof("wp11_hsskey_priv_state_") - 1) != 0) continue; { char path[512]; @@ -473,7 +477,10 @@ static CK_RV lms_state_persistence_test(void) * (b) C_FindObjects: object is suppressed. * (c) C_Sign: SignInit succeeds but Sign refuses (poison flag). * ANY of these constitutes a successful tamper-detection. The test - * MUST fail only if a Sign produces a signature with no error. */ + * MUST fail only if a Sign produces a signature with no error. The + * "fail-fast at Login" path is the current implementation behavior + * (token-load eagerly reloads); the explicit Sign-refusal branch + * remains as a backstop in case a future change makes Login lazy. */ { CK_RV initRet, sessRet; initRet = pkcs11_init(); diff --git a/wolfpkcs11/pkcs11.h b/wolfpkcs11/pkcs11.h index b4159789..720013b9 100644 --- a/wolfpkcs11/pkcs11.h +++ b/wolfpkcs11/pkcs11.h @@ -178,9 +178,10 @@ extern "C" { #define CKK_HKDF 0x00000042UL #define CKK_ML_KEM 0x00000049UL #define CKK_ML_DSA 0x0000004AUL -#ifdef WOLFPKCS11_LMS +/* PKCS#11 v3.2 LMS/HSS key type. Exposed unconditionally; the library + * rejects the mechanism at runtime with CKR_MECHANISM_INVALID if it was + * built without WOLFPKCS11_LMS. */ #define CKK_HSS 0x00000046UL -#endif #ifdef WOLFPKCS11_NSS /* Not defined by NSS, but we need one */ @@ -265,15 +266,13 @@ extern "C" { /* KEM */ #define CKA_ENCAPSULATE 0x00000633UL #define CKA_DECAPSULATE 0x00000634UL -#ifdef WOLFPKCS11_LMS -/* LMS/HSS (RFC 8554) */ +/* LMS/HSS (RFC 8554). Exposed unconditionally — see comment on CKK_HSS. */ #define CKA_HSS_LEVELS 0x00000617UL #define CKA_HSS_LMS_TYPE 0x00000618UL #define CKA_HSS_LMOTS_TYPE 0x00000619UL #define CKA_HSS_LMS_TYPES 0x0000061AUL #define CKA_HSS_LMOTS_TYPES 0x0000061BUL #define CKA_HSS_KEYS_REMAINING 0x0000061CUL -#endif #ifdef WOLFPKCS11_NSS #define CKA_NSS_EMAIL (CKA_NSS + 2) @@ -377,10 +376,9 @@ extern "C" { #define CKM_ML_DSA_KEY_PAIR_GEN 0x0000001CUL #define CKM_ML_DSA 0x0000001DUL #define CKM_HASH_ML_DSA 0x0000001FUL -#ifdef WOLFPKCS11_LMS +/* LMS/HSS mechanisms (RFC 8554). Exposed unconditionally — see CKK_HSS. */ #define CKM_HSS_KEY_PAIR_GEN 0x00004032UL #define CKM_HSS 0x00004033UL -#endif #ifdef WOLFPKCS11_NSS #define CKM_NSS_TLS_PRF_GENERAL_SHA256 (CKM_NSS + 21) @@ -893,8 +891,13 @@ typedef CK_ULONG CK_ML_KEM_PARAMETER_SET_TYPE; #define CKP_ML_KEM_768 0x00000002UL #define CKP_ML_KEM_1024 0x00000003UL -#ifdef WOLFPKCS11_LMS -/* HSS / LMS / LMOTS algorithm identifiers (RFC 8554) used in CK_HSS_PARAMS. */ +/* HSS / LMS / LMOTS algorithm identifiers (RFC 8554) used in CK_HSS_PARAMS. + * + * These typedefs and the CK_HSS_PARAMS struct are exposed unconditionally + * so a downstream consumer that includes compiles + * regardless of whether the wolfPKCS11 library was built with + * WOLFPKCS11_LMS. If the library is built without HSS support, the + * mechanism is rejected at runtime with CKR_MECHANISM_INVALID. */ typedef CK_ULONG CK_HSS_LEVELS; typedef CK_ULONG CK_LMS_TYPE; typedef CK_ULONG CK_LMOTS_TYPE; @@ -928,7 +931,6 @@ typedef struct CK_HSS_PARAMS { CK_LMOTS_TYPE lm_ots_type[CK_HSS_PARAMS_MAX_LEVELS]; /* per-level LMOTS typecode */ } CK_HSS_PARAMS; typedef CK_HSS_PARAMS* CK_HSS_PARAMS_PTR; -#endif /* WOLFPKCS11_LMS */ /* Function list types. */