diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index 428929dc..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,8 +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 C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --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 1400247c..9d5badc0 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -110,6 +110,30 @@ 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 + # 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/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..eb520e0d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,71 @@ 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_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 #### Define WOLFPKCS11_TPM_STORE @@ -219,6 +284,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 | @@ -246,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/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..b4901486 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[] = { @@ -257,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 }, @@ -678,6 +695,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 +777,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 +4527,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 +4925,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 +5660,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 +6004,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 +7305,79 @@ 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; + 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; + hssParams = (const CK_HSS_PARAMS*)pMechanism->pParameter; + 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. + * + * 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) { + 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) { + if (tokAttr->pValue == NULL || + tokAttr->ulValueLen != sizeof(CK_BBOOL)) { + return CKR_ATTRIBUTE_VALUE_INVALID; + } + 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); + 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 || @@ -7364,7 +7524,6 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, rv = AddObject(session, priv, pPrivateKeyTemplate, ulPrivateKeyAttributeCount, phPrivateKey); } - if (pub != NULL && rv == CKR_OK) { rv = SetInitialStates(pub); } diff --git a/src/internal.c b/src/internal.c index b52bfd18..67ad99e7 100644 --- a/src/internal.c +++ b/src/internal.c @@ -45,6 +45,9 @@ #ifdef WOLFPKCS11_MLKEM #include #endif +#ifdef WOLFPKCS11_LMS +#include +#endif #if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) /* OS-specific includes for directory creation */ @@ -260,6 +263,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 @@ -288,6 +294,20 @@ 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 + /* 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 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 statefulSigCount; +#endif WP11_Session* session; /* Session object belongs to */ WP11_Slot* slot; /* Slot object belongs to */ @@ -1103,10 +1123,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. */ +static 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 +1174,86 @@ 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. 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; + 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) { + 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; + 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'; + else + XSTRNCPY(dirCopy, ".", sizeof(dirCopy)); + dirFd = open(dirCopy, O_RDONLY +#ifdef O_DIRECTORY + | O_DIRECTORY +#endif + ); + 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); + } +#endif + } + return ret; } @@ -1413,6 +1529,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; @@ -1696,26 +1828,50 @@ 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 + * 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 || _commit(fd) != 0) + fsyncFailed = 1; + } +#else + { + int fd = fileno(ctx->file); + if (fd < 0 || fsync(fd) != 0) + fsyncFailed = 1; + } +#endif + } XFCLOSE(ctx->file); ctx->file = XBADFILE; } @@ -1728,16 +1884,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); } /** @@ -2497,6 +2663,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 +3100,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 +3303,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,608 +4824,1207 @@ static int wp11_Object_Store_MldsaKey(WP11_Object* object, int tokenId, } #endif /* WOLFPKCS11_MLDSA */ -#ifndef NO_DH -/** - * Decode the DH key. +#ifdef WOLFPKCS11_STATEFUL_SIG_ANY +/* =========================================================================== + * Stateful hash-based signature framework + * =========================================================================== * - * Encoded private keys are encrypted. + * 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. * - * @param [in, out] object DH key object. - * @return 0 on success. - * @return -ve on failure. + * Contract every stateful-sig scheme MUST honour: + * + * 1. Token-resident only. Session-only stateful keys are unsafe (a process + * crash between sign and cleanup can release a signature whose OTS + * index was never persisted). Reject CKA_TOKEN=FALSE in C_GenerateKeyPair + * with CKR_TEMPLATE_INCONSISTENT. + * + * 2. State file keyed by a per-key 64-bit nonce, NOT by the object's + * position in the token list. The nonce is generated at keygen time + * and stored in the shell file's last 8 bytes. WP11_Object holds a + * copy in `statefulStateId`. Token-list reordering (Add/Remove, + * Slot_Store) must NEVER move or delete the state file. + * + * 3. State persistence is synchronous and durable. The wolfSSL write + * callback writes the encrypted state file with fsync + atomic rename + * + parent-directory fsync (unless WOLFPKCS11_STATEFUL_RELAX_FSYNC=1) + * BEFORE returning success — so wolfSSL never releases a signature + * whose state advance has not reached stable storage. + * + * 4. State file uses AES-GCM (slot-key) with the AAD-bound header below. + * Tampering with parameters or the persisted sigCount fails decrypt. + * + * 5. SigCount tracked in the wp11 layer (`statefulSigCount`), persisted + * inside the AAD-bound header, and used to compute KEYS_REMAINING — + * no reads from wolfSSL key internals (priv_raw layout is not public + * API and varies between backends). + * + * 6. Poison-on-failure. Any state-write failure or sign error clears + * WP11_FLAG_STATEFUL_STATE_VALID; subsequent signs return + * CKR_DEVICE_ERROR until the object is reloaded from disk + * (wolfSSL Reload restores in-memory state from the durable file). + * + * 7. Locking: all post-keygen state-mutating paths run under the token + * lock (priv->lock points at &token->lock for token-resident keys). + * EXCEPTION: during the keygen function itself, priv is thread-local + * (no other thread has the handle yet, and priv->lock has not been + * assigned by WP11_Session_AddObject), so the write callback fires + * unlocked. This is safe only because no concurrent reader can find + * priv before keygen returns. + * + * On-disk layouts: + * + * Shell file (per scheme; non-secret metadata): + * [u32 magic][u32 version][scheme-specific params + pub][u64 stateId] + * The trailer (stateId) is always the last 8 bytes — that's how + * wp11_Stateful_PeekStateIdFromShell finds it without parsing. + * + * State file (AES-GCM, AAD-bound): + * AAD: [u32 magic][u32 version][scheme params (P bytes)][u64 sigCount] + * [u32 ivLen][iv (12 bytes)] + * [u32 ctLen][ciphertext (privSz + 16-byte GCM tag)] */ -static int wp11_Object_Decode_DhKey(WP11_Object* object) + +/* Big-endian u32 reader. Always built — used by verify-only shell parsing. */ +static word32 wp11_Stateful_ReadU32(const byte* p) { - int ret = 0; + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; +} - if (object->objClass == CKO_PRIVATE_KEY) { - ret = wp11_DecryptData(object->data.dhKey->key, object->keyData, - object->keyDataLen - AES_BLOCK_SIZE, - object->slot->token.key, - sizeof(object->slot->token.key), object->iv, - sizeof(object->iv), object->devId); - if (ret == 0) - object->data.dhKey->len = object->keyDataLen - AES_BLOCK_SIZE; - } - else { - XMEMCPY(object->data.dhKey->key, object->keyData, object->keyDataLen); - object->data.dhKey->len = object->keyDataLen; - } - object->encoded = (ret != 0); +#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]; +} - return ret; +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; } -/** - * Encode the DH key. - * - * Private keys are encrypted. - * - * @param [in, out] object DH key object. - * @return 0 on success. - * @return -ve on failure. - */ -static int wp11_Object_Encode_DhKey(WP11_Object* object) +static void wp11_Stateful_WriteU64(byte* p, word64 v) { - int ret = 0; + p[0] = (byte)(v >> 56); + p[1] = (byte)(v >> 48); + p[2] = (byte)(v >> 40); + p[3] = (byte)(v >> 32); + p[4] = (byte)(v >> 24); + p[5] = (byte)(v >> 16); + p[6] = (byte)(v >> 8); + p[7] = (byte)v; +} - object->keyDataLen = object->data.dhKey->len + AES_BLOCK_SIZE; - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - /* Allocate buffer to hold encoded key. */ - object->keyData = (unsigned char*)XMALLOC(object->keyDataLen, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) - ret = MEMORY_E; +/* IV length for the AES-GCM-encrypted state file. The full 96-bit GCM + * standard IV; same for every scheme. */ +#define WP11_STATEFUL_IV_LEN 12 - if (ret == 0) { - if (object->objClass == CKO_PRIVATE_KEY) { - ret = wp11_EncryptData(object->keyData, object->data.dhKey->key, - object->data.dhKey->len, - object->slot->token.key, - sizeof(object->slot->token.key), object->iv, - sizeof(object->iv), object->devId); - if (ret == 0) - object->keyDataLen = object->data.dhKey->len + AES_BLOCK_SIZE; - } - else { - XMEMCPY(object->keyData, object->data.dhKey->key, - object->data.dhKey->len); - object->keyDataLen = object->data.dhKey->len; +/* 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 ret; + return wp11_StatefulRelaxFsync ? 0 : 1; } -/** - * Load an DH key from storage. +/* 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. * - * @param [in, out] object DH key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. - * @return 0 on success. - * @return MEMORY_E when dynamic memory allocation fails. - * @return BUFFER_E when loading fails. - * @return NOT_AVAILABLE_E when unable to locate data. - */ -static int wp11_Object_Load_DhKey(WP11_Object* object, int tokenId, int objId) + * WOLFPKCS11_TPM_STORE is forbidden combined with WOLFPKCS11_LMS_PRIVATE + * (compile-time #error in wolfpkcs11/internal.h), so this code path is + * file-backed-storage only. */ +static int wp11_Stateful_StateFile_Path(const char* schemeFilePrefix, + int tokenId, word64 stateId, char* name, int nameLen) { - int ret; - void* storage = NULL; - unsigned char* der = NULL; - int len; - word32 idx = 0; - int storeType; - - /* Determine store type - private keys may be encrypted. */ - if (object->objClass == CKO_PRIVATE_KEY) - storeType = WOLFPKCS11_STORE_DHKEY_PRIV; - else - storeType = WOLFPKCS11_STORE_DHKEY_PUB; + 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 }; - /* Open access to DH key. */ - ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); - if (ret == 0) { - /* Read DH key. */ - ret = wp11_storage_read_alloc_array(storage, &object->keyData, - &object->keyDataLen); - if (ret == 0) { - /* Read DER encoded DH parameters. */ - ret = wp11_storage_read_alloc_array(storage, &der, &len); - } - if (ret == 0) { - /* Decode DH parameters. */ - ret = wc_DhKeyDecode(der, &idx, &object->data.dhKey->params, len); + 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; } - if (der != NULL) { - XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#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; } - - wp11_storage_close(storage); +#endif } - - return ret; +#ifdef WOLFPKCS11_DEFAULT_TOKEN_PATH + if (str == NULL) + str = WC_STRINGIFY(WOLFPKCS11_DEFAULT_TOKEN_PATH); +#endif + if (str == NULL) + return -1; + if (nameLen <= WP11_STATEFUL_STATE_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); } -#if !defined(WOLFSSL_DH_EXTRA) || LIBWOLFSSL_VERSION_HEX < 0x04008000 -/* Calculates the minimum number of bytes required to encode the value. - * - * @param [in] value Value to be encoded. - * @return Number of bytes to encode value. - */ -static word32 wp11_BytePrecision(word32 value) -{ - word32 i; - for (i = (word32)sizeof(value); i; --i) - if (value >> ((i - 1) * 8)) - break; - - return i; -} - -/* Encode a length for DER. - * - * @param [in] length Value to encode. - * @param [out] output Buffer to encode into. - * @return Number of bytes encoded. - */ -static word32 wp11_SetLength(word32 length, byte* output) +/* 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) { - /* Start encoding at start of buffer. */ - word32 i = 0; + int ret; + char name[WP11_STORE_MAX_PATH] = ""; + WP11_FileStoreCtx* ctx = NULL; - if (length < 0x80) { - /* Only one byte needed to encode. */ - if (output) { - /* Write out length value. */ - output[i] = (byte)length; - } - /* Skip over length. */ - i++; +#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; } - else { - /* Calculate the number of bytes required to encode value. */ - byte j = (byte)wp11_BytePrecision(length); + 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 (output) { - /* Encode count byte. */ - output[i] = j | 0x80; + if (ret == 0 && read) { + ctx->file = XFOPEN(name, "rb"); + if (ctx->file == NULL) + ret = NOT_AVAILABLE_E; } - /* Skip over count byte. */ - i++; - - /* Encode value as a big-endian byte array. */ - for (; j > 0; --j) { - if (output) { - /* Encode next most-significant byte. */ - output[i] = (byte)(length >> ((j - 1) * 8)); + 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); + } } - /* Skip over byte. */ - i++; } } - - /* Return number of bytes in encoded length. */ - return i; + 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; } -/* Encode DH key parameters to DER format into output and setting len to outSz. - * - * If output is NULL then max expected size is set to outSz and LENGTH_ONLY_E is - * returned. - * - * Assumes key and outSz are not NULL. - * - * @param [in] key Dh key with parameters. - * @param [out] output Buffer to place DER data. May be NULL. - * @param [out] outSz Length of DER data. - * - * @return Number of bytes written on success - * @return LENGTH_ONLY_E when NULL output buffer passed in. outSz will be set. - * @return Other -ve on failure. - */ -static int wp11_DhParamsToDer(DhKey* key, byte* output, word32* outSz) +static int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, + int tokenId, word64 stateId) { - int ret = 0; - word32 len = 0, idx = 0, len2; + int ret; + char name[WP11_STORE_MAX_PATH]; - len = 5; /* Sequence */ - len += 1 + 4; /* Integer */ - len += mp_leading_bit(&key->p) ? 1 : 0; - len += mp_unsigned_bin_size(&key->p); - len += 1 + 4; /* Integer */ - len += mp_leading_bit(&key->g) ? 1 : 0; - len += mp_unsigned_bin_size(&key->g); +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) + return NOT_AVAILABLE_E; +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + return remove(name); + return -1; +} + +/* Sanity caps for inputs to the generic state-blob helpers. The wolfSSL + * state buffer for any practical scheme is well below 1 MiB; the scheme + * parameter region is small (HSS = 12 bytes, XMSS = 4-12 bytes). Reject + * absurd inputs up front so the size-arithmetic below cannot overflow. */ +#define WP11_STATEFUL_PRIV_MAX (1U << 20) /* 1 MiB */ +#define WP11_STATEFUL_PARAMS_MAX 256U + +/* Persist the encrypted state file durably. Caller passes the scheme's + * (schemeFilePrefix, magic, version) and a packed scheme-params buffer + * that is bound into the GCM AAD. On a successful return the state file + * at (slot_id, statefulStateId) is fsync'd and renamed atomically. + * + * AAD-bound layout (caught by AES-GCM tag): + * [u32 magic][u32 version][u32 schemeParamLen][schemeParamBytes] + * [u64 sigCount] + * Including schemeParamLen in the AAD diagnoses thin-wrapper version skew + * cleanly (a length mismatch surfaces as a header-mismatch BAD_FUNC_ARG + * before decrypt rather than as opaque AES-GCM tag failure). */ +static int wp11_Stateful_WriteStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* schemeParamBytes, word32 schemeParamLen, + const byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* hdr = NULL; + word32 hdrLen; + byte* ct = NULL; + word32 ctLen; + int tokenId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + /* Stateful keys are token-resident only — reject any attempt to + * persist state for a session-only object. The HSS write CB enforces + * this too, but the framework helper enforces it for all schemes. */ + if (!o->onToken) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (schemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + /* Defensive caps: prevent size-arithmetic overflow. */ + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; - if (output == NULL) { - *outSz = len; - return LENGTH_ONLY_E; - } + 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; - if (ret == 0) { - idx = len; - len2 = mp_unsigned_bin_size(&key->g); - idx -= len2; - ret = mp_to_unsigned_bin(&key->g, output + idx); + wp11_Stateful_WriteU32(hdr + 0, magic); + wp11_Stateful_WriteU32(hdr + 4, version); + wp11_Stateful_WriteU32(hdr + 8, schemeParamLen); + if (schemeParamLen > 0) + XMEMCPY(hdr + 12, schemeParamBytes, schemeParamLen); + wp11_Stateful_WriteU64(hdr + 12 + schemeParamLen, o->statefulSigCount); + + /* Fresh GCM nonce per write (NEVER reuse object->iv). */ + ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_STATEFUL_IV_LEN); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; } - if (ret >= 0) { - if (mp_leading_bit(&key->g)) { - output[--idx] = 0x00; - len2++; - } - idx -= wp11_SetLength(len2, NULL); - wp11_SetLength(len2, output + idx); - output[--idx] = 0x02; - len2 = mp_unsigned_bin_size(&key->p); - idx -= len2; - ret = mp_to_unsigned_bin(&key->p, output + idx); + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return MEMORY_E; } - if (ret >= 0) { - if (mp_leading_bit(&key->p)) { - output[--idx] = 0x00; - len2++; - } - idx -= wp11_SetLength(len2, NULL); - wp11_SetLength(len2, output + idx); - output[--idx] = 0x02; - len2 = len - idx; - idx -= wp11_SetLength(len2, NULL); - idx -= 1; - output[idx] = 0x30; - wp11_SetLength(len2, output + idx + 1); + 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; } - if (ret >= 0) { - XMEMMOVE(output, output + idx, len - idx); - *outSz = len - idx; + + /* total bytes to write: hdr + ivLen u32 + iv + ctLen u32 + ct||tag — + * unused for file storage, but kept for parity with TPM_STORE. */ + (void)(hdrLen + 4 + WP11_STATEFUL_IV_LEN + 4 + ctLen); + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 0, &storage); + if (ret == 0) { + if (wp11_StatefulShouldFsync()) + wolfPKCS11_Store_SetDurable(storage, 1); + ret = wp11_storage_write(storage, hdr, (int)hdrLen); + if (ret == 0) { + byte ivLenBuf[4]; + wp11_Stateful_WriteU32(ivLenBuf, WP11_STATEFUL_IV_LEN); + ret = wp11_storage_write(storage, ivLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, iv, WP11_STATEFUL_IV_LEN); + if (ret == 0) { + byte ctLenBuf[4]; + wp11_Stateful_WriteU32(ctLenBuf, ctLen); + ret = wp11_storage_write(storage, ctLenBuf, 4); + } + if (ret == 0) + ret = wp11_storage_write(storage, ct, (int)ctLen); + /* Capture the close-and-commit result. A void close would mask + * a failed rename/fsync; that would let wolfSSL release a signature + * whose state advance never reached durable storage. */ + { + int closeRet = wolfPKCS11_Store_CloseAndReport(storage); + if (ret == 0) + ret = closeRet; + } } + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); return ret; } -#define wc_DhParamsToDer wp11_DhParamsToDer -#endif /* !WOLFSSL_DH_EXTRA || LIBWOLFSSL_VERSION_HEX < 0x04008000 */ - -/** - * Store an DH key to storage. - * - * @param [in] object DH key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. - * @return 0 on success. - * @return MEMORY_E when dynamic memory allocation fails. - * @return BUFFER_E when storing fails. - * @return NOT_AVAILABLE_E when unable to write data. - */ -static int wp11_Object_Store_DhKey(WP11_Object* object, int tokenId, int objId) +/* Read and decrypt the state file. The full header is verified as AAD by + * the AES-GCM tag; tampering with magic, version, scheme params, scheme- + * params length, or the persisted sigCount fails decrypt. The expected + * scheme params are passed by the caller (typically: re-pack from the + * in-memory wolfSSL key); a mismatch returns BAD_FUNC_ARG before decrypt. + * The decrypted sigCount is restored into o->statefulSigCount. */ +static int wp11_Stateful_ReadStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* expectedSchemeParamBytes, word32 schemeParamLen, + byte* priv, word32 privSz) { - int ret = 0; + int ret; void* storage = NULL; - unsigned char* der = NULL; - word32 len = 0; - int storeType; + byte* hdr = NULL; + word32 hdrLen; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* ct = NULL; + word32 m, v, persistedParamLen, ivLen, ctLen; + word64 sigCount = 0; + int tokenId; + + if (o == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (expectedSchemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; - if (object->keyData == NULL) { - ret = wp11_Object_Encode_DhKey(object); + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) { + wc_ForceZero(priv, privSz); + return MEMORY_E; + } + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 1, &storage); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_ForceZero(priv, privSz); + return ret; } + + ret = wp11_storage_read(storage, hdr, (int)hdrLen); if (ret == 0) { - /* Get length of encoded DH parameters. */ - ret = wc_DhParamsToDer(&object->data.dhKey->params, NULL, &len); - if (ret == LENGTH_ONLY_E) { - ret = 0; + m = wp11_Stateful_ReadU32(hdr); + v = wp11_Stateful_ReadU32(hdr + 4); + persistedParamLen = wp11_Stateful_ReadU32(hdr + 8); + sigCount = wp11_Stateful_ReadU64(hdr + 12 + schemeParamLen); + if (m != magic || v != version) { + ret = BAD_FUNC_ARG; + } + else if (persistedParamLen != schemeParamLen) { + /* Scheme-params length mismatch — diagnoses thin-wrapper + * version skew before AES-GCM tag failure. */ + ret = BAD_FUNC_ARG; + } + else if (schemeParamLen > 0 && + XMEMCMP(hdr + 12, expectedSchemeParamBytes, schemeParamLen) + != 0) { + ret = BAD_FUNC_ARG; } } if (ret == 0) { - /* Allocate buffer to hold encoded key. */ - der = (unsigned char*)XMALLOC(len, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (der == NULL) - ret = MEMORY_E; + 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) { - /* Encode DH parameters. */ - ret = wc_DhParamsToDer(&object->data.dhKey->params, der, &len); - if (ret >= 0) { - 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; + } } } - - /* Determine store type - private keys may be encrypted. */ - if (object->objClass == CKO_PRIVATE_KEY) - storeType = WOLFPKCS11_STORE_DHKEY_PRIV; - else - storeType = WOLFPKCS11_STORE_DHKEY_PUB; - - /* Open access to DH key. */ if (ret == 0) { - ret = wp11_storage_open(storeType, tokenId, objId, - object->keyDataLen + len, &storage); + 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_storage_write_array(storage, object->keyData, - object->keyDataLen); - if (ret == 0) { - /* Write encoded DH parameters to storage. */ - ret = wp11_storage_write_array(storage, der, len); - } - - wp11_storage_close(storage); + 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. */ } - if (der != NULL) { - XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); + 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; } -#endif /* !NO_DH */ -#ifdef WOLFPKCS11_MLKEM -static int MlKemKeyTryDecode(MlKemKey* key, int level, byte* data, word32 len, - int devId, CK_OBJECT_CLASS objClass) +/* Recover the per-key stateId from the on-disk shell file. The shell + * format is otherwise scheme-specific, but by contract the LAST 8 bytes + * are always the u64 stateId. Used by Unstore to find the state file + * before deleting it, when the in-memory object hasn't been decoded yet. + * + * Validates the shell's magic + version against caller-supplied values + * before returning the trailer. Without this check, a truncated, replaced, + * or wrong-scheme file at the same (tokenId, objId) would yield a garbage + * stateId that the caller then passes to Store_Remove — silently no-op'ing + * or deleting the wrong file while the real state file orphans on disk. + * + * Returns BAD_FUNC_ARG if magic or version don't match (e.g., wrong scheme, + * file truncated below the header size, or the file is something else). */ +static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, + int storeTypeShell, word32 expectedShellMagic, word32 expectedShellVersion, + word64* outStateId) { int ret; + void* storage = NULL; + unsigned char* buf = NULL; + int bufLen = 0; - ret = wc_MlKemKey_Init(key, level, NULL, devId); - if (ret == 0) { - if (objClass == CKO_PRIVATE_KEY) { - ret = wc_MlKemKey_DecodePrivateKey(key, data, len); - } - else { - ret = wc_MlKemKey_DecodePublicKey(key, data, len); - } + if (outStateId == NULL) + return BAD_FUNC_ARG; + *outStateId = 0; + + ret = wp11_storage_open_readonly(storeTypeShell, + (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); + if (ret != 0) + return ret; + ret = wp11_storage_read_alloc_array(storage, &buf, &bufLen); + wp11_storage_close(storage); + if (ret != 0) + return ret; + + /* Need at least [u32 magic][u32 version] + [u64 stateId trailer]. */ + if (bufLen < 16) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + if (wp11_Stateful_ReadU32(buf) != expectedShellMagic || + wp11_Stateful_ReadU32(buf + 4) != expectedShellVersion) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; } + *outStateId = wp11_Stateful_ReadU64(buf + (bufLen - 8)); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return 0; +} - if (ret != 0) { - wc_MlKemKey_Free(key); +#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 + * 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 2U + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* 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. */ +#define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ +#define WP11_HSS_STATE_VERSION 2U + +/* 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. */ +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; } +} - return ret; +/* 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; + } } -/** - * Decode the ML-KEM key. - * - * Encoded private keys are encrypted. +/* 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 [in, out] object ML-KEM key object. - * @return 0 on success. - * @return -ve on failure. - */ -static int wp11_Object_Decode_MlKemKey(WP11_Object* object) + * @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) { - int ret = 0; + if (levels == NULL || height == NULL || winternitz == NULL) + return BAD_FUNC_ARG; - if (object->objClass == CKO_PRIVATE_KEY) { - unsigned char* der; - int len; + 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; + /* 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; + } - if (object->keyDataLen < AES_BLOCK_SIZE) + { + 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; - len = object->keyDataLen - AES_BLOCK_SIZE; + /* 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; +} - der = (unsigned char*)XMALLOC(len, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (der == NULL) { - ret = MEMORY_E; +/* 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; +} + +/* 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_Stateful_ReadU32(in + idx); idx += 4; + version = wp11_Stateful_ReadU32(in + idx); idx += 4; + if (magic != WP11_HSS_SHELL_MAGIC || version != WP11_HSS_SHELL_VERSION) + return BAD_FUNC_ARG; + *levels = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *height = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *winternitz = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + pl = wp11_Stateful_ReadU32(in + idx); idx += 4; + /* Cap pubLen against the 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; + *pubLen = pl; + return 0; +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* 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) +{ + 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 levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_WriteStateBlob(o, "hsskey", + WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, + priv, privSz); +} + +/* 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 levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_ReadStateBlob(o, "hsskey", + 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->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; + if (o == NULL || priv == NULL || privSz == 0) + 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->statefulStateId == 0) + return WC_LMS_RC_WRITE_FAIL; + + if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_WRITE_FAIL; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + if (o == NULL) + return WC_LMS_RC_BAD_ARG; + if (wp11_Hss_ReadStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_READ_FAIL; + return WC_LMS_RC_READ_TO_MEMORY; +} + +/* Recover the statefulStateId from the on-disk HSS shell file. Thin wrapper + * over the generic helper — by contract the stateId is always the last 8 + * 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, + WP11_HSS_SHELL_MAGIC, WP11_HSS_SHELL_VERSION, outStateId); +} +#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) { + 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) { - ret = wp11_DecryptData(der, object->keyData, len, - object->slot->token.key, - sizeof(object->slot->token.key), object->iv, - sizeof(object->iv), object->devId); + object->statefulStateId = wp11_Stateful_ReadU64(object->keyData + idx); + if (object->statefulStateId == 0) + ret = BAD_FUNC_ARG; } + if (ret == 0) + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, object->devId); if (ret == 0) { - ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_512, - der, len, object->devId, object->objClass); - if (ret != 0) { - ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_768, - der, len, object->devId, - object->objClass); - } - if (ret != 0) { - ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_1024, - der, len, object->devId, - object->objClass); - } + ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, + height, winternitz); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); } - if (der != NULL) { - wc_ForceZero(der, len); - XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ret == 0) { + ret = wc_LmsKey_SetWriteCb(object->data.lmsKey, + wp11_Hss_WriteState_Cb); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); } - } - else { - ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_512, - object->keyData, object->keyDataLen, - object->devId, object->objClass); - if (ret != 0) { - ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_768, - object->keyData, object->keyDataLen, - object->devId, object->objClass); + if (ret == 0) { + ret = wc_LmsKey_SetReadCb(object->data.lmsKey, + wp11_Hss_ReadState_Cb); + if (ret != 0) + wc_LmsKey_Free(object->data.lmsKey); } - if (ret != 0) { - ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_1024, - object->keyData, object->keyDataLen, - object->devId, object->objClass); + if (ret == 0) { + ret = wc_LmsKey_SetContext(object->data.lmsKey, object); + 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) { + 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_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, + * 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); - return ret; } -/** - * Encode the ML-KEM key. - * - * Private keys are encoded and then encrypted. +/* 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. * - * @param [in, out] object ML-KEM key object. - * @return 0 on success. - * @return -ve on failure. - */ -static int wp11_Object_Encode_MlKemKey(WP11_Object* object) + * 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; - word32 keyLen = 0; + int ret = 0; + int levels = 0, height = 0, winternitz = 0; + 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; - if (object->objClass == CKO_PRIVATE_KEY) { - ret = wc_MlKemKey_PrivateKeySize(object->data.mlKemKey, &keyLen); - if (ret == 0) { - object->keyDataLen = keyLen + AES_BLOCK_SIZE; - } - } - else { - ret = wc_MlKemKey_PublicKeySize(object->data.mlKemKey, &keyLen); - if (ret == 0) { - object->keyDataLen = keyLen; - } - } + 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) { + needed = 24 + pubLen + (isPriv ? 8U : 0U); XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(object->keyDataLen, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) + object->keyData = (unsigned char*)XMALLOC(needed, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) { ret = MEMORY_E; - } - - if (ret == 0 && object->objClass == CKO_PRIVATE_KEY) { - ret = wc_MlKemKey_EncodePrivateKey(object->data.mlKemKey, - object->keyData, keyLen); - if (ret == 0) { - ret = wp11_EncryptData(object->keyData, object->keyData, keyLen, - object->slot->token.key, - sizeof(object->slot->token.key), object->iv, - sizeof(object->iv), object->devId); + } + else { + hdrLen = wp11_HssPackShellHeader(object->keyData, needed, + levels, height, winternitz, pub, pubLen); + if (hdrLen < 0) { + ret = hdrLen; + } + else { + if (isPriv) { +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->statefulStateId == 0) { + ret = BAD_FUNC_ARG; + } + else { + wp11_Stateful_WriteU64(object->keyData + hdrLen, + object->statefulStateId); + object->keyDataLen = hdrLen + 8; + } +#else + /* Verify-only build never encodes private shells. */ + ret = BAD_FUNC_ARG; +#endif + } + else { + object->keyDataLen = hdrLen; + } + } } } - else if (ret == 0 && object->objClass == CKO_PUBLIC_KEY) { - ret = wc_MlKemKey_EncodePublicKey(object->data.mlKemKey, - object->keyData, keyLen); - } - + wc_ForceZero(pub, sizeof(pub)); if (ret != 0) { - if (object->keyData != NULL) - wc_ForceZero(object->keyData, object->keyDataLen); XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); object->keyData = NULL; object->keyDataLen = 0; } - return ret; } -/** - * Load a ML-KEM key from storage. - * - * @param [in, out] object ML-KEM key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. - * @return 0 on success. - * @return MEMORY_E when dynamic memory allocation fails. - * @return BUFFER_E when loading fails. - * @return NOT_AVAILABLE_E when unable to locate data. - */ -static int wp11_Object_Load_MlKemKey(WP11_Object* object, int tokenId, - int objId) +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_MLKEMKEY_PRIV; + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; else - storeType = WOLFPKCS11_STORE_MLKEMKEY_PUB; + storeType = WOLFPKCS11_STORE_HSSKEY_PUB; - ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); + 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); + &object->keyDataLen); wp11_storage_close(storage); } - return ret; } -/** - * Store a ML-KEM key to storage. - * - * @param [in] object ML-KEM key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. - * @return 0 on success. - * @return MEMORY_E when dynamic memory allocation fails. - * @return BUFFER_E when storing fails. - * @return NOT_AVAILABLE_E when unable to write data. - */ -static int wp11_Object_Store_MlKemKey(WP11_Object* object, int tokenId, - int objId) +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_MlKemKey(object); - if (ret != 0) - return ret; - } + if (object->keyData == NULL) + ret = wp11_Object_Encode_HssKey(object); if (object->objClass == CKO_PRIVATE_KEY) - storeType = WOLFPKCS11_STORE_MLKEMKEY_PRIV; + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; else - storeType = WOLFPKCS11_STORE_MLKEMKEY_PUB; + storeType = WOLFPKCS11_STORE_HSSKEY_PUB; - ret = wp11_storage_open(storeType, tokenId, objId, object->keyDataLen, - &storage); + 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); + object->keyDataLen); wp11_storage_close(storage); } - return ret; } -#endif /* WOLFPKCS11_MLKEM */ +#endif /* WOLFPKCS11_LMS */ + +#ifndef NO_DH /** - * Decode the symmetric key - requires decryption. + * Decode the DH key. * - * @param [in, out] object Symmetric key object. + * Encoded private keys are encrypted. + * + * @param [in, out] object DH key object. * @return 0 on success. * @return -ve on failure. */ -static int wp11_Object_Decode_SymmKey(WP11_Object* object) +static int wp11_Object_Decode_DhKey(WP11_Object* object) { int ret = 0; - if (object->keyDataLen - AES_BLOCK_SIZE > WP11_MAX_SYM_KEY_SZ) - ret = BUFFER_E; - if (ret == 0) { - ret = wp11_DecryptData(object->data.symmKey->data, object->keyData, + if (object->objClass == CKO_PRIVATE_KEY) { + ret = wp11_DecryptData(object->data.dhKey->key, object->keyData, object->keyDataLen - AES_BLOCK_SIZE, object->slot->token.key, sizeof(object->slot->token.key), object->iv, sizeof(object->iv), object->devId); + if (ret == 0) + object->data.dhKey->len = object->keyDataLen - AES_BLOCK_SIZE; + } + else { + XMEMCPY(object->data.dhKey->key, object->keyData, object->keyDataLen); + object->data.dhKey->len = object->keyDataLen; } - if (ret == 0) - object->data.symmKey->len = object->keyDataLen - AES_BLOCK_SIZE; object->encoded = (ret != 0); return ret; } /** - * Encode the symmetric key - requires encryption. + * Encode the DH key. * - * @param [in, out] object Symmetric key object. + * Private keys are encrypted. + * + * @param [in, out] object DH key object. * @return 0 on success. * @return -ve on failure. */ -static int wp11_Object_Encode_SymmKey(WP11_Object* object) +static int wp11_Object_Encode_DhKey(WP11_Object* object) { int ret = 0; - object->keyDataLen = object->data.symmKey->len + AES_BLOCK_SIZE; + object->keyDataLen = object->data.dhKey->len + AES_BLOCK_SIZE; XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); /* Allocate buffer to hold encoded key. */ object->keyData = (unsigned char*)XMALLOC(object->keyDataLen, NULL, @@ -5157,372 +6033,478 @@ static int wp11_Object_Encode_SymmKey(WP11_Object* object) ret = MEMORY_E; if (ret == 0) { - ret = wp11_EncryptData(object->keyData, object->data.symmKey->data, - object->data.symmKey->len, + if (object->objClass == CKO_PRIVATE_KEY) { + ret = wp11_EncryptData(object->keyData, object->data.dhKey->key, + object->data.dhKey->len, object->slot->token.key, sizeof(object->slot->token.key), object->iv, sizeof(object->iv), object->devId); - if (ret == 0) - object->keyDataLen = object->data.symmKey->len + AES_BLOCK_SIZE; + if (ret == 0) + object->keyDataLen = object->data.dhKey->len + AES_BLOCK_SIZE; + } + else { + XMEMCPY(object->keyData, object->data.dhKey->key, + object->data.dhKey->len); + object->keyDataLen = object->data.dhKey->len; + } } return ret; } /** - * Load an symmetric key from storage. + * Load an DH key from storage. * - * @param [in, out] object Symmetric key object. + * @param [in, out] object DH key object. * @param [in] tokenId Id of token this key belongs to. * @param [in] objId Id of object for token. * @return 0 on success. + * @return MEMORY_E when dynamic memory allocation fails. * @return BUFFER_E when loading fails. * @return NOT_AVAILABLE_E when unable to locate data. */ -static int wp11_Object_Load_SymmKey(WP11_Object* object, int tokenId, int objId) +static int wp11_Object_Load_DhKey(WP11_Object* object, int tokenId, int objId) { int ret; void* storage = NULL; + unsigned char* der = NULL; + int len; + word32 idx = 0; + int storeType; - /* Open access to symmetric key. */ - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_SYMMKEY, tokenId, objId, - &storage); + /* Determine store type - private keys may be encrypted. */ + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_DHKEY_PRIV; + else + storeType = WOLFPKCS11_STORE_DHKEY_PUB; + + /* Open access to DH key. */ + ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); if (ret == 0) { - /* Read symmetric key from storage. */ + /* Read DH key. */ ret = wp11_storage_read_alloc_array(storage, &object->keyData, &object->keyDataLen); + if (ret == 0) { + /* Read DER encoded DH parameters. */ + ret = wp11_storage_read_alloc_array(storage, &der, &len); + } + if (ret == 0) { + /* Decode DH parameters. */ + ret = wc_DhKeyDecode(der, &idx, &object->data.dhKey->params, len); + } + if (der != NULL) { + XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + wp11_storage_close(storage); } return ret; } -/** - * Store a symmetric key to storage. +#if !defined(WOLFSSL_DH_EXTRA) || LIBWOLFSSL_VERSION_HEX < 0x04008000 +/* Calculates the minimum number of bytes required to encode the value. * - * @param [in] object Symmetric key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. - * @return 0 on success. - * @return BUFFER_E when storing fails. - * @return NOT_AVAILABLE_E when unable to write data. + * @param [in] value Value to be encoded. + * @return Number of bytes to encode value. */ -static int wp11_Object_Store_SymmKey(WP11_Object* object, int tokenId, - int objId) +static word32 wp11_BytePrecision(word32 value) { - int ret = 0; - void* storage = NULL; + word32 i; + for (i = (word32)sizeof(value); i; --i) + if (value >> ((i - 1) * 8)) + break; - if (object->keyData == NULL) { - ret = wp11_Object_Encode_SymmKey(object); - } + return i; +} - /* Open access to symmetric key. */ - if (ret == 0) { - ret = wp11_storage_open(WOLFPKCS11_STORE_SYMMKEY, tokenId, objId, - object->keyDataLen, &storage); +/* Encode a length for DER. + * + * @param [in] length Value to encode. + * @param [out] output Buffer to encode into. + * @return Number of bytes encoded. + */ +static word32 wp11_SetLength(word32 length, byte* output) +{ + /* Start encoding at start of buffer. */ + word32 i = 0; + + if (length < 0x80) { + /* Only one byte needed to encode. */ + if (output) { + /* Write out length value. */ + output[i] = (byte)length; + } + /* Skip over length. */ + i++; } - if (ret == 0) { - /* Write symmetric key to storage. */ - ret = wp11_storage_write_array(storage, object->keyData, - object->keyDataLen); + else { + /* Calculate the number of bytes required to encode value. */ + byte j = (byte)wp11_BytePrecision(length); - wp11_storage_close(storage); + if (output) { + /* Encode count byte. */ + output[i] = j | 0x80; + } + /* Skip over count byte. */ + i++; + + /* Encode value as a big-endian byte array. */ + for (; j > 0; --j) { + if (output) { + /* Encode next most-significant byte. */ + output[i] = (byte)(length >> ((j - 1) * 8)); + } + /* Skip over byte. */ + i++; + } } - return ret; + /* Return number of bytes in encoded length. */ + return i; } -static int wp11_Object_Load_Object(WP11_Object* object, int tokenId, int objId) +/* Encode DH key parameters to DER format into output and setting len to outSz. + * + * If output is NULL then max expected size is set to outSz and LENGTH_ONLY_E is + * returned. + * + * Assumes key and outSz are not NULL. + * + * @param [in] key Dh key with parameters. + * @param [out] output Buffer to place DER data. May be NULL. + * @param [out] outSz Length of DER data. + * + * @return Number of bytes written on success + * @return LENGTH_ONLY_E when NULL output buffer passed in. outSz will be set. + * @return Other -ve on failure. + */ +static int wp11_DhParamsToDer(DhKey* key, byte* output, word32* outSz) { - int ret; - void* storage = NULL; - word32 dummy = 0; + int ret = 0; + word32 len = 0, idx = 0, len2; + + len = 5; /* Sequence */ + len += 1 + 4; /* Integer */ + len += mp_leading_bit(&key->p) ? 1 : 0; + len += mp_unsigned_bin_size(&key->p); + len += 1 + 4; /* Integer */ + len += mp_leading_bit(&key->g) ? 1 : 0; + len += mp_unsigned_bin_size(&key->g); + + if (output == NULL) { + *outSz = len; + return LENGTH_ONLY_E; + } - /* Open access to key object. */ - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_OBJECT, tokenId, objId, - &storage); if (ret == 0) { - /* Read the IV. (12) */ - ret = wp11_storage_read_fixed_array(storage, object->iv, - sizeof(object->iv)); - if (ret == 0) { - /* Read handle value. (8) */ - ret = wp11_storage_read_ulong(storage, &object->handle); - } - if (ret == 0) { - /* Read object class. (8) */ - ret = wp11_storage_read_ulong(storage, &object->objClass); - } - if (ret == 0) { - /* Read key gen mechanism. (8) */ - ret = wp11_storage_read_ulong(storage, &object->keyGenMech); - } - if (ret == 0) { - /* Read whether the object is on a token. (1) */ - byte onToken = 0; - ret = wp11_storage_read_boolean(storage, &onToken); - if (ret == 0) { - object->onToken = (onToken != 0); - } - } - if (ret == 0) { - /* Read whether the object is local. (1) */ - byte local = 0; - ret = wp11_storage_read_boolean(storage, &local); - if (ret == 0) { - object->local = (local != 0); - } - } - if (ret == 0) { - /* Unused word32. (4) */ - ret = wp11_storage_read_word32(storage, &dummy); - } - if (ret == 0) { - /* Read the operational flags of the object. (4) */ - ret = wp11_storage_read_word32(storage, &object->opFlag); - } - if (ret == 0) { - /* Read the start date. (8) */ - ret = wp11_storage_read_fixed_array(storage, - (unsigned char*)object->startDate, sizeof(object->startDate)); - } - if (ret == 0) { - /* Read the end date. (8) */ - ret = wp11_storage_read_fixed_array(storage, - (unsigned char*)object->endDate, sizeof(object->endDate)); - } - - if (ret == 0) { - /* Read id for the object. (variable keyIdLen) */ - ret = wp11_storage_read_alloc_array(storage, &object->keyId, - &object->keyIdLen); - } - if (ret == 0) { - /* Read label for the object. (variable labelLen) */ - ret = wp11_storage_read_alloc_array(storage, &object->label, - &object->labelLen); + idx = len; + len2 = mp_unsigned_bin_size(&key->g); + idx -= len2; + ret = mp_to_unsigned_bin(&key->g, output + idx); + } + if (ret >= 0) { + if (mp_leading_bit(&key->g)) { + output[--idx] = 0x00; + len2++; } - if (ret == 0) { - /* Read issuer of the object. (variable issuerLen) */ - ret = wp11_storage_read_alloc_array(storage, &object->issuer, - &object->issuerLen); - if (ret == 0) { - /* Read serial number of the object. (variable serialLen) */ - ret = wp11_storage_read_alloc_array(storage, - &object->serial, &object->serialLen); + idx -= wp11_SetLength(len2, NULL); + wp11_SetLength(len2, output + idx); + output[--idx] = 0x02; - if (ret == 0) { - /* Read subject of the object. (variable subjectLen) */ - ret = wp11_storage_read_alloc_array(storage, - &object->subject, &object->subjectLen); - } - if (ret == 0) { - /* Read the category of the object. (4) */ - ret = wp11_storage_read_word32(storage, &object->category); - } -#ifdef WOLFPKCS11_NSS - if (ret == 0) { - /* Read email of the object. (variable emailLen) */ - ret = wp11_storage_read_alloc_array(storage, - &object->email, &object->emailLen); - if (ret == BUFFER_E) - ret = 0; - } -#endif - } - else if (ret == BUFFER_E) { - /* Older version of the storage format, doesn't have these, so - * skip reading. - */ - ret = 0; - } + len2 = mp_unsigned_bin_size(&key->p); + idx -= len2; + ret = mp_to_unsigned_bin(&key->p, output + idx); + } + if (ret >= 0) { + if (mp_leading_bit(&key->p)) { + output[--idx] = 0x00; + len2++; } + idx -= wp11_SetLength(len2, NULL); + wp11_SetLength(len2, output + idx); + output[--idx] = 0x02; - wp11_storage_close(storage); + len2 = len - idx; + idx -= wp11_SetLength(len2, NULL); + idx -= 1; + output[idx] = 0x30; + wp11_SetLength(len2, output + idx + 1); + } + if (ret >= 0) { + XMEMMOVE(output, output + idx, len - idx); + *outSz = len - idx; } + return ret; } +#define wc_DhParamsToDer wp11_DhParamsToDer +#endif /* !WOLFSSL_DH_EXTRA || LIBWOLFSSL_VERSION_HEX < 0x04008000 */ + /** - * Load a key object from storage. + * Store an DH key to storage. * - * @param [in, out] object Key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. + * @param [in] object DH key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. * @return 0 on success. * @return MEMORY_E when dynamic memory allocation fails. - * @return BUFFER_E when loading fails. - * @return NOT_AVAILABLE_E when unable to locate data. + * @return BUFFER_E when storing fails. + * @return NOT_AVAILABLE_E when unable to write data. */ -static int wp11_Object_Load(WP11_Object* object, int tokenId, int objId) +static int wp11_Object_Store_DhKey(WP11_Object* object, int tokenId, int objId) { - int ret; + int ret = 0; + void* storage = NULL; + unsigned char* der = NULL; + word32 len = 0; + int storeType; - ret = wp11_Object_Load_Object(object, tokenId, objId); - if (ret == 0) { - /* Now that we know the object class, allocate type-specific data */ - ret = wp11_Object_AllocateTypeData(object); + if (object->keyData == NULL) { + ret = wp11_Object_Encode_DhKey(object); } if (ret == 0) { - if (object->objClass == CKO_CERTIFICATE) { - ret = wp11_Object_Load_Cert(object, tokenId, objId); - } -#ifdef WOLFPKCS11_NSS - else if (object->objClass == CKO_NSS_TRUST) { - ret = wp11_Object_Load_Trust(object, tokenId, objId); + /* Get length of encoded DH parameters. */ + ret = wc_DhParamsToDer(&object->data.dhKey->params, NULL, &len); + if (ret == LENGTH_ONLY_E) { + ret = 0; } -#endif - else if (object->objClass == CKO_DATA) { - ret = wp11_Object_Load_Data(object, tokenId, objId); + } + if (ret == 0) { + /* Allocate buffer to hold encoded key. */ + der = (unsigned char*)XMALLOC(len, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (der == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + /* Encode DH parameters. */ + ret = wc_DhParamsToDer(&object->data.dhKey->params, der, &len); + if (ret >= 0) { + ret = 0; } - else { - /* Load separate key data. */ - switch (object->type) { - #ifndef NO_RSA - case CKK_RSA: - ret = wp11_Object_Load_RsaKey(object, tokenId, objId); - break; - #endif - #ifdef HAVE_ECC - case CKK_EC: - ret = wp11_Object_Load_EccKey(object, tokenId, objId); - break; - #endif - #ifdef WOLFPKCS11_MLDSA - case CKK_ML_DSA: - ret = wp11_Object_Load_MldsaKey(object, tokenId, objId); - break; - #endif - #ifndef NO_DH - case CKK_DH: - ret = wp11_Object_Load_DhKey(object, tokenId, objId); - break; - #endif - #ifdef WOLFPKCS11_MLKEM - case CKK_ML_KEM: - ret = wp11_Object_Load_MlKemKey(object, tokenId, objId); - break; - #endif - #ifndef NO_AES - case CKK_AES: - #endif - case CKK_GENERIC_SECRET: - ret = wp11_Object_Load_SymmKey(object, tokenId, objId); - break; - default: - ret = NOT_AVAILABLE_E; - } + } + + /* Determine store type - private keys may be encrypted. */ + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_DHKEY_PRIV; + else + storeType = WOLFPKCS11_STORE_DHKEY_PUB; + + /* Open access to DH key. */ + if (ret == 0) { + ret = wp11_storage_open(storeType, tokenId, objId, + object->keyDataLen + len, &storage); + } + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + if (ret == 0) { + /* Write encoded DH parameters to storage. */ + ret = wp11_storage_write_array(storage, der, len); } + + wp11_storage_close(storage); + } + if (der != NULL) { + XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); } return ret; } +#endif /* !NO_DH */ -static int wp11_Object_Store_Object(WP11_Object* object, int tokenId, int objId) +#ifdef WOLFPKCS11_MLKEM +static int MlKemKeyTryDecode(MlKemKey* key, int level, byte* data, word32 len, + int devId, CK_OBJECT_CLASS objClass) { int ret; - void* storage = NULL; - word32 dummy = 0; - int variableSz = (object->keyIdLen + object->labelLen + - object->issuerLen + object->serialLen + object->subjectLen -#ifdef WOLFPKCS11_NSS - + object->emailLen -#endif - ); - /* Open access to key object. */ - ret = wp11_storage_open(WOLFPKCS11_STORE_OBJECT, tokenId, objId, variableSz, - &storage); + ret = wc_MlKemKey_Init(key, level, NULL, devId); if (ret == 0) { - /* Write the IV (12) */ - ret = wp11_storage_write_fixed_array(storage, object->iv, - sizeof(object->iv)); - if (ret == 0) { - /* Write handle value. (8) */ - ret = wp11_storage_write_ulong(storage, object->handle); + if (objClass == CKO_PRIVATE_KEY) { + ret = wc_MlKemKey_DecodePrivateKey(key, data, len); } - if (ret == 0) { - /* Write object class. (8) */ - ret = wp11_storage_write_ulong(storage, object->objClass); - } - if (ret == 0) { - /* Write key gen mechanism. (8) */ - ret = wp11_storage_write_ulong(storage, object->keyGenMech); + else { + ret = wc_MlKemKey_DecodePublicKey(key, data, len); } - if (ret == 0) { - /* Write whether the object is on a token. (1) */ - ret = wp11_storage_write_boolean(storage, object->onToken); + } + + if (ret != 0) { + wc_MlKemKey_Free(key); + } + + return ret; +} + +/** + * Decode the ML-KEM key. + * + * Encoded private keys are encrypted. + * + * @param [in, out] object ML-KEM key object. + * @return 0 on success. + * @return -ve on failure. + */ +static int wp11_Object_Decode_MlKemKey(WP11_Object* object) +{ + int ret = 0; + + if (object->objClass == CKO_PRIVATE_KEY) { + unsigned char* der; + int len; + + if (object->keyDataLen < AES_BLOCK_SIZE) + return BAD_FUNC_ARG; + len = object->keyDataLen - AES_BLOCK_SIZE; + + der = (unsigned char*)XMALLOC(len, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (der == NULL) { + ret = MEMORY_E; } if (ret == 0) { - /* Write whether the object is local. (1) */ - ret = wp11_storage_write_boolean(storage, object->local); + ret = wp11_DecryptData(der, object->keyData, len, + object->slot->token.key, + sizeof(object->slot->token.key), object->iv, + sizeof(object->iv), object->devId); } if (ret == 0) { - /* Unused word32. (4) */ - ret = wp11_storage_write_word32(storage, dummy); + ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_512, + der, len, object->devId, object->objClass); + if (ret != 0) { + ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_768, + der, len, object->devId, + object->objClass); + } + if (ret != 0) { + ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_1024, + der, len, object->devId, + object->objClass); + } } - if (ret == 0) { - /* Write the operational flags of the object. (4) */ - ret = wp11_storage_write_word32(storage, object->opFlag); + if (der != NULL) { + wc_ForceZero(der, len); + XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); } - if (ret == 0) { - /* Write the start date. (8) */ - ret = wp11_storage_write_fixed_array(storage, - (unsigned char*)object->startDate, sizeof(object->startDate)); + } + else { + ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_512, + object->keyData, object->keyDataLen, + object->devId, object->objClass); + if (ret != 0) { + ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_768, + object->keyData, object->keyDataLen, + object->devId, object->objClass); } - if (ret == 0) { - /* Write the end date. (8) */ - ret = wp11_storage_write_fixed_array(storage, - (unsigned char*)object->endDate, sizeof(object->endDate)); + if (ret != 0) { + ret = MlKemKeyTryDecode(object->data.mlKemKey, WC_ML_KEM_1024, + object->keyData, object->keyDataLen, + object->devId, object->objClass); } + } + object->encoded = (ret != 0); + return ret; +} + +/** + * Encode the ML-KEM key. + * + * Private keys are encoded and then encrypted. + * + * @param [in, out] object ML-KEM key object. + * @return 0 on success. + * @return -ve on failure. + */ +static int wp11_Object_Encode_MlKemKey(WP11_Object* object) +{ + int ret; + word32 keyLen = 0; + + if (object->objClass == CKO_PRIVATE_KEY) { + ret = wc_MlKemKey_PrivateKeySize(object->data.mlKemKey, &keyLen); if (ret == 0) { - /* Write id of the object. (variable keyIdLen) */ - ret = wp11_storage_write_array(storage, object->keyId, - object->keyIdLen); - } - if (ret == 0) { - /* Write label of the object. (variable labelLen) */ - ret = wp11_storage_write_array(storage, object->label, - object->labelLen); - } - if (ret == 0) { - /* Write issuer of the object. (variable issuerLen) */ - ret = wp11_storage_write_array(storage, object->issuer, - object->issuerLen); - } - if (ret == 0) { - /* Write serial of the object. (variable serialLen) */ - ret = wp11_storage_write_array(storage, object->serial, - object->serialLen); - } - if (ret == 0) { - /* Write subject of the object. (variable subjectLen) */ - ret = wp11_storage_write_array(storage, object->subject, - object->subjectLen); + object->keyDataLen = keyLen + AES_BLOCK_SIZE; } + } + else { + ret = wc_MlKemKey_PublicKeySize(object->data.mlKemKey, &keyLen); if (ret == 0) { - /* Write the category of the object. (4) */ - ret = wp11_storage_write_word32(storage, object->category); + object->keyDataLen = keyLen; } -#ifdef WOLFPKCS11_NSS + } + + if (ret == 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(object->keyDataLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + + if (ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + ret = wc_MlKemKey_EncodePrivateKey(object->data.mlKemKey, + object->keyData, keyLen); if (ret == 0) { - /* Write email of the object. (variable emailLen) */ - ret = wp11_storage_write_array(storage, object->email, - object->emailLen); + ret = wp11_EncryptData(object->keyData, object->keyData, keyLen, + object->slot->token.key, + sizeof(object->slot->token.key), object->iv, + sizeof(object->iv), object->devId); } -#endif + } + else if (ret == 0 && object->objClass == CKO_PUBLIC_KEY) { + ret = wc_MlKemKey_EncodePublicKey(object->data.mlKemKey, + object->keyData, keyLen); + } + + if (ret != 0) { + if (object->keyData != NULL) + wc_ForceZero(object->keyData, object->keyDataLen); + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + + return ret; +} + +/** + * Load a ML-KEM key from storage. + * + * @param [in, out] object ML-KEM key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + * @return 0 on success. + * @return MEMORY_E when dynamic memory allocation fails. + * @return BUFFER_E when loading fails. + * @return NOT_AVAILABLE_E when unable to locate data. + */ +static int wp11_Object_Load_MlKemKey(WP11_Object* object, int tokenId, + int objId) +{ + int ret; + void* storage = NULL; + int storeType; + + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_MLKEMKEY_PRIV; + else + storeType = WOLFPKCS11_STORE_MLKEMKEY_PUB; + ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); + if (ret == 0) { + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); wp11_storage_close(storage); } + return ret; } /** - * Store a key object to storage. + * Store a ML-KEM key to storage. * - * @param [in] object Key object. + * @param [in] object ML-KEM key object. * @param [in] tokenId Id of token this key belongs to. * @param [in] objId Id of object for token. * @return 0 on success. @@ -5530,255 +6512,729 @@ static int wp11_Object_Store_Object(WP11_Object* object, int tokenId, int objId) * @return BUFFER_E when storing fails. * @return NOT_AVAILABLE_E when unable to write data. */ -static int wp11_Object_Store(WP11_Object* object, int tokenId, int objId) +static int wp11_Object_Store_MlKemKey(WP11_Object* object, int tokenId, + int objId) { - int ret; - - /* Open access to key object. */ - ret = wp11_Object_Store_Object(object, tokenId, objId); + int ret = 0; + void* storage = NULL; + int storeType; - if (ret == 0 && object->keyData == NULL && - (object->objClass == CKO_PRIVATE_KEY || - object->type == CKK_AES || - object->type == CKK_GENERIC_SECRET)) { - /* Generate new IV if needed */ - ret = wc_RNG_GenerateBlock(&object->slot->token.rng, object->iv, - sizeof(object->iv)); + if (object->keyData == NULL) { + ret = wp11_Object_Encode_MlKemKey(object); + if (ret != 0) + return ret; } + + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_MLKEMKEY_PRIV; + else + storeType = WOLFPKCS11_STORE_MLKEMKEY_PUB; + + ret = wp11_storage_open(storeType, tokenId, objId, object->keyDataLen, + &storage); if (ret == 0) { - if (object->objClass == CKO_CERTIFICATE) { - ret = wp11_Object_Store_Cert(object, tokenId, objId); - } -#ifdef WOLFPKCS11_NSS - else if (object->objClass == CKO_NSS_TRUST) { - ret = wp11_Object_Store_Trust(object, tokenId, objId); - } -#endif - else if (object->objClass == CKO_DATA) { - ret = wp11_Object_Store_Data(object, tokenId, objId); - } - else { - /* Store key data separately. */ - switch (object->type) { - #ifndef NO_RSA - case CKK_RSA: - ret = wp11_Object_Store_RsaKey(object, tokenId, objId); - break; - #endif - #ifdef HAVE_ECC - case CKK_EC: - ret = wp11_Object_Store_EccKey(object, tokenId, objId); - break; - #endif - #ifdef WOLFPKCS11_MLDSA - case CKK_ML_DSA: - ret = wp11_Object_Store_MldsaKey(object, tokenId, objId); - break; - #endif - #ifndef NO_DH - case CKK_DH: - ret = wp11_Object_Store_DhKey(object, tokenId, objId); - break; - #endif - #ifdef WOLFPKCS11_MLKEM - case CKK_ML_KEM: - ret = wp11_Object_Store_MlKemKey(object, tokenId, objId); - break; - #endif - #ifndef NO_AES - case CKK_AES: - #endif - case CKK_GENERIC_SECRET: - ret = wp11_Object_Store_SymmKey(object, tokenId, objId); - break; - default: - ret = NOT_AVAILABLE_E; - } - } + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + wp11_storage_close(storage); } return ret; } +#endif /* WOLFPKCS11_MLKEM */ /** - * Decode the key object. Private keys require decryption. - * - * When decryption authentication fails then the wrong user is trying to access - * the private key. + * Decode the symmetric key - requires decryption. * - * @param [in, out] object Key object. + * @param [in, out] object Symmetric key object. * @return 0 on success. * @return -ve on failure. */ -static int wp11_Object_Decode(WP11_Object* object) +static int wp11_Object_Decode_SymmKey(WP11_Object* object) { - int ret; + int ret = 0; - if (object->objClass == CKO_CERTIFICATE) { - wp11_Object_Decode_Cert(object); - ret = 0; - } -#ifdef WOLFPKCS11_NSS - else if (object->objClass == CKO_NSS_TRUST) { - wp11_Object_Decode_Trust(object); - ret = 0; - } -#endif - else if (object->objClass == CKO_DATA) { - wp11_Object_Decode_Data(object); - ret = 0; - } - else { - switch (object->type) { - #ifndef NO_RSA - case CKK_RSA: - ret = wp11_Object_Decode_RsaKey(object); - break; - #endif - #ifdef HAVE_ECC - case CKK_EC: - ret = wp11_Object_Decode_EccKey(object); - break; - #endif - #ifdef WOLFPKCS11_MLDSA - case CKK_ML_DSA: - ret = wp11_Object_Decode_MldsaKey(object); - break; - #endif - #ifndef NO_DH - case CKK_DH: - ret = wp11_Object_Decode_DhKey(object); - break; - #endif - #ifdef WOLFPKCS11_MLKEM - case CKK_ML_KEM: - ret = wp11_Object_Decode_MlKemKey(object); - break; - #endif - #ifndef NO_AES - case CKK_AES: - #endif - case CKK_GENERIC_SECRET: - ret = wp11_Object_Decode_SymmKey(object); - break; - default: - ret = NOT_AVAILABLE_E; - } + if (object->keyDataLen - AES_BLOCK_SIZE > WP11_MAX_SYM_KEY_SZ) + ret = BUFFER_E; + if (ret == 0) { + ret = wp11_DecryptData(object->data.symmKey->data, object->keyData, + object->keyDataLen - AES_BLOCK_SIZE, + object->slot->token.key, + sizeof(object->slot->token.key), object->iv, + sizeof(object->iv), object->devId); } - - /* Authentication failure means this object isn't for this user. */ - if (ret == AES_GCM_AUTH_E) - ret = 0; + if (ret == 0) + object->data.symmKey->len = object->keyDataLen - AES_BLOCK_SIZE; + object->encoded = (ret != 0); return ret; } /** - * Encode the key object. Private keys require encryption. + * Encode the symmetric key - requires encryption. * - * @param [in, out] object Key object. - * @param [in] protect Unencrypted private key data is cleared. + * @param [in, out] object Symmetric key object. * @return 0 on success. * @return -ve on failure. */ -static int wp11_Object_Encode(WP11_Object* object, int protect) +static int wp11_Object_Encode_SymmKey(WP11_Object* object) { - int ret; + int ret = 0; -#ifdef WOLFPKCS11_NSS - if ((object->objClass == CKO_CERTIFICATE) || - (object->objClass == CKO_NSS_TRUST)) -#else - if (object->objClass == CKO_CERTIFICATE) -#endif - ret = 0; - else if (object->objClass == CKO_DATA) { - ret = 0; - } - else { - switch (object->type) { - #ifndef NO_RSA - case CKK_RSA: - ret = wp11_Object_Encode_RsaKey(object); - if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { - wc_FreeRsaKey(object->data.rsaKey); - object->encoded = 1; - } - break; - #endif - #ifdef HAVE_ECC - case CKK_EC: - ret = wp11_Object_Encode_EccKey(object); - if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { - wc_ecc_free(object->data.ecKey); - object->encoded = 1; - } - break; - #endif - #ifdef WOLFPKCS11_MLDSA - case CKK_ML_DSA: - ret = wp11_Object_Encode_MldsaKey(object); - if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { - wc_MlDsaKey_Free(object->data.mldsaKey); - object->encoded = 1; - } - break; - #endif - #ifndef NO_DH - case CKK_DH: - ret = wp11_Object_Encode_DhKey(object); - if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { - wc_ForceZero(object->data.dhKey->key, object->data.dhKey->len); - object->encoded = 1; - } - break; - #endif - #ifdef WOLFPKCS11_MLKEM - case CKK_ML_KEM: - ret = wp11_Object_Encode_MlKemKey(object); - if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { - wc_MlKemKey_Free(object->data.mlKemKey); - object->encoded = 1; - } - break; - #endif - #ifndef NO_AES - case CKK_AES: - #endif - case CKK_GENERIC_SECRET: - ret = wp11_Object_Encode_SymmKey(object); - if (protect && ret == 0) { - wc_ForceZero(object->data.symmKey->data, object->data.symmKey->len); - object->encoded = 1; - } - break; - default: - ret = NOT_AVAILABLE_E; - } + object->keyDataLen = object->data.symmKey->len + AES_BLOCK_SIZE; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + /* Allocate buffer to hold encoded key. */ + object->keyData = (unsigned char*)XMALLOC(object->keyDataLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + + if (ret == 0) { + ret = wp11_EncryptData(object->keyData, object->data.symmKey->data, + object->data.symmKey->len, + object->slot->token.key, + sizeof(object->slot->token.key), object->iv, + sizeof(object->iv), object->devId); + if (ret == 0) + object->keyDataLen = object->data.symmKey->len + AES_BLOCK_SIZE; } return ret; } /** - * Unstore a key object to storage. + * Load an symmetric key from storage. * - * Empties the contents of the object. + * @param [in, out] object Symmetric key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + * @return 0 on success. + * @return BUFFER_E when loading fails. + * @return NOT_AVAILABLE_E when unable to locate data. + */ +static int wp11_Object_Load_SymmKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + + /* Open access to symmetric key. */ + ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_SYMMKEY, tokenId, objId, + &storage); + if (ret == 0) { + /* Read symmetric key from storage. */ + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); + wp11_storage_close(storage); + } + + return ret; +} + +/** + * Store a symmetric key to storage. * - * @param [in] object Key object. + * @param [in] object Symmetric key object. * @param [in] tokenId Id of token this key belongs to. * @param [in] objId Id of object for token. + * @return 0 on success. + * @return BUFFER_E when storing fails. + * @return NOT_AVAILABLE_E when unable to write data. */ -static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) +static int wp11_Object_Store_SymmKey(WP11_Object* object, int tokenId, + int objId) { - int ret; - int storeObjType = -1; + int ret = 0; + void* storage = NULL; - if (objId < 0) { - return BAD_FUNC_ARG; + if (object->keyData == NULL) { + ret = wp11_Object_Encode_SymmKey(object); } - /* Remove store object */ + /* Open access to symmetric key. */ + if (ret == 0) { + ret = wp11_storage_open(WOLFPKCS11_STORE_SYMMKEY, tokenId, objId, + object->keyDataLen, &storage); + } + if (ret == 0) { + /* Write symmetric key to storage. */ + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + + wp11_storage_close(storage); + } + + return ret; +} + +static int wp11_Object_Load_Object(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + word32 dummy = 0; + + /* Open access to key object. */ + ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_OBJECT, tokenId, objId, + &storage); + if (ret == 0) { + /* Read the IV. (12) */ + ret = wp11_storage_read_fixed_array(storage, object->iv, + sizeof(object->iv)); + if (ret == 0) { + /* Read handle value. (8) */ + ret = wp11_storage_read_ulong(storage, &object->handle); + } + if (ret == 0) { + /* Read object class. (8) */ + ret = wp11_storage_read_ulong(storage, &object->objClass); + } + if (ret == 0) { + /* Read key gen mechanism. (8) */ + ret = wp11_storage_read_ulong(storage, &object->keyGenMech); + } + if (ret == 0) { + /* Read whether the object is on a token. (1) */ + byte onToken = 0; + ret = wp11_storage_read_boolean(storage, &onToken); + if (ret == 0) { + object->onToken = (onToken != 0); + } + } + if (ret == 0) { + /* Read whether the object is local. (1) */ + byte local = 0; + ret = wp11_storage_read_boolean(storage, &local); + if (ret == 0) { + object->local = (local != 0); + } + } + if (ret == 0) { + /* Unused word32. (4) */ + ret = wp11_storage_read_word32(storage, &dummy); + } + if (ret == 0) { + /* Read the operational flags of the object. (4) */ + ret = wp11_storage_read_word32(storage, &object->opFlag); + } + if (ret == 0) { + /* Read the start date. (8) */ + ret = wp11_storage_read_fixed_array(storage, + (unsigned char*)object->startDate, sizeof(object->startDate)); + } + if (ret == 0) { + /* Read the end date. (8) */ + ret = wp11_storage_read_fixed_array(storage, + (unsigned char*)object->endDate, sizeof(object->endDate)); + } + + if (ret == 0) { + /* Read id for the object. (variable keyIdLen) */ + ret = wp11_storage_read_alloc_array(storage, &object->keyId, + &object->keyIdLen); + } + if (ret == 0) { + /* Read label for the object. (variable labelLen) */ + ret = wp11_storage_read_alloc_array(storage, &object->label, + &object->labelLen); + } + if (ret == 0) { + /* Read issuer of the object. (variable issuerLen) */ + ret = wp11_storage_read_alloc_array(storage, &object->issuer, + &object->issuerLen); + if (ret == 0) { + /* Read serial number of the object. (variable serialLen) */ + ret = wp11_storage_read_alloc_array(storage, + &object->serial, &object->serialLen); + + if (ret == 0) { + /* Read subject of the object. (variable subjectLen) */ + ret = wp11_storage_read_alloc_array(storage, + &object->subject, &object->subjectLen); + } + if (ret == 0) { + /* Read the category of the object. (4) */ + ret = wp11_storage_read_word32(storage, &object->category); + } +#ifdef WOLFPKCS11_NSS + if (ret == 0) { + /* Read email of the object. (variable emailLen) */ + ret = wp11_storage_read_alloc_array(storage, + &object->email, &object->emailLen); + if (ret == BUFFER_E) + ret = 0; + } +#endif + } + else if (ret == BUFFER_E) { + /* Older version of the storage format, doesn't have these, so + * skip reading. + */ + ret = 0; + } + } + + wp11_storage_close(storage); + } + return ret; +} + +/** + * Load a key object from storage. + * + * @param [in, out] object Key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + * @return 0 on success. + * @return MEMORY_E when dynamic memory allocation fails. + * @return BUFFER_E when loading fails. + * @return NOT_AVAILABLE_E when unable to locate data. + */ +static int wp11_Object_Load(WP11_Object* object, int tokenId, int objId) +{ + int ret; + + ret = wp11_Object_Load_Object(object, tokenId, objId); + if (ret == 0) { + /* Now that we know the object class, allocate type-specific data */ + ret = wp11_Object_AllocateTypeData(object); + } + if (ret == 0) { + if (object->objClass == CKO_CERTIFICATE) { + ret = wp11_Object_Load_Cert(object, tokenId, objId); + } +#ifdef WOLFPKCS11_NSS + else if (object->objClass == CKO_NSS_TRUST) { + ret = wp11_Object_Load_Trust(object, tokenId, objId); + } +#endif + else if (object->objClass == CKO_DATA) { + ret = wp11_Object_Load_Data(object, tokenId, objId); + } + else { + /* Load separate key data. */ + switch (object->type) { + #ifndef NO_RSA + case CKK_RSA: + ret = wp11_Object_Load_RsaKey(object, tokenId, objId); + break; + #endif + #ifdef HAVE_ECC + case CKK_EC: + ret = wp11_Object_Load_EccKey(object, tokenId, objId); + break; + #endif + #ifdef WOLFPKCS11_MLDSA + case CKK_ML_DSA: + 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); + break; + #endif + #ifdef WOLFPKCS11_MLKEM + case CKK_ML_KEM: + ret = wp11_Object_Load_MlKemKey(object, tokenId, objId); + break; + #endif + #ifndef NO_AES + case CKK_AES: + #endif + case CKK_GENERIC_SECRET: + ret = wp11_Object_Load_SymmKey(object, tokenId, objId); + break; + default: + ret = NOT_AVAILABLE_E; + } + } + } + + return ret; +} + +static int wp11_Object_Store_Object(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + word32 dummy = 0; + int variableSz = (object->keyIdLen + object->labelLen + + object->issuerLen + object->serialLen + object->subjectLen +#ifdef WOLFPKCS11_NSS + + object->emailLen +#endif + ); + + /* Open access to key object. */ + ret = wp11_storage_open(WOLFPKCS11_STORE_OBJECT, tokenId, objId, variableSz, + &storage); + if (ret == 0) { + /* Write the IV (12) */ + ret = wp11_storage_write_fixed_array(storage, object->iv, + sizeof(object->iv)); + if (ret == 0) { + /* Write handle value. (8) */ + ret = wp11_storage_write_ulong(storage, object->handle); + } + if (ret == 0) { + /* Write object class. (8) */ + ret = wp11_storage_write_ulong(storage, object->objClass); + } + if (ret == 0) { + /* Write key gen mechanism. (8) */ + ret = wp11_storage_write_ulong(storage, object->keyGenMech); + } + if (ret == 0) { + /* Write whether the object is on a token. (1) */ + ret = wp11_storage_write_boolean(storage, object->onToken); + } + if (ret == 0) { + /* Write whether the object is local. (1) */ + ret = wp11_storage_write_boolean(storage, object->local); + } + if (ret == 0) { + /* Unused word32. (4) */ + ret = wp11_storage_write_word32(storage, dummy); + } + if (ret == 0) { + /* Write the operational flags of the object. (4) */ + ret = wp11_storage_write_word32(storage, object->opFlag); + } + if (ret == 0) { + /* Write the start date. (8) */ + ret = wp11_storage_write_fixed_array(storage, + (unsigned char*)object->startDate, sizeof(object->startDate)); + } + if (ret == 0) { + /* Write the end date. (8) */ + ret = wp11_storage_write_fixed_array(storage, + (unsigned char*)object->endDate, sizeof(object->endDate)); + } + + if (ret == 0) { + /* Write id of the object. (variable keyIdLen) */ + ret = wp11_storage_write_array(storage, object->keyId, + object->keyIdLen); + } + if (ret == 0) { + /* Write label of the object. (variable labelLen) */ + ret = wp11_storage_write_array(storage, object->label, + object->labelLen); + } + if (ret == 0) { + /* Write issuer of the object. (variable issuerLen) */ + ret = wp11_storage_write_array(storage, object->issuer, + object->issuerLen); + } + if (ret == 0) { + /* Write serial of the object. (variable serialLen) */ + ret = wp11_storage_write_array(storage, object->serial, + object->serialLen); + } + if (ret == 0) { + /* Write subject of the object. (variable subjectLen) */ + ret = wp11_storage_write_array(storage, object->subject, + object->subjectLen); + } + if (ret == 0) { + /* Write the category of the object. (4) */ + ret = wp11_storage_write_word32(storage, object->category); + } +#ifdef WOLFPKCS11_NSS + if (ret == 0) { + /* Write email of the object. (variable emailLen) */ + ret = wp11_storage_write_array(storage, object->email, + object->emailLen); + } +#endif + + wp11_storage_close(storage); + } + return ret; +} + +/** + * Store a key object to storage. + * + * @param [in] object Key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + * @return 0 on success. + * @return MEMORY_E when dynamic memory allocation fails. + * @return BUFFER_E when storing fails. + * @return NOT_AVAILABLE_E when unable to write data. + */ +static int wp11_Object_Store(WP11_Object* object, int tokenId, int objId) +{ + int ret; + + /* Open access to key object. */ + ret = wp11_Object_Store_Object(object, tokenId, objId); + + if (ret == 0 && object->keyData == NULL && + (object->objClass == CKO_PRIVATE_KEY || + object->type == CKK_AES || + object->type == CKK_GENERIC_SECRET)) { + /* Generate new IV if needed */ + ret = wc_RNG_GenerateBlock(&object->slot->token.rng, object->iv, + sizeof(object->iv)); + } + if (ret == 0) { + if (object->objClass == CKO_CERTIFICATE) { + ret = wp11_Object_Store_Cert(object, tokenId, objId); + } +#ifdef WOLFPKCS11_NSS + else if (object->objClass == CKO_NSS_TRUST) { + ret = wp11_Object_Store_Trust(object, tokenId, objId); + } +#endif + else if (object->objClass == CKO_DATA) { + ret = wp11_Object_Store_Data(object, tokenId, objId); + } + else { + /* Store key data separately. */ + switch (object->type) { + #ifndef NO_RSA + case CKK_RSA: + ret = wp11_Object_Store_RsaKey(object, tokenId, objId); + break; + #endif + #ifdef HAVE_ECC + case CKK_EC: + ret = wp11_Object_Store_EccKey(object, tokenId, objId); + break; + #endif + #ifdef WOLFPKCS11_MLDSA + case CKK_ML_DSA: + 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); + break; + #endif + #ifdef WOLFPKCS11_MLKEM + case CKK_ML_KEM: + ret = wp11_Object_Store_MlKemKey(object, tokenId, objId); + break; + #endif + #ifndef NO_AES + case CKK_AES: + #endif + case CKK_GENERIC_SECRET: + ret = wp11_Object_Store_SymmKey(object, tokenId, objId); + break; + default: + ret = NOT_AVAILABLE_E; + } + } + } + + return ret; +} + +/** + * Decode the key object. Private keys require decryption. + * + * When decryption authentication fails then the wrong user is trying to access + * the private key. + * + * @param [in, out] object Key object. + * @return 0 on success. + * @return -ve on failure. + */ +static int wp11_Object_Decode(WP11_Object* object) +{ + int ret; + + if (object->objClass == CKO_CERTIFICATE) { + wp11_Object_Decode_Cert(object); + ret = 0; + } +#ifdef WOLFPKCS11_NSS + else if (object->objClass == CKO_NSS_TRUST) { + wp11_Object_Decode_Trust(object); + ret = 0; + } +#endif + else if (object->objClass == CKO_DATA) { + wp11_Object_Decode_Data(object); + ret = 0; + } + else { + switch (object->type) { + #ifndef NO_RSA + case CKK_RSA: + ret = wp11_Object_Decode_RsaKey(object); + break; + #endif + #ifdef HAVE_ECC + case CKK_EC: + ret = wp11_Object_Decode_EccKey(object); + break; + #endif + #ifdef WOLFPKCS11_MLDSA + case CKK_ML_DSA: + 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); + break; + #endif + #ifdef WOLFPKCS11_MLKEM + case CKK_ML_KEM: + ret = wp11_Object_Decode_MlKemKey(object); + break; + #endif + #ifndef NO_AES + case CKK_AES: + #endif + case CKK_GENERIC_SECRET: + ret = wp11_Object_Decode_SymmKey(object); + break; + default: + ret = NOT_AVAILABLE_E; + } + } + + /* Authentication failure means this object isn't for this user. */ + if (ret == AES_GCM_AUTH_E) + ret = 0; + + return ret; +} + +/** + * Encode the key object. Private keys require encryption. + * + * @param [in, out] object Key object. + * @param [in] protect Unencrypted private key data is cleared. + * @return 0 on success. + * @return -ve on failure. + */ +static int wp11_Object_Encode(WP11_Object* object, int protect) +{ + int ret; + +#ifdef WOLFPKCS11_NSS + if ((object->objClass == CKO_CERTIFICATE) || + (object->objClass == CKO_NSS_TRUST)) +#else + if (object->objClass == CKO_CERTIFICATE) +#endif + ret = 0; + else if (object->objClass == CKO_DATA) { + ret = 0; + } + else { + switch (object->type) { + #ifndef NO_RSA + case CKK_RSA: + ret = wp11_Object_Encode_RsaKey(object); + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + wc_FreeRsaKey(object->data.rsaKey); + object->encoded = 1; + } + break; + #endif + #ifdef HAVE_ECC + case CKK_EC: + ret = wp11_Object_Encode_EccKey(object); + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + wc_ecc_free(object->data.ecKey); + object->encoded = 1; + } + break; + #endif + #ifdef WOLFPKCS11_MLDSA + case CKK_ML_DSA: + ret = wp11_Object_Encode_MldsaKey(object); + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + wc_MlDsaKey_Free(object->data.mldsaKey); + object->encoded = 1; + } + 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); + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + wc_ForceZero(object->data.dhKey->key, object->data.dhKey->len); + object->encoded = 1; + } + break; + #endif + #ifdef WOLFPKCS11_MLKEM + case CKK_ML_KEM: + ret = wp11_Object_Encode_MlKemKey(object); + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + wc_MlKemKey_Free(object->data.mlKemKey); + object->encoded = 1; + } + break; + #endif + #ifndef NO_AES + case CKK_AES: + #endif + case CKK_GENERIC_SECRET: + ret = wp11_Object_Encode_SymmKey(object); + if (protect && ret == 0) { + wc_ForceZero(object->data.symmKey->data, object->data.symmKey->len); + object->encoded = 1; + } + break; + default: + ret = NOT_AVAILABLE_E; + } + } + + return ret; +} + +/** + * Unstore a key object to storage. + * + * Empties the contents of the object. + * + * @param [in] object Key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + */ +static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) +{ + int ret; + int storeObjType = -1; + + if (objId < 0) { + return BAD_FUNC_ARG; + } + + /* Remove store object */ ret = wp11_storage_remove(WOLFPKCS11_STORE_OBJECT, tokenId, objId); if (ret != 0) { return ret; @@ -5823,6 +7279,41 @@ 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. + * 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 + * value, falling back to a shell read for objects that have + * not been decoded yet. */ + storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV_SHELL; +#ifdef WOLFPKCS11_LMS_PRIVATE + { + word64 stateId = 0; + if (object->statefulStateId != 0) { + stateId = object->statefulStateId; + } + else { + (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, + &stateId); + } + if (stateId != 0) { + /* Use the word64-safe helper — CK_ULONG would + * truncate the nonce on LLP64 platforms. */ + (void)wp11_Stateful_StateFile_Remove("hsskey", + tokenId, stateId); + } + } +#endif + } + else { + storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; + } + break; + #endif #ifndef NO_DH case CKK_DH: if (object->objClass == CKO_PRIVATE_KEY) @@ -6552,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) { @@ -7605,6 +9103,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 +9116,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: @@ -8425,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 } @@ -8764,6 +10280,21 @@ 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 + /* 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->statefulStateId != 0 && + 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) XFREE(object->issuer, NULL, DYNAMIC_TYPE_CERT); if (object->serial != NULL) @@ -8805,6 +10336,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); @@ -8828,487 +10368,877 @@ void WP11_Object_Free(WP11_Object* object) } } -#ifndef WOLFPKCS11_NO_STORE - if (object->keyData != NULL && certFreed == 0) - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); -#else - (void)certFreed; -#endif - - /* Dispose of object. */ - XFREE(object, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#ifndef WOLFPKCS11_NO_STORE + if (object->keyData != NULL && certFreed == 0) + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#else + (void)certFreed; +#endif + + /* Dispose of object. */ + XFREE(object, NULL, DYNAMIC_TYPE_TMP_BUFFER); +} + +/** + * Get the object's handle. + * + * @param object [in] Object object. + * @return Object's handle. + */ +CK_OBJECT_HANDLE WP11_Object_GetHandle(WP11_Object* object) +{ + return object->handle; +} + +/** + * Check whether the object is stored on the token. + * + * @param object [in] Object object. + * @return 1 when object is on token. + * 0 when object is a session object. + */ +int WP11_Object_OnToken(WP11_Object* object) +{ + return object->onToken; +} + +/** + * Get the object's type - for example the key type. + * + * @param object [in] Object object. + * @return Object's type. + */ +CK_KEY_TYPE WP11_Object_GetType(WP11_Object* object) +{ + return object->type; +} + +/** + * Get the object's devId. + * + * @param object [in] Object object. + * @return Object's devId. + */ +CK_ULONG WP11_Object_GetDevId(WP11_Object* object) +{ + return (CK_ULONG)object->devId; +} + +/** + * Get the object's class. + * + * @param object [in] Object object. + * @return Object's class. + */ +CK_OBJECT_CLASS WP11_Object_GetClass(WP11_Object* object) +{ + return object->objClass; +} + +#if !defined(NO_RSA) || defined(HAVE_ECC) +/** + * Set the multi-precision integer from the data. + * + * @param mpi [in] Multi-precision integer. + * @param data [in] Big-endian encoding of number. + * @param len [in] Length in bytes. + * @return -ve on failure. + * 0 on success. + */ +static int SetMPI(mp_int* mpi, unsigned char* data, int len) +{ + int ret = 0; + + if (data != NULL) { + ret = mp_init(mpi); + if (ret == 0) + ret = mp_read_unsigned_bin(mpi, data, len); + } + + return ret; +} +#endif + +#ifndef NO_RSA +/** + * Set the RSA key data into the object. + * Store the data in the wolfCrypt data structure. + * + * @param object [in] Object object. + * @param data [in] Array of byte arrays. + * @param len [in] Array of lengths of byte arrays. + * @return -ve on failure. + * 0 on success. + */ +int WP11_Object_SetRsaKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) +{ + int ret; + RsaKey* key; + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + key = object->data.rsaKey; + ret = wc_InitRsaKey_ex(key, NULL, object->devId); + if (ret == 0) { + ret = SetMPI(&key->d, data[1], (int)len[1]); + if (ret == 0) + ret = SetMPI(&key->p, data[2], (int)len[2]); + if (ret == 0) + ret = SetMPI(&key->q, data[3], (int)len[3]); + /* If modulus is not provided, calculate it */ + if (ret == 0) { + if (data[0] == NULL || len[0] == 0) { + ret = mp_mul(&key->p, &key->q, &key->n); + } else { + ret = SetMPI(&key->n, data[0], (int)len[0]); + } + } + if (ret == 0) + ret = SetMPI(&key->dP, data[4], (int)len[4]); + if (ret == 0) + ret = SetMPI(&key->dQ, data[5], (int)len[5]); + if (ret == 0) + ret = SetMPI(&key->u, data[6], (int)len[6]); + if (ret == 0) { + /* Public exponent defaults to 65537 in PKCS11 > 2.11 */ + if (len[7] > 0) + ret = SetMPI(&key->e, data[7], (int)len[7]); + else { + byte defaultPublic[] = {0x01, 0x00, 0x01}; + ret = SetMPI(&key->e, defaultPublic, sizeof(defaultPublic)); + } + } + if (ret == 0) { + if (len[8] == sizeof(CK_ULONG)) + object->size = (word32)*(CK_ULONG*)data[8]; + else if (len[8] != 0) + ret = BUFFER_E; + } + if (ret == 0) { + if (mp_iszero(&key->d) && mp_iszero(&key->p)) { + key->type = RSA_PUBLIC; + } + else { + key->type = RSA_PRIVATE; + } + } + + if (ret != 0) { + wc_FreeRsaKey(key); + } + } + +#ifdef WOLFPKCS11_TPM + if (ret == 0) { + ret = WP11_Object_WrapTpmKey(object); + } +#endif + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + + return ret; +} +#endif + +#ifdef HAVE_ECC +#ifdef USE_LOCAL_CURVE_OID_LOOKUP +static int ecc_lookup_curve(const byte* oid, word32 len) +{ + const WP11_Ecc_Curve* curve; + + for (curve = DefinedCurves; curve->curve_id < ECC_CURVE_MAX; curve++) + { + if (len == curve->curve_size && + XMEMCMP(oid, curve->curve_oid, len) == 0) { + return curve->curve_id; + } + } + return ECC_CURVE_INVALID; +} +#endif + +/** + * Set the EC Parameters based on the DER encoding of the OID. + * + * @param key [in] EC Key object. + * @param der [in] DER encoding of OID. + * @param len [in] Length of DER encoding. + * @return BUFFER_E when len is too short. + * ASN_PARSE_E when DER encoding is bad. + * BAD_FUNC_ARG when OID is not known. + * Other -ve on failure. + * 0 on success. + */ +static int EcSetParams(ecc_key* key, byte* der, int len) +{ + int ret = 0; + int keySize; + int curveId; + + if (len < 2) + ret = BUFFER_E; + if (ret == 0 && der[0] != ASN_OBJECT_ID) + ret = ASN_PARSE_E; + if (ret == 0 && der[1] != len - 2) + ret = BUFFER_E; + if (ret == 0) { +#ifdef USE_LOCAL_CURVE_OID_LOOKUP + /* Find the curve matching the OID. */ + /* wc_ecc_get_curve_id_from_oid() is broken in FIPSv5 and ecc_sets is + * not accessible in FIPS, so we have our own lookup. + */ + curveId = ecc_lookup_curve(der +2, der[1]); +#else + curveId = wc_ecc_get_curve_id_from_oid(der + 2, der[1]); +#endif + if (curveId == ECC_CURVE_INVALID) + ret = BAD_FUNC_ARG; + } + if (ret == 0) { + /* Set the curve into the EC key. */ + keySize = wc_ecc_get_curve_size_from_id(curveId); + ret = wc_ecc_set_curve(key, keySize, curveId); + } + + return ret; } /** - * Get the object's handle. + * Set the EC Point, encoded in DER and X9.63, as the public key. * - * @param object [in] Object object. - * @return Object's handle. + * @param key [in] EC Key object. + * @param der [in] DER encoding of OID. + * @param len [in] Length of DER encoding. + * @return BUFFER_E when len is too short. + * ASN_PARSE_E when DER encoding is bad. + * Other -ve on failure. + * 0 on success. */ -CK_OBJECT_HANDLE WP11_Object_GetHandle(WP11_Object* object) +static int EcSetPoint(ecc_key* key, byte* der, int len) { - return object->handle; -} + int ret = 0; + int dataLen; + int i = 0; -/** - * Check whether the object is stored on the token. - * - * @param object [in] Object object. - * @return 1 when object is on token. - * 0 when object is a session object. - */ -int WP11_Object_OnToken(WP11_Object* object) -{ - return object->onToken; -} + if (len < 3) + ret = BUFFER_E; + if (ret == 0 && der[i++] != ASN_OCTET_STRING) + ret = ASN_PARSE_E; + if (ret == 0 && der[i] >= ASN_LONG_LENGTH) { + if (der[i] != (ASN_LONG_LENGTH | 1)) + ret = ASN_PARSE_E; + else + i++; + } + if (ret == 0) { + dataLen = der[i++]; + if (dataLen != len - i) + ret = BUFFER_E; + } + if (ret == 0) { + /* Now stripped of DER encoding. */ + ret = wc_ecc_import_x963_ex(der + i, len - i, key, key->dp->id); + } -/** - * Get the object's type - for example the key type. - * - * @param object [in] Object object. - * @return Object's type. - */ -CK_KEY_TYPE WP11_Object_GetType(WP11_Object* object) -{ - return object->type; + return ret; } /** - * Get the object's devId. + * Set the EC key data into the object. + * Store the data in the wolfCrypt data structure. * * @param object [in] Object object. - * @return Object's devId. + * @param data [in] Array of byte arrays. + * @param len [in] Array of lengths of byte arrays. + * @return -ve on failure. + * 0 on success. */ -CK_ULONG WP11_Object_GetDevId(WP11_Object* object) +int WP11_Object_SetEcKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) { - return (CK_ULONG)object->devId; + int ret; + ecc_key* key; + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + key = object->data.ecKey; + ret = wc_ecc_init_ex(key, NULL, object->devId); + if (ret == 0) { + if (ret == 0 && data[0] != NULL) + ret = EcSetParams(key, data[0], (int)len[0]); + if (ret == 0 && data[1] != NULL) { + key->type = ECC_PRIVATEKEY_ONLY; +#if defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION <= 5) + ret = SetMPI(&key->k, data[1], (int)len[1]); +#else + ret = SetMPI(key->k, data[1], (int)len[1]); +#endif + } + if (ret == 0 && data[2] != NULL) { + if (key->type == ECC_PRIVATEKEY_ONLY) + key->type = ECC_PRIVATEKEY; + else + key->type = ECC_PUBLICKEY; + ret = EcSetPoint(key, data[2], (int)len[2]); + } + + if (ret != 0) + wc_ecc_free(key); + } + +#ifdef WOLFPKCS11_TPM + if (ret == 0) { + ret = WP11_Object_WrapTpmKey(object); + } +#endif + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + + return ret; } +#endif /* HAVE_ECC */ +#ifdef WOLFPKCS11_MLDSA /** - * Get the object's class. + * Set the ML-DSA parameters based on provided data. * - * @param object [in] Object object. - * @return Object's class. + * @param key [in] ML-DSA key object. + * @param params [in] Pointer to parameters structure. + * @param len [in] Length of parameters. + * @return BUFFER_E when len is too short. + * ASN_PARSE_E when parameter is bad. + * Other -ve on failure. + * 0 on success. */ -CK_OBJECT_CLASS WP11_Object_GetClass(WP11_Object* object) +static int mldsaSetParameters(MlDsaKey* key, + CK_ML_DSA_PARAMETER_SET_TYPE* params, + int len) { - return object->objClass; + int ret = 0; + + if (params == NULL || key == NULL) + return BAD_FUNC_ARG; + + if (len != sizeof(CK_ML_DSA_PARAMETER_SET_TYPE)) + return BUFFER_E; + + /* Set ML-DSA level based on the parameter */ + switch (*params) { + case CKP_ML_DSA_44: + ret = wc_MlDsaKey_SetParams(key, WC_ML_DSA_44); + break; + case CKP_ML_DSA_65: + ret = wc_MlDsaKey_SetParams(key, WC_ML_DSA_65); + break; + case CKP_ML_DSA_87: + ret = wc_MlDsaKey_SetParams(key, WC_ML_DSA_87); + break; + default: + ret = ASN_PARSE_E; + break; + } + + return ret; } -#if !defined(NO_RSA) || defined(HAVE_ECC) /** - * Set the multi-precision integer from the data. - * - * @param mpi [in] Multi-precision integer. - * @param data [in] Big-endian encoding of number. - * @param len [in] Length in bytes. - * @return -ve on failure. - * 0 on success. - */ -static int SetMPI(mp_int* mpi, unsigned char* data, int len) +* Set the ML-DSA key data into the object. +* Store the data in the wolfCrypt data structure. +* +* @param object [in] Object object. +* @param data [in] Array of byte arrays. +* @param len [in] Array of lengths of byte arrays. +* @return -ve on failure. +* 0 on success. +*/ +int WP11_Object_SetMldsaKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) { - int ret = 0; + int ret; + MlDsaKey* key; + int seedUsed = 0; - if (data != NULL) { - ret = mp_init(mpi); - if (ret == 0) - ret = mp_read_unsigned_bin(mpi, data, len); + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + key = object->data.mldsaKey; + ret = wc_MlDsaKey_Init(key, NULL, object->devId); + + /* Set parameters */ + if (ret == 0 && data[0] != NULL) { + ret = mldsaSetParameters(key, + (CK_ML_DSA_PARAMETER_SET_TYPE*)data[0], + (int)len[0]); + } + + /* Set seed (only for private keys) */ + if (ret == 0 && data[1] != NULL) { + if (object->objClass != CKO_PRIVATE_KEY) { + ret = BAD_FUNC_ARG; + } + else if (len[1] != DILITHIUM_SEED_SZ) { + ret = BAD_FUNC_ARG; + } + else { + ret = wc_dilithium_make_key_from_seed(key, data[1]); + seedUsed = 1; + } + } + + /* Set key data */ + if (ret == 0 && data[2] != NULL) { + if (seedUsed == 0) { + /* Import given public/private key data */ + if (object->objClass == CKO_PUBLIC_KEY) { + ret = wc_MlDsaKey_ImportPubRaw(key, data[2], + (word32)len[2]); + } + else { + ret = wc_MlDsaKey_ImportPrivRaw(key, data[2], + (word32)len[2]); + } + } + else { + if (object->objClass == CKO_PUBLIC_KEY) { + /* Seed is only allowed for private keys */ + ret = BAD_FUNC_ARG; + } + else { + /* Check if the provided expanded private key is identical + * to the one generated from the seed */ + byte* expandedKey = NULL; + word32 expandedKeyLen = 0; + + expandedKeyLen = wc_dilithium_size(key); + if (expandedKeyLen != len[2]) { + ret = BAD_FUNC_ARG; + } + if (ret == 0) { + expandedKey = XMALLOC(expandedKeyLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (expandedKey == NULL) { + ret = MEMORY_E; + } + } + if (ret == 0) { + ret = wc_MlDsaKey_ExportPrivRaw(key, expandedKey, + &expandedKeyLen); + if (ret == 0) { + if (WP11_ConstantCompare(expandedKey, data[2], + (int)expandedKeyLen) != 1) { + ret = BAD_FUNC_ARG; + } + } + XFREE(expandedKey, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } + } } + if (ret != 0) + wc_MlDsaKey_Free(key); + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + return ret; } -#endif +#endif /* WOLFPKCS11_MLDSA */ -#ifndef NO_RSA +#ifdef WOLFPKCS11_LMS /** - * Set the RSA key data into the object. - * Store the data in the wolfCrypt data structure. + * Set the HSS key data into the object. * - * @param object [in] Object object. - * @param data [in] Array of byte arrays. - * @param len [in] Array of lengths of byte arrays. - * @return -ve on failure. - * 0 on success. + * 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_SetRsaKey(WP11_Object* object, unsigned char** data, +int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, CK_ULONG* len) { - int ret; - RsaKey* key; + 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 (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) WP11_Lock_LockRW(object->lock); - key = object->data.rsaKey; - ret = wc_InitRsaKey_ex(key, NULL, object->devId); - if (ret == 0) { - ret = SetMPI(&key->d, data[1], (int)len[1]); - if (ret == 0) - ret = SetMPI(&key->p, data[2], (int)len[2]); - if (ret == 0) - ret = SetMPI(&key->q, data[3], (int)len[3]); - /* If modulus is not provided, calculate it */ - if (ret == 0) { - if (data[0] == NULL || len[0] == 0) { - ret = mp_mul(&key->p, &key->q, &key->n); - } else { - ret = SetMPI(&key->n, data[0], (int)len[0]); - } - } - if (ret == 0) - ret = SetMPI(&key->dP, data[4], (int)len[4]); - if (ret == 0) - ret = SetMPI(&key->dQ, data[5], (int)len[5]); - if (ret == 0) - ret = SetMPI(&key->u, data[6], (int)len[6]); - if (ret == 0) { - /* Public exponent defaults to 65537 in PKCS11 > 2.11 */ - if (len[7] > 0) - ret = SetMPI(&key->e, data[7], (int)len[7]); - else { - byte defaultPublic[] = {0x01, 0x00, 0x01}; - ret = SetMPI(&key->e, defaultPublic, sizeof(defaultPublic)); - } - } - if (ret == 0) { - if (len[8] == sizeof(CK_ULONG)) - object->size = (word32)*(CK_ULONG*)data[8]; - else if (len[8] != 0) - ret = BUFFER_E; - } - if (ret == 0) { - if (mp_iszero(&key->d) && mp_iszero(&key->p)) { - key->type = RSA_PUBLIC; - } - else { - key->type = RSA_PRIVATE; - } - } + key = object->data.lmsKey; - if (ret != 0) { - wc_FreeRsaKey(key); - } + /* 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; } -#ifdef WOLFPKCS11_TPM - if (ret == 0) { - ret = WP11_Object_WrapTpmKey(object); + /* 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); } -#endif + 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; } -#endif -#ifdef HAVE_ECC -#ifdef USE_LOCAL_CURVE_OID_LOOKUP -static int ecc_lookup_curve(const byte* oid, word32 len) +/** + * Return signature length for the HSS key. + */ +int WP11_Hss_SigLen(WP11_Object* key) { - const WP11_Ecc_Curve* curve; - - for (curve = DefinedCurves; curve->curve_id < ECC_CURVE_MAX; curve++) - { - if (len == curve->curve_size && - XMEMCMP(oid, curve->curve_oid, len) == 0) { - return curve->curve_id; - } - } - return ECC_CURVE_INVALID; + 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; } -#endif /** - * Set the EC Parameters based on the DER encoding of the OID. - * - * @param key [in] EC Key object. - * @param der [in] DER encoding of OID. - * @param len [in] Length of DER encoding. - * @return BUFFER_E when len is too short. - * ASN_PARSE_E when DER encoding is bad. - * BAD_FUNC_ARG when OID is not known. - * Other -ve on failure. - * 0 on success. + * Return public key length for the HSS key. */ -static int EcSetParams(ecc_key* key, byte* der, int len) +int WP11_Hss_PubLen(WP11_Object* key) { - int ret = 0; - int keySize; - int curveId; - - if (len < 2) - ret = BUFFER_E; - if (ret == 0 && der[0] != ASN_OBJECT_ID) - ret = ASN_PARSE_E; - if (ret == 0 && der[1] != len - 2) - ret = BUFFER_E; - if (ret == 0) { -#ifdef USE_LOCAL_CURVE_OID_LOOKUP - /* Find the curve matching the OID. */ - /* wc_ecc_get_curve_id_from_oid() is broken in FIPSv5 and ecc_sets is - * not accessible in FIPS, so we have our own lookup. - */ - curveId = ecc_lookup_curve(der +2, der[1]); -#else - curveId = wc_ecc_get_curve_id_from_oid(der + 2, der[1]); -#endif - if (curveId == ECC_CURVE_INVALID) - ret = BAD_FUNC_ARG; - } - if (ret == 0) { - /* Set the curve into the EC key. */ - keySize = wc_ecc_get_curve_size_from_id(curveId); - ret = wc_ecc_set_curve(key, keySize, curveId); - } + 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; +} - return ret; +/** + * 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); } /** - * Set the EC Point, encoded in DER and X9.63, as the public key. - * - * @param key [in] EC Key object. - * @param der [in] DER encoding of OID. - * @param len [in] Length of DER encoding. - * @return BUFFER_E when len is too short. - * ASN_PARSE_E when DER encoding is bad. - * Other -ve on failure. - * 0 on success. + * Verify an HSS signature over a raw message. Stateless on the verifier. */ -static int EcSetPoint(ecc_key* key, byte* der, int len) +int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, + word32 dataLen, int* stat, WP11_Object* pub) { - int ret = 0; - int dataLen; - int i = 0; + int ret; - if (len < 3) - ret = BUFFER_E; - if (ret == 0 && der[i++] != ASN_OCTET_STRING) - ret = ASN_PARSE_E; - if (ret == 0 && der[i] >= ASN_LONG_LENGTH) { - if (der[i] != (ASN_LONG_LENGTH | 1)) - ret = ASN_PARSE_E; - else - i++; - } + 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 "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) { - dataLen = der[i++]; - if (dataLen != len - i) - ret = BUFFER_E; + *stat = 1; } - if (ret == 0) { - /* Now stripped of DER encoding. */ - ret = wc_ecc_import_x963_ex(der + i, len - i, key, key->dp->id); + 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 */ + } + else { + *stat = 0; + /* leave ret < 0 for the C-layer to map to CKR_FUNCTION_FAILED */ } + if (pub->onToken) + WP11_Lock_UnlockRO(pub->lock); + return ret; } +#ifdef WOLFPKCS11_LMS_PRIVATE /** - * Set the EC key data into the object. - * Store the data in the wolfCrypt data structure. + * Generate an HSS key pair, persist genesis state durably, and populate the + * public-key object with the matching public key. * - * @param object [in] Object object. - * @param data [in] Array of byte arrays. - * @param len [in] Array of lengths of byte arrays. - * @return -ve on failure. - * 0 on success. + * 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 + * 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_Object_SetEcKey(WP11_Object* object, unsigned char** data, - CK_ULONG* len) +int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + const CK_HSS_PARAMS* params, CK_ULONG paramsLen, + WP11_Slot* slot) { int ret; - ecc_key* key; + int levels = 0, height = 0, winternitz = 0; + WC_RNG rng; + byte pubBuf[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubLen = sizeof(pubBuf); + int stateWritten = 0; - if (object->onToken) - WP11_Lock_LockRW(object->lock); + if (pub == NULL || priv == NULL || slot == NULL || + pub->data.lmsKey == NULL || priv->data.lmsKey == NULL) { + return BAD_FUNC_ARG; + } - key = object->data.ecKey; - ret = wc_ecc_init_ex(key, NULL, object->devId); - if (ret == 0) { - if (ret == 0 && data[0] != NULL) - ret = EcSetParams(key, data[0], (int)len[0]); - if (ret == 0 && data[1] != NULL) { - key->type = ECC_PRIVATEKEY_ONLY; -#if defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION <= 5) - ret = SetMPI(&key->k, data[1], (int)len[1]); -#else - ret = SetMPI(key->k, data[1], (int)len[1]); -#endif + /* 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->statefulStateId = wp11_Stateful_ReadU64(nonceBuf); + if (priv->statefulStateId != 0) + break; } - if (ret == 0 && data[2] != NULL) { - if (key->type == ECC_PRIVATEKEY_ONLY) - key->type = ECC_PRIVATEKEY; - else - key->type = ECC_PUBLICKEY; - ret = EcSetPoint(key, data[2], (int)len[2]); + 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; } + } - if (ret != 0) - wc_ecc_free(key); + /* 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->statefulSigCount = 0; + + 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. + * 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; } -#ifdef WOLFPKCS11_TPM + /* 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) { - ret = WP11_Object_WrapTpmKey(object); + priv->local = pub->local = 1; + priv->keyGenMech = pub->keyGenMech = CKM_HSS_KEY_PAIR_GEN; + priv->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + } + else { + wc_LmsKey_Free(priv->data.lmsKey); + /* Remove any state file we may have written before the failure. */ + if (stateWritten && priv->statefulStateId != 0) { + (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. */ + priv->statefulStateId = 0; } -#endif - if (object->onToken) - WP11_Lock_UnlockRW(object->lock); + wc_ForceZero(pubBuf, sizeof(pubBuf)); return ret; } -#endif /* HAVE_ECC */ -#ifdef WOLFPKCS11_MLDSA /** - * Set the ML-DSA parameters based on provided data. + * 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_STATEFUL_STATE_VALID cleared) + * to force a reload from durable storage on the next attempt. * - * @param key [in] ML-DSA key object. - * @param params [in] Pointer to parameters structure. - * @param len [in] Length of parameters. - * @return BUFFER_E when len is too short. - * ASN_PARSE_E when parameter is bad. - * Other -ve on failure. - * 0 on success. + * 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 statefulSigCount before the wolfSSL Sign call so the persisted + * counter in the state file matches "this signature is leaf N"; on failure + * we revert. */ -static int mldsaSetParameters(MlDsaKey* key, - CK_ML_DSA_PARAMETER_SET_TYPE* params, - int len) +int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, + word32* sigLen, WP11_Object* priv) { int ret = 0; + word32 outLen = 0; - if (params == NULL || key == NULL) + if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || + sigLen == NULL || priv->lock == NULL) { return BAD_FUNC_ARG; + } - if (len != sizeof(CK_ML_DSA_PARAMETER_SET_TYPE)) - return BUFFER_E; + WP11_Lock_LockRW(priv->lock); - /* Set ML-DSA level based on the parameter */ - switch (*params) { - case CKP_ML_DSA_44: - ret = wc_MlDsaKey_SetParams(key, WC_ML_DSA_44); - break; - case CKP_ML_DSA_65: - ret = wc_MlDsaKey_SetParams(key, WC_ML_DSA_65); - break; - case CKP_ML_DSA_87: - ret = wc_MlDsaKey_SetParams(key, WC_ML_DSA_87); - break; - default: - ret = ASN_PARSE_E; - break; + if ((priv->opFlag & WP11_FLAG_STATEFUL_STATE_VALID) == 0) { + ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ + } + if (ret == 0) { + word64 prevCount = priv->statefulSigCount; + priv->statefulSigCount = prevCount + 1; + outLen = *sigLen; + ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, + (int)dataLen); + if (ret == 0) { + *sigLen = outLen; + } + else { + /* 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 only the bytes wolfSSL may have written. */ + 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->statefulSigCount = prevCount; + } } + WP11_Lock_UnlockRW(priv->lock); + return ret; } /** -* Set the ML-DSA key data into the object. -* Store the data in the wolfCrypt data structure. -* -* @param object [in] Object object. -* @param data [in] Array of byte arrays. -* @param len [in] Array of lengths of byte arrays. -* @return -ve on failure. -* 0 on success. -*/ -int WP11_Object_SetMldsaKey(WP11_Object* object, unsigned char** data, - CK_ULONG* len) + * Returns the number of remaining one-time-keys ("signatures left") for the + * 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). + * + * 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 ret; - MlDsaKey* key; - int seedUsed = 0; + int levels = 0, height = 0, winternitz = 0; + int totalH; + word64 used; + word64 total; - if (object->onToken) - WP11_Lock_LockRW(object->lock); - - key = object->data.mldsaKey; - ret = wc_MlDsaKey_Init(key, NULL, object->devId); + if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) + return BAD_FUNC_ARG; - /* Set parameters */ - if (ret == 0 && data[0] != NULL) { - ret = mldsaSetParameters(key, - (CK_ML_DSA_PARAMETER_SET_TYPE*)data[0], - (int)len[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; } - /* Set seed (only for private keys) */ - if (ret == 0 && data[1] != NULL) { - if (object->objClass != CKO_PRIVATE_KEY) { - ret = BAD_FUNC_ARG; - } - else if (len[1] != DILITHIUM_SEED_SZ) { - ret = BAD_FUNC_ARG; - } - else { - ret = wc_dilithium_make_key_from_seed(key, data[1]); - seedUsed = 1; - } + if (wc_LmsKey_GetParameters(key->data.lmsKey, &levels, &height, + &winternitz) != 0) { + *remaining = 0; + return BAD_FUNC_ARG; } - - /* Set key data */ - if (ret == 0 && data[2] != NULL) { - if (seedUsed == 0) { - /* Import given public/private key data */ - if (object->objClass == CKO_PUBLIC_KEY) { - ret = wc_MlDsaKey_ImportPubRaw(key, data[2], - (word32)len[2]); - } - else { - ret = wc_MlDsaKey_ImportPrivRaw(key, data[2], - (word32)len[2]); - } - } - else { - if (object->objClass == CKO_PUBLIC_KEY) { - /* Seed is only allowed for private keys */ - ret = BAD_FUNC_ARG; - } - else { - /* Check if the provided expanded private key is identical - * to the one generated from the seed */ - byte* expandedKey = NULL; - word32 expandedKeyLen = 0; - - expandedKeyLen = wc_dilithium_size(key); - if (expandedKeyLen != len[2]) { - ret = BAD_FUNC_ARG; - } - if (ret == 0) { - expandedKey = XMALLOC(expandedKeyLen, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (expandedKey == NULL) { - ret = MEMORY_E; - } - } - if (ret == 0) { - ret = wc_MlDsaKey_ExportPrivRaw(key, expandedKey, - &expandedKeyLen); - if (ret == 0) { - if (WP11_ConstantCompare(expandedKey, data[2], - (int)expandedKeyLen) != 1) { - ret = BAD_FUNC_ARG; - } - } - XFREE(expandedKey, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - } - } + totalH = levels * height; + if (totalH <= 0) { + *remaining = 0; + return BAD_FUNC_ARG; } - - if (ret != 0) - wc_MlDsaKey_Free(key); - - if (object->onToken) - WP11_Lock_UnlockRW(object->lock); - - return ret; + if (totalH >= 64) { + /* 2^totalH does not fit in word64. Cap at UINT32_MAX. */ + *remaining = 0xFFFFFFFFU; + return 0; + } + total = ((word64)1) << totalH; + used = key->statefulSigCount; + if (used >= total) + *remaining = 0; + else if ((total - used) > 0xFFFFFFFFULL) + *remaining = 0xFFFFFFFFU; + else + *remaining = (word32)(total - used); + return 0; } -#endif /* WOLFPKCS11_MLDSA */ +#endif /* WOLFPKCS11_LMS_PRIVATE */ +#endif /* WOLFPKCS11_LMS */ #ifndef NO_DH /** @@ -10401,6 +12331,188 @@ 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; + ret = WP11_Hss_SigsLeft(object, &remaining); + if (ret == 0) { + CK_ULONG 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 { + *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 +13049,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..fb718d0e 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,26 @@ static CK_MECHANISM_INFO mldsaMechInfo = { CKF_SIGN | CKF_VERIFY }; #endif +#ifdef WOLFPKCS11_LMS +/* 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 = { + 0, 0, + CKF_VERIFY +# ifdef WOLFPKCS11_LMS_PRIVATE + | CKF_SIGN +# endif +}; +# ifdef WOLFPKCS11_LMS_PRIVATE +static CK_MECHANISM_INFO hssKgMechInfo = { + 0, 0, CKF_GENERATE_KEY_PAIR +}; +# endif +#endif #ifdef WOLFPKCS11_HKDF static CK_MECHANISM_INFO hkdfMechInfo = { 1, 16320, CKF_DERIVE @@ -998,6 +1024,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..636c9831 --- /dev/null +++ b/tests/lms_state_persistence_test.c @@ -0,0 +1,607 @@ +/* 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. + * 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 + #include +#endif + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include +#include + +#ifndef WOLFPKCS11_USER_SETTINGS + #include +#endif +#include + +#ifndef HAVE_PKCS11_STATIC +#include +#ifndef WOLFPKCS11_DLL_FILENAME + #define WOLFPKCS11_DLL_FILENAME "src/.libs/libwolfpkcs11.so" +#endif +#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, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d - %s FAIL\n", \ + __FILE__, __LINE__, (msg)); \ + return 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 char tokenDir[256]; + +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"); + + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + CHECK_CKR(ret, "GetSlotList"); + if (slotCount == 0) + return CKR_GENERAL_ERROR; + slot = slotList[0]; + 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)"); + ret = funcList->C_Login(s, CKU_SO, soPin, soPinLen); + 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 (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)"); + ret = funcList->C_FindObjects(session, h, 1, &count); + CHECK_CKR(ret, "FindObjects (priv)"); + funcList->C_FindObjectsFinal(session); + if (count != 1) + return CKR_GENERAL_ERROR; + return CKR_OK; +} + +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)"); + ret = funcList->C_FindObjects(session, h, 1, &count); + CHECK_CKR(ret, "FindObjects (pub)"); + funcList->C_FindObjectsFinal(session); + if (count != 1) + return CKR_GENERAL_ERROR; + return CKR_OK; +} + +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)"); + 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)"); + 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); +} + +/* 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) { + /* 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]; + 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; + 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, + "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); + pkcs11_final(); + + 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 = find_hss_priv(s2, &priv2); + 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, + "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, + "KEYS_REMAINING != 30 after second sign"); + } + if (ret != CKR_OK) { + funcList->C_Logout(s2); + funcList->C_CloseSession(s2); + pkcs11_final(); + return ret; + } + + /* 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(); + + 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. 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(); + 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 */ + +int main(int argc, char* argv[]) +{ + (void)argc; (void)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 + { + 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"); + printf("Token store directory: %s\n\n", tokenDir); + + 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..bbc7b1bd 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; @@ -1021,6 +1025,361 @@ 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. + * + * 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, + 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, 1); + 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 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, 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) { + /* 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(remainingPrev > 0 && + remaining == remainingPrev - 1, ret, + "HSS keys remaining must decrement by exactly one per sign"); + } + } + } + + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + 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) +{ + 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, 1); + 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, 1); + 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 +3134,18 @@ 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), + PKCS11TEST_FUNC_SESS_DECL(test_hss_session_keygen_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..d2e8fc33 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,45 @@ 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 + +/* 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 @@ -218,6 +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 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 @@ -265,6 +314,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 +596,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..720013b9 100644 --- a/wolfpkcs11/pkcs11.h +++ b/wolfpkcs11/pkcs11.h @@ -178,6 +178,10 @@ extern "C" { #define CKK_HKDF 0x00000042UL #define CKK_ML_KEM 0x00000049UL #define CKK_ML_DSA 0x0000004AUL +/* 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 #ifdef WOLFPKCS11_NSS /* Not defined by NSS, but we need one */ @@ -262,6 +266,13 @@ extern "C" { /* KEM */ #define CKA_ENCAPSULATE 0x00000633UL #define CKA_DECAPSULATE 0x00000634UL +/* 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 #ifdef WOLFPKCS11_NSS #define CKA_NSS_EMAIL (CKA_NSS + 2) @@ -365,6 +376,9 @@ extern "C" { #define CKM_ML_DSA_KEY_PAIR_GEN 0x0000001CUL #define CKM_ML_DSA 0x0000001DUL #define CKM_HASH_ML_DSA 0x0000001FUL +/* LMS/HSS mechanisms (RFC 8554). Exposed unconditionally — see CKK_HSS. */ +#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 +891,47 @@ 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. + * + * 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; + +/* 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 + +/* 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 (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..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; + /* 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.