diff --git a/.github/workflows/build-workflow.yml b/.github/workflows/build-workflow.yml index 9741730b..a623ea2b 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: + # Callers passing --enable-lms-private in `config` must also set this + # to 'yes': wolfPKCS11 keygen and signing need a sign-capable wolfSSL + # LMS build, and its configure now fails outright on a verify-only one. + required: false + type: string + default: 'verify-only' + wolfssl_xmss: + required: false + type: string + default: 'verify-only' + wolfssl_extra: + required: false + type: string + default: '' jobs: build: @@ -36,7 +51,7 @@ jobs: working-directory: ./wolfssl run: | ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 \ - --enable-mldsa --enable-lms=verify-only --enable-xmss=verify-only C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa --enable-lms=${{inputs.wolfssl_lms}} --enable-xmss=${{inputs.wolfssl_xmss}} ${{inputs.wolfssl_extra}} C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" - name: wolfssl make install working-directory: ./wolfssl run: make diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e8c73717..fb433a8d 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -65,7 +65,7 @@ jobs: -DWOLFPKCS11_AESECB:BOOL=yes -DWOLFPKCS11_AESCTS:BOOL=yes -DWOLFPKCS11_AESCMAC:BOOL=yes \ -DWOLFPKCS11_PBKDF2:BOOL=yes -DWOLFPKCS11_SHA3:BOOL=yes -DWOLFPKCS11_PKCS11_V3_0:BOOL=yes \ -DWOLFPKCS11_PKCS11_V3_2:BOOL=yes -DWOLFPKCS11_MLDSA:BOOL=yes -DWOLFPKCS11_MLKEM:BOOL=yes \ - -DWOLFPKCS11_LMS:BOOL=yes -DWOLFPKCS11_XMSS:BOOL=yes \ + -DWOLFPKCS11_LMS:BOOL=yes -DWOLFPKCS11_LMS_PRIVATE:BOOL=yes -DWOLFPKCS11_XMSS:BOOL=yes \ -DCMAKE_MODULE_PATH="$GITHUB_WORKSPACE/install/${CMAKE_INSTALL_LIBDIR}" \ .. cmake --build . diff --git a/.github/workflows/sanitizer-tests.yml b/.github/workflows/sanitizer-tests.yml index 3eb3d00f..6aa5354d 100644 --- a/.github/workflows/sanitizer-tests.yml +++ b/.github/workflows/sanitizer-tests.yml @@ -28,6 +28,8 @@ jobs: configure_flags: "--enable-nss --enable-tpm" - name: "PKCS#11 V3.2 PQC Build" configure_flags: "--enable-pkcs11v32 --enable-mldsa --enable-mlkem" + - name: "LMS/HSS private (sign)" + configure_flags: "--enable-lms-private" steps: #pull wolfPKCS11 @@ -76,7 +78,7 @@ jobs: ;; esac ./configure --enable-cryptocb --enable-aescfb --enable-rsapss --enable-keygen --enable-pwdbased --enable-scrypt --enable-md5 --enable-debug \ - --enable-mldsa C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" + --enable-mldsa --enable-lms C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT -DHAVE_AES_ECB -DHAVE_AES_KEYWRAP" - name: wolfssl make working-directory: ./wolfssl run: | diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8cfcb303..8130db33 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -126,6 +126,28 @@ jobs: uses: ./.github/workflows/build-workflow.yml with: config: --enable-lms --enable-xmss --disable-shared + # LMS/HSS keygen + signing. Needs a sign-capable wolfSSL LMS + # build (full --enable-lms, not verify-only); --enable-lms-private implies + # --enable-lms on the wolfPKCS11 side. + lms_private: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private + wolfssl_lms: 'yes' + lms_private_static: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private --disable-shared + wolfssl_lms: 'yes' + # Small-memory WC_LMS variant (WOLFSSL_WC_LMS_SMALL): different private-data + # sizing, so it exercises the state-file engine's dynamic sizing. Skip + # wolfSSL's own crypttests, which fail to link in this LMS config. + lms_private_small: + uses: ./.github/workflows/build-workflow.yml + with: + config: --enable-lms-private + wolfssl_lms: 'small' + wolfssl_extra: '--disable-crypttests' debug: uses: ./.github/workflows/build-workflow.yml with: diff --git a/CMakeLists.txt b/CMakeLists.txt index bf4ada64..cb6a836c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -474,7 +474,7 @@ endif() if(WOLFPKCS11_MLDSA) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "ML-DSA requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "ML-DSA requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -498,7 +498,7 @@ endif() if(WOLFPKCS11_MLKEM) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "ML-KEM requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "ML-KEM requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -522,7 +522,7 @@ endif() if(WOLFPKCS11_LMS) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "LMS/HSS requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "LMS/HSS requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -533,6 +533,38 @@ if(WOLFPKCS11_LMS) list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS") endif() +# LMS/HSS keygen + signing (EXPERIMENTAL, stateful private keys; default: disabled) +add_option("WOLFPKCS11_LMS_PRIVATE" + "Enable wolfPKCS11 LMS/HSS keygen and signing (EXPERIMENTAL, stateful private keys, default: disabled)" + "no" "yes;no" +) + +# LMS/HSS is SHA-256 based. Check the dependency BEFORE the force-enable below +# so a missing SHA-256 disables LMS_PRIVATE without leaving LMS enabled on its +# own (the force-enable would otherwise turn LMS on for a broken build). +if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_SHA256) + message(STATUS "LMS/HSS requires SHA-256 support (disabled); disabling WOLFPKCS11_LMS_PRIVATE") + override_cache(WOLFPKCS11_LMS_PRIVATE "no") +endif() + +if(WOLFPKCS11_LMS_PRIVATE AND NOT WOLFPKCS11_LMS) + message(STATUS "WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS - enabling WOLFPKCS11_LMS automatically") + override_cache(WOLFPKCS11_LMS "yes") + if(NOT WOLFPKCS11_PKCS11_V3_2) + override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") + if(NOT WOLFPKCS11_PKCS11_V3_0) + override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_PKCS11_V3_0") + endif() + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_PKCS11_V3_2") + endif() + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS") +endif() + +if(WOLFPKCS11_LMS_PRIVATE) + list(APPEND WOLFPKCS11_DEFINITIONS "-DWOLFPKCS11_LMS_PRIVATE") +endif() + # XMSS/XMSS^MT (RFC 8391) verify + public-key import (default: disabled) add_option("WOLFPKCS11_XMSS" "Enable wolfPKCS11 XMSS/XMSS^MT verification and public-key import (default: disabled)" @@ -546,7 +578,7 @@ endif() if(WOLFPKCS11_XMSS) if(NOT WOLFPKCS11_PKCS11_V3_2) - message(STATUS "XMSS requires PKCS#11 v3.2 support — enabling WOLFPKCS11_PKCS11_V3_2 automatically") + message(STATUS "XMSS requires PKCS#11 v3.2 support - enabling WOLFPKCS11_PKCS11_V3_2 automatically") override_cache(WOLFPKCS11_PKCS11_V3_2 "yes") if(NOT WOLFPKCS11_PKCS11_V3_0) override_cache(WOLFPKCS11_PKCS11_V3_0 "yes") @@ -599,6 +631,7 @@ set(LIB_SOURCES src/wolfpkcs11.c src/slot.c src/crypto.c + src/hbs.c ) if(BUILD_SHARED_LIBS) message(STATUS "BUILD_SHARED_LIBS enabled: ${LIB_SOURCES}") @@ -785,6 +818,26 @@ if(WOLFPKCS11_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/tests/hbs_persistence_test.c) set_tests_properties(hbs_persistence_test PROPERTIES SKIP_RETURN_CODE 77) endif() + + # LMS/HSS keygen and signing tests. Both exit 77 (skip) when the feature is + # compiled out, so they can be registered unconditionally like the test + # above and still behave in a verify-only build. + # + # Each gets its own token directory. They wipe the store between sub-tests + # to start from a known state, which would clobber the other tests running + # beside them under `ctest -j`. + add_wpkcs11_test(hbs_private_test + ${CMAKE_CURRENT_SOURCE_DIR}/tests/hbs_private_test.c) + set_tests_properties(hbs_private_test PROPERTIES + SKIP_RETURN_CODE 77 + ENVIRONMENT + "WOLFPKCS11_TOKEN_PATH=${CMAKE_CURRENT_BINARY_DIR}/store-hbs-private") + add_wpkcs11_test(hss_concurrency_test + ${CMAKE_CURRENT_SOURCE_DIR}/tests/hss_concurrency_test.c) + set_tests_properties(hss_concurrency_test PROPERTIES + SKIP_RETURN_CODE 77 + ENVIRONMENT + "WOLFPKCS11_TOKEN_PATH=${CMAKE_CURRENT_BINARY_DIR}/store-hss-concurrency") endif() if(WOLFPKCS11_EXAMPLES) diff --git a/README.md b/README.md index c7908a49..94f79dd2 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,94 @@ As ML-KEM is a feature of PKCS#11 version 3.2, support for that is required, too. Hence, to enable all in wolfPKCS11, add `--enable-pkcs11v32 --enable-mlkem` during the configure step. +### Optional: LMS/HSS Hash-Based Signatures (RFC 8554) + +Verification and public-key import for LMS/HSS are enabled with `--enable-lms` +(or `WOLFPKCS11_LMS=yes` in CMake) and require a wolfSSL build with LMS support +(`--enable-lms`, `WOLFSSL_HAVE_LMS`). LMS/HSS is a feature of PKCS#11 version +3.2, so that support is enabled automatically. + +Key generation and signing are **EXPERIMENTAL** and off by default. Enable them +with `--enable-lms-private` (or `WOLFPKCS11_LMS_PRIVATE=yes` in CMake), which +implies `--enable-lms` and requires a *sign-capable* wolfSSL LMS build (full +`--enable-lms`, not `--enable-lms=verify-only`). + +LMS/HSS is a **stateful** signature scheme: every one-time-signature (OTS) leaf +index must be used at most once, and reusing an index catastrophically breaks +security. wolfPKCS11 therefore: + +* Requires stateful private keys to be token-resident (`CKA_TOKEN=TRUE`); a + session-only key that vanished on a crash could release a signature whose OTS + index was never persisted. +* Durably persists the advanced key state (fsync plus atomic rename of an + encrypted, authenticated state file) *before* each signature is released, so a + crash or power loss cannot roll the OTS index backwards. + +Because the signature state is durable and per-key, do not clone, snapshot, or +restore the token storage directory of a machine that signs with LMS/HSS keys - +rolling the state files back to an earlier point would reuse OTS indices. + +#### Security model for stateful private keys + +Each HSS private key has a durable, per-key state file separate from the key +object. Its layout is an AES-GCM ciphertext whose Additional Authenticated Data +(AAD) binds the header magic, format version, scheme parameters (levels, LMS +height, LMOTS Winternitz), and the running signature counter. A fresh random +nonce is generated for every write. The private key material is encrypted with +the token key (derived from the user PIN) and the decrypted buffers are cleared +with `wc_ForceZero` on every path. + +What this **detects and refuses** (fail-closed): + +* In-place tampering or corruption of a state file - a flipped byte, a truncated + file, a wrong magic/version, or altered scheme parameters fails the AES-GCM + authentication (or the header check) at reload time, and the key will not + sign. +* A signature-state write that does not become durable (fsync/rename failure) - + the key is poisoned in memory, the in-memory counter is reverted, and no + signature is released; a reload recovers to the last durable index and never + advances past the failed write. + +What this **cannot** protect against (inherent to any software-only stateful +scheme - handle operationally): + +* **Offline state rollback.** Anyone who can copy the token directory and later + restore it - a filesystem backup/restore, a VM or container snapshot, `cp`, + or block-level replication - resets the OTS counter and causes one-time-key + **reuse**, which breaks the signatures irrecoverably. Only an HSM/TPM with a + hardware monotonic counter can prevent this, and that path is out of scope + here. Never restore or duplicate a signing machine's token directory. +* **`WOLFPKCS11_STATEFUL_RELAX_FSYNC=1`.** An opt-in environment variable that + trades durability for speed when running tests on throwaway storage, at the + cost of the guarantee above: a crash or power loss can then leave the on-disk + state behind the signatures already released, reusing OTS keys. It is never + set by default; leave it unset in any real deployment. See + [WOLFPKCS11_STATEFUL_RELAX_FSYNC](#wolfpkcs11_stateful_relax_fsync). +* **Concurrent use of one token directory from multiple processes.** State + durability and mutual exclusion are enforced within a single library instance; + two processes signing against the same on-disk token can reuse indices. Use + one signing process per token directory. + +HSS signing is not secret-input timing-sensitive the way an RSA/ECC private +scalar is (the message and the tree position are not confidential), so no +constant-time treatment beyond the existing token-key handling is required. + +#### Known limitation: a spent or damaged key is hidden, not reportable + +An HSS private key whose state cannot be reloaded - because the tree is +exhausted, or because the state file was damaged - stays flagged as undecoded, +which is how this library already treats any object it cannot decode for the +current user. Such an object is skipped by `C_FindObjects`. + +The token itself keeps working: `C_Login` succeeds and every other object on it +stays usable. But the spent key is not enumerable, so it cannot be handed to +`C_DestroyObject`; its object and state files have to be removed from the token +directory by hand. + +Reporting the key instead of hiding it means quarantining objects individually +in the shared decode-at-login path, which affects every key type. That is +planned as a separate change. + ### Build options and defines #### Define WOLFPKCS11_TPM_STORE @@ -267,6 +355,8 @@ cmake -DCMAKE_PREFIX_PATH=/path/to/wolfssl/install .. | `WOLFPKCS11_PKCS11_V3_2` | `no` | PKCS#11 v3.2 support | | `WOLFPKCS11_MLDSA` | `no` | ML-DSA support | | `WOLFPKCS11_MLKEM` | `no` | ML-KEM support | +| `WOLFPKCS11_LMS` | `no` | LMS/HSS verification and public-key import | +| `WOLFPKCS11_LMS_PRIVATE` | `no` | LMS/HSS keygen + signing (EXPERIMENTAL, stateful private keys) | | `WOLFPKCS11_EXAMPLES` | `yes` | Build examples | | `WOLFPKCS11_TESTS` | `yes` | Build and register tests | | `WOLFPKCS11_COVERAGE` | `no` | Code coverage support | @@ -294,6 +384,15 @@ POSIX or `%APPDIR%\wolfPKCS11` on Windows), and finally the optional Set to any value to stop storage of token data. +### WOLFPKCS11_STATEFUL_RELAX_FSYNC + +**Non-production only.** When set to `1`, wolfPKCS11 skips the `fsync()` calls +(file and parent directory) that make LMS/HSS signature-state writes durable. +This speeds up test runs on throwaway storage but is **dangerous**: after a +crash or power loss the on-disk state can lag the signatures already released, +leading to **OTS-key reuse**. Leave it unset (the default always fsyncs) in any +real deployment. Only meaningful when built with `--enable-lms-private`. + ## Release Notes diff --git a/cmake/options.h.in b/cmake/options.h.in index 594bcdb3..f1234dee 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -98,6 +98,8 @@ extern "C" { #cmakedefine WOLFPKCS11_MLKEM #undef WOLFPKCS11_LMS #cmakedefine WOLFPKCS11_LMS +#undef WOLFPKCS11_LMS_PRIVATE +#cmakedefine WOLFPKCS11_LMS_PRIVATE #undef WOLFPKCS11_XMSS #cmakedefine WOLFPKCS11_XMSS #undef WOLFPKCS11_TPM diff --git a/configure.ac b/configure.ac index 2ef69ffa..be0d84cf 100644 --- a/configure.ac +++ b/configure.ac @@ -596,6 +596,64 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS" fi +# LMS/HSS key generation and signing (EXPERIMENTAL). Stateful private keys: +# a one-time signature leaf must never be reused, so signing durably persists +# the advanced key state before releasing each signature. Implies --enable-lms +# and requires a sign-capable wolfSSL LMS build (not --enable-lms=verify-only). +AC_ARG_ENABLE([lms-private], + [AS_HELP_STRING([--enable-lms-private],[Enable LMS/HSS keygen and signing (EXPERIMENTAL, stateful private keys, default: disabled)])], + [ ENABLED_LMS_PRIVATE=$enableval ], + [ ENABLED_LMS_PRIVATE=no ] + ) + +# LMS/HSS is SHA-256 based. Check the dependency BEFORE the --enable-lms +# implication below so a missing SHA-256 disables lms-private without leaving +# --enable-lms turned on for a broken build. +if test "$ENABLED_SHA256" = "no" && test "$ENABLED_LMS_PRIVATE" = "yes" +then + echo "LMS/HSS requires SHA-256 support (disabled), disabling LMS/HSS keygen+sign" + ENABLED_LMS_PRIVATE=no +fi + +if test "$ENABLED_LMS_PRIVATE" = "yes" && test "$ENABLED_LMS" = "no" +then + AC_MSG_NOTICE([--enable-lms-private implies --enable-lms; enabling]) + ENABLED_LMS=yes + if test "$ENABLED_PKCS11V3_2" = "no"; then + ENABLED_PKCS11V3_2=yes + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_PKCS11_V3_2" + if test "$ENABLED_PKCS11V3_0" = "no"; then + ENABLED_PKCS11V3_0=yes + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_PKCS11_V3_0" + fi + fi + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS" +fi + +# A verify-only wolfSSL LMS build has no wc_LmsKey_MakeKey/_Sign, which would +# otherwise surface as a pile of errors deep in the compile of src/hbs.c. Catch +# it here instead. WOLFSSL_LMS_VERIFY_ONLY is only visible when the installed +# headers expose it, so probe the symbol rather than the macro. +if test "$ENABLED_LMS_PRIVATE" = "yes" +then + AC_MSG_CHECKING([for a sign-capable wolfSSL LMS build]) + SAVED_LIBS="$LIBS" + LIBS="$LIBS -lwolfssl" + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[#include + #include + #include ]], + [[LmsKey* k = (LmsKey*)0; WC_RNG* r = (WC_RNG*)0; + return wc_LmsKey_MakeKey(k, r);]])], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([--enable-lms-private needs a sign-capable wolfSSL LMS build; wolfSSL configured with --enable-lms=verify-only provides verification only. Rebuild wolfSSL with --enable-lms=yes.])]) + LIBS="$SAVED_LIBS" + + AM_CFLAGS="$AM_CFLAGS -DWOLFPKCS11_LMS_PRIVATE" +fi + # XMSS/XMSS^MT (RFC 8391) verification and public-key import. Requires wolfSSL # built with --enable-xmss (WOLFSSL_HAVE_XMSS). Verify + public-key import only. AC_ARG_ENABLE([xmss], @@ -815,6 +873,7 @@ echo " * HKDF: $ENABLED_HKDF" echo " * ML-DSA: $ENABLED_MLDSA" echo " * ML-KEM: $ENABLED_MLKEM" echo " * LMS/HSS verify: $ENABLED_LMS" +echo " * LMS/HSS sign+keygen (EXP): $ENABLED_LMS_PRIVATE" echo " * XMSS/XMSS^MT verify: $ENABLED_XMSS" echo " * NSS modifications: $ENABLED_NSS" echo " * Default token path: $WOLFPKCS11_DEFAULT_TOKEN_PATH" diff --git a/src/crypto.c b/src/crypto.c index f8596104..aad2628c 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -1516,6 +1516,23 @@ static CK_RV CreateObject(WP11_Session* session, CK_ATTRIBUTE_PTR pTemplate, } } +#ifdef WOLFPKCS11_LMS_PRIVATE + /* A sign-capable build lets an HSS key object be created empty, because + * C_GenerateKeyPair builds both objects from templates that carry no key + * material and fills them in afterwards. C_CreateObject has no such second + * step, so an HSS public key imported without CKA_VALUE would be a + * permanently unusable object whose first C_Verify fails inside wolfSSL. + * Require the value here, matching what a verify-only build enforces in + * WP11_Object_SetHssKey. */ + if (objType == CKK_HSS && objectClass == CKO_PUBLIC_KEY) { + CK_ATTRIBUTE_PTR value = NULL; + + FindAttributeType(pTemplate, ulCount, CKA_VALUE, &value); + if (value == NULL || value->pValue == NULL || value->ulValueLen == 0) + return CKR_ATTRIBUTE_VALUE_INVALID; + } +#endif + rv = NewObject(session, objType, objectClass, pTemplate, ulCount, object); return rv; @@ -5054,6 +5071,16 @@ CK_RV C_SignInit(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMechanism, init |= WP11_INIT_MLDSA_SIGN; break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS: + if (type != CKK_HSS) + return CKR_KEY_TYPE_INCONSISTENT; + if (pMechanism->pParameter != NULL || + pMechanism->ulParameterLen != 0) + return CKR_MECHANISM_PARAM_INVALID; + init |= WP11_INIT_HSS_SIGN; + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -5441,6 +5468,43 @@ CK_RV C_Sign(CK_SESSION_HANDLE hSession, CK_BYTE_PTR pData, *pulSignatureLen = sigLen; break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS: + if (!WP11_Session_IsOpInitialized(session, WP11_INIT_HSS_SIGN)) + return CKR_OPERATION_NOT_INITIALIZED; + + sigLen = WP11_Hss_SigLen(obj); + if (sigLen == 0) { + /* Genuine failure - terminate the active signing operation. */ + WP11_Session_SetOpInitialized(session, 0); + return CKR_FUNCTION_FAILED; + } + if (pSignature == NULL) { + /* Length query - operation remains active per PKCS#11. */ + *pulSignatureLen = sigLen; + return CKR_OK; + } + if (sigLen > (word32)*pulSignatureLen) + return CKR_BUFFER_TOO_SMALL; + + ret = WP11_Hss_Sign(pData, (word32)ulDataLen, pSignature, &sigLen, + obj); + if (ret == NOT_AVAILABLE_E) { + /* The key's signature state was poisoned by an earlier + * failure, so its in-memory leaf index can no longer be + * trusted. Every further sign fails this way until the object + * is re-read from the token (C_Logout/C_Login, or restarting + * the application), which restores the state from the durable + * state file. Terminate the operation. */ + WP11_Session_SetOpInitialized(session, 0); + return CKR_DEVICE_ERROR; + } + /* Other failures fall through to the shared cleanup below, which + * clears the op flag and maps ret < 0 to CKR_FUNCTION_FAILED + * (matching the RSA/ECC/ML-DSA arms). */ + *pulSignatureLen = sigLen; + break; +#endif #ifndef NO_HMAC #ifndef NO_MD5 case CKM_MD5_HMAC: @@ -7782,6 +7846,96 @@ CK_RV C_GenerateKey(CK_SESSION_HANDLE hSession, * type of operation. * CKR_OK on success. */ +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Look up an attribute in the public-key template, falling back to the + * private-key one. The v3.3 HSS profile lets the caller supply the key + * parameters on either arm of C_GenerateKeyPair. */ +static CK_ATTRIBUTE_PTR HssFindGenAttr(CK_ATTRIBUTE_PTR pPublicKeyTemplate, + CK_ULONG ulPublicKeyAttributeCount, + CK_ATTRIBUTE_PTR pPrivateKeyTemplate, + CK_ULONG ulPrivateKeyAttributeCount, + CK_ATTRIBUTE_TYPE type) +{ + CK_ATTRIBUTE_PTR attr = NULL; + + FindAttributeType(pPublicKeyTemplate, ulPublicKeyAttributeCount, type, + &attr); + if (attr == NULL) { + FindAttributeType(pPrivateKeyTemplate, ulPrivateKeyAttributeCount, + type, &attr); + } + return attr; +} + +/* True when the template holds CKA_TOKEN with the value CK_TRUE. */ +static int HssTemplateIsToken(CK_ATTRIBUTE_PTR pTemplate, CK_ULONG ulCount) +{ + CK_ATTRIBUTE_PTR attr = NULL; + + FindAttributeType(pTemplate, ulCount, CKA_TOKEN, &attr); + return attr != NULL && attr->pValue != NULL && + attr->ulValueLen == sizeof(CK_BBOOL) && + *(CK_BBOOL*)attr->pValue == CK_TRUE; +} + +/* The private key must be token-resident: a session-only key that vanishes on + * a crash could release a signature whose one-time index was never persisted, + * and the durable state file is only written for token objects. A public key + * holds no state, so it may be session-scoped like any other public key - + * requiring CKA_TOKEN on that arm too would be stricter than every other + * keygen mechanism here, for no safety gain. */ +static CK_RV HssKeyPairGenCheckToken(CK_ATTRIBUTE_PTR pPrivateKeyTemplate, + CK_ULONG ulPrivateKeyAttributeCount) +{ + if (!HssTemplateIsToken(pPrivateKeyTemplate, ulPrivateKeyAttributeCount)) + return CKR_TEMPLATE_INCONSISTENT; + return CKR_OK; +} + +/* Collect the HSS parameter set (CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE and + * CKA_HSS_LMOTS_TYPE) from the key templates. All three are required; each is + * a CK_ULONG-sized value. + * + * @return CKR_TEMPLATE_INCOMPLETE when an attribute is absent. + * @return CKR_ATTRIBUTE_VALUE_INVALID when one has no value or a bad length. + * @return CKR_OK on success. + */ +static CK_RV HssKeyPairGenParams(CK_ATTRIBUTE_PTR pPublicKeyTemplate, + CK_ULONG ulPublicKeyAttributeCount, + CK_ATTRIBUTE_PTR pPrivateKeyTemplate, + CK_ULONG ulPrivateKeyAttributeCount, + CK_ULONG* levels, CK_LMS_TYPE* lmsType, + CK_LMOTS_TYPE* lmotsType) +{ + CK_ATTRIBUTE_PTR aLevels; + CK_ATTRIBUTE_PTR aLms; + CK_ATTRIBUTE_PTR aLmots; + + aLevels = HssFindGenAttr(pPublicKeyTemplate, ulPublicKeyAttributeCount, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_HSS_LEVELS); + aLms = HssFindGenAttr(pPublicKeyTemplate, ulPublicKeyAttributeCount, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_HSS_LMS_TYPE); + aLmots = HssFindGenAttr(pPublicKeyTemplate, ulPublicKeyAttributeCount, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, CKA_HSS_LMOTS_TYPE); + + if (aLevels == NULL || aLms == NULL || aLmots == NULL) + return CKR_TEMPLATE_INCOMPLETE; + if (aLevels->pValue == NULL || + aLevels->ulValueLen != sizeof(CK_ULONG) || + aLms->pValue == NULL || + aLms->ulValueLen != sizeof(CK_ULONG) || + aLmots->pValue == NULL || + aLmots->ulValueLen != sizeof(CK_ULONG)) { + return CKR_ATTRIBUTE_VALUE_INVALID; + } + + *levels = *(CK_ULONG*)aLevels->pValue; + *lmsType = *(CK_LMS_TYPE*)aLms->pValue; + *lmotsType = *(CK_LMOTS_TYPE*)aLmots->pValue; + return CKR_OK; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, CK_MECHANISM_PTR pMechanism, CK_ATTRIBUTE_PTR pPublicKeyTemplate, @@ -7823,7 +7977,7 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, return rv; } /* Only require R/W session for token objects. Each template must be - * inspected independently — a public template with CKA_TOKEN=FALSE must + * inspected independently - a public template with CKA_TOKEN=FALSE must * not mask a private template requesting CKA_TOKEN=TRUE. */ if (!WP11_Session_IsRW(session)) { CK_ATTRIBUTE_PTR tpls[2] = { pPublicKeyTemplate, pPrivateKeyTemplate }; @@ -7926,6 +8080,46 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, } break; #endif +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS_KEY_PAIR_GEN: { + CK_ULONG hssLevels; + CK_LMS_TYPE hssLms; + CK_LMOTS_TYPE hssLmots; + + /* v3.3: the keygen mechanism carries no parameter. */ + if (pMechanism->pParameter != NULL || + pMechanism->ulParameterLen != 0) { + return CKR_MECHANISM_PARAM_INVALID; + } + rv = HssKeyPairGenCheckToken(pPrivateKeyTemplate, + ulPrivateKeyAttributeCount); + if (rv != CKR_OK) + return rv; + + rv = HssKeyPairGenParams(pPublicKeyTemplate, + ulPublicKeyAttributeCount, pPrivateKeyTemplate, + ulPrivateKeyAttributeCount, &hssLevels, &hssLms, &hssLmots); + if (rv != CKR_OK) + return rv; + + *phPublicKey = *phPrivateKey = CK_INVALID_HANDLE; + rv = NewObject(session, CKK_HSS, CKO_PUBLIC_KEY, + pPublicKeyTemplate, ulPublicKeyAttributeCount, &pub); + if (rv == CKR_OK) { + rv = NewObject(session, CKK_HSS, CKO_PRIVATE_KEY, + pPrivateKeyTemplate, ulPrivateKeyAttributeCount, &priv); + } + if (rv == CKR_OK) { + ret = WP11_Hss_GenerateKeyPair(pub, priv, hssLevels, hssLms, + hssLmots, WP11_Session_GetSlot(session)); + if (ret == BAD_FUNC_ARG) + rv = CKR_MECHANISM_PARAM_INVALID; + else if (ret != 0) + rv = CKR_FUNCTION_FAILED; + } + break; + } +#endif #ifdef WOLFPKCS11_MLDSA case CKM_ML_DSA_KEY_PAIR_GEN: if (pMechanism->pParameter != NULL || @@ -8122,6 +8316,11 @@ CK_RV C_GenerateKeyPair(CK_SESSION_HANDLE hSession, if (rv != CKR_OK && priv != NULL) { if (*phPrivateKey != CK_INVALID_HANDLE) WP11_Session_RemoveObject(session, priv); +#ifdef WOLFPKCS11_LMS_PRIVATE + /* An HSS keygen writes a durable state file before this point; if a + * later step failed, remove it so it is not orphaned on disk. */ + WP11_Hss_RemoveOrphanState(priv, WP11_Session_GetSlot(session)); +#endif WP11_Object_Free(priv); } diff --git a/src/hbs.c b/src/hbs.c new file mode 100644 index 00000000..8d5e279c --- /dev/null +++ b/src/hbs.c @@ -0,0 +1,1966 @@ +/* hbs.c + * + * Copyright (C) 2006-2023 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfPKCS11 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Hash-based signature schemes: LMS/HSS (RFC 8554) and XMSS/XMSS^MT + * (RFC 8391), plus the scheme-neutral framework for stateful private keys. + * + * The durable state file itself is storage, so it lives with the rest of the + * store code in internal.c (wp11_Stateful_StateFile_Open / _Remove); this file + * owns what goes into it - the AAD-bound header, the AES-GCM sealing, the + * signature counter and the per-scheme thin wrappers. + */ + +#include "wp11_types.h" +#include "hbs.h" + +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* AES-GCM encrypt with caller-supplied AAD. The tag is appended to the + * ciphertext (out[len .. len+AES_BLOCK_SIZE-1]). Used for the state files so + * the file header (magic, version, parameters, sig count) is bound to the + * ciphertext via authenticated additional data; any tampering with those + * bytes is detected at decrypt time. */ +static int wp11_EncryptDataAAD(byte* out, const byte* data, int len, + const byte* key, int keySz, const byte* iv, int ivSz, + const byte* aad, int aadSz, int devId) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, devId); + if (ret == 0) + ret = wc_AesGcmSetKey(&aes, key, keySz); + if (ret == 0) { + ret = wc_AesGcmEncrypt(&aes, out, data, len, iv, ivSz, out + len, + AES_BLOCK_SIZE, aad, aadSz); + } + wc_AesFree(&aes); + + return ret; +} + +static int wp11_DecryptDataAAD(byte* out, const byte* data, int len, + const byte* key, int keySz, const byte* iv, int ivSz, + const byte* aad, int aadSz, int devId) +{ + Aes aes; + int ret; + + ret = wc_AesInit(&aes, NULL, devId); + if (ret == 0) + ret = wc_AesGcmSetKey(&aes, key, keySz); + if (ret == 0) { + ret = wc_AesGcmDecrypt(&aes, out, data, len, iv, ivSz, data + len, + AES_BLOCK_SIZE, aad, aadSz); + } + wc_AesFree(&aes); + + return ret; +} + +/* =========================================================================== + * Stateful hash-based signature framework + * =========================================================================== + * + * Shared infrastructure for stateful hash-based signature schemes. The first + * concrete user is LMS/HSS (RFC 8554, this file's WOLFPKCS11_LMS_PRIVATE + * block below). Future schemes (XMSS, XMSS^MT - RFC 8391) plug into the same + * framework via thin wrappers. + * + * Contract every stateful-sig scheme MUST honour: + * + * 1. Token-resident only. Session-only stateful keys are unsafe (a process + * crash between sign and cleanup can release a signature whose OTS + * index was never persisted). Reject CKA_TOKEN=FALSE in C_GenerateKeyPair + * with CKR_TEMPLATE_INCONSISTENT. + * + * 2. State file keyed by a per-key 64-bit nonce, NOT by the object's + * position in the token list. The nonce is generated at keygen time + * and stored in the shell file's last 8 bytes. WP11_Object holds a + * copy in `statefulStateId`. Token-list reordering (Add/Remove, + * Slot_Store) must NEVER move or delete the state file. + * + * 3. State persistence is synchronous and durable. The wolfSSL write + * callback writes the encrypted state file with fsync + atomic rename + * + parent-directory fsync (unless WOLFPKCS11_STATEFUL_RELAX_FSYNC=1) + * BEFORE returning success - so wolfSSL never releases a signature + * whose state advance has not reached stable storage. + * + * 4. State file uses AES-GCM (slot-key) with the AAD-bound header below. + * Tampering with parameters or the persisted sigCount fails decrypt. + * + * 5. SigCount tracked in the wp11 layer (`statefulSigCount`), persisted + * inside the AAD-bound header, and used to compute KEYS_REMAINING - + * no reads from wolfSSL key internals (priv_raw layout is not public + * API and varies between backends). + * + * 6. Poison-on-failure. Any state-write failure or sign error clears + * WP11_FLAG_STATEFUL_STATE_VALID; subsequent signs return + * CKR_DEVICE_ERROR until the object is reloaded from disk + * (wolfSSL Reload restores in-memory state from the durable file). + * + * 7. Locking: all post-keygen state-mutating paths run under the token + * lock (priv->lock points at &token->lock for token-resident keys). + * EXCEPTION: during the keygen function itself, priv is thread-local + * (no other thread has the handle yet, and priv->lock has not been + * assigned by WP11_Session_AddObject), so the write callback fires + * unlocked. This is safe only because no concurrent reader can find + * priv before keygen returns. + * + * On-disk layouts: + * + * Shell file (per scheme; non-secret metadata): + * [u32 magic][u32 version][scheme-specific params + pub][u64 stateId] + * The trailer (stateId) is always the last 8 bytes - that's how + * wp11_Stateful_PeekStateIdFromShell finds it without parsing. + * + * State file (AES-GCM, AAD-bound): + * AAD: [u32 magic][u32 version][scheme params (P bytes)][u64 sigCount] + * [u32 ivLen][iv (12 bytes)] + * [u32 ctLen][ciphertext (privSz + 16-byte GCM tag)] + */ + +/* Big-endian byte helpers for encoding/decoding the AAD-bound state header + * and the shell trailer (compiled in sign builds only - the whole framework + * is gated by WOLFPKCS11_STATEFUL_SIG_PRIVATE above). */ +static word32 wp11_Stateful_ReadU32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; +} + +static word64 wp11_Stateful_ReadU64(const byte* p) +{ + return ((word64)p[0] << 56) | ((word64)p[1] << 48) + | ((word64)p[2] << 40) | ((word64)p[3] << 32) + | ((word64)p[4] << 24) | ((word64)p[5] << 16) + | ((word64)p[6] << 8) | (word64)p[7]; +} + +static void wp11_Stateful_WriteU32(byte* p, word32 v) +{ + p[0] = (byte)(v >> 24); + p[1] = (byte)(v >> 16); + p[2] = (byte)(v >> 8); + p[3] = (byte)v; +} + +static void wp11_Stateful_WriteU64(byte* p, word64 v) +{ + p[0] = (byte)(v >> 56); + p[1] = (byte)(v >> 48); + p[2] = (byte)(v >> 40); + p[3] = (byte)(v >> 32); + p[4] = (byte)(v >> 24); + p[5] = (byte)(v >> 16); + p[6] = (byte)(v >> 8); + p[7] = (byte)v; +} + +/* IV length for the AES-GCM-encrypted state file. The full 96-bit GCM + * standard IV; same for every scheme. */ +#define WP11_STATEFUL_IV_LEN 12 + +/* When 1, the state-write callback skips fsync() of the file and its parent + * directory. DANGEROUS: documented as non-production only. Default is to + * always fsync. The setting applies to ALL stateful hash-based signature + * schemes (LMS/HSS today; XMSS in the future) - there is no per-scheme knob, + * since the durability requirement is identical. + * + * Resolved once from WP11_Stateful_InitRelaxFsync() during library init, not + * lazily on first use: signing threads on different slots hold different token + * locks, so a lazy initialiser here would be an unsynchronised write to this + * static, which ThreadSanitizer reports and which no lock in the sign path + * covers. */ +static int wp11_StatefulRelaxFsync = 0; + +void WP11_Stateful_InitRelaxFsync(void) +{ +#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"); + } +} + +static int wp11_StatefulShouldFsync(void) +{ + return wp11_StatefulRelaxFsync ? 0 : 1; +} + +/* Sanity caps for inputs to the generic state-blob helpers. The wolfSSL + * state buffer for any practical scheme is well below 1 MiB; the scheme + * parameter region is small (HSS = 12 bytes, XMSS = 4-12 bytes). Reject + * absurd inputs up front so the size-arithmetic below cannot overflow. */ +#define WP11_STATEFUL_PRIV_MAX (1U << 20) /* 1 MiB */ +#define WP11_STATEFUL_PARAMS_MAX 256U + +/* Persist the encrypted state file durably. Caller passes the scheme's + * (schemeFilePrefix, magic, version) and a packed scheme-params buffer + * that is bound into the GCM AAD. On a successful return the state file + * at (slot_id, statefulStateId) is fsync'd and renamed atomically. + * + * AAD-bound layout (caught by AES-GCM tag): + * [u32 magic][u32 version][u32 schemeParamLen][schemeParamBytes] + * [u64 sigCount] + * Including schemeParamLen in the AAD diagnoses thin-wrapper version skew + * cleanly (a length mismatch surfaces as a header-mismatch BAD_FUNC_ARG + * before decrypt rather than as opaque AES-GCM tag failure). */ +static int wp11_Stateful_WriteStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* schemeParamBytes, word32 schemeParamLen, + const byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* hdr = NULL; + word32 hdrLen; + byte* ct = NULL; + word32 ctLen; + int tokenId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + /* Stateful keys are token-resident only - reject any attempt to + * persist state for a session-only object. The HSS write CB enforces + * this too, but the framework helper enforces it for all schemes. */ + if (!o->onToken) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (schemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + /* Defensive caps: prevent size-arithmetic overflow. */ + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; + + ctLen = privSz + AES_BLOCK_SIZE; /* +16 for GCM tag */ + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) + return MEMORY_E; + + wp11_Stateful_WriteU32(hdr + 0, magic); + wp11_Stateful_WriteU32(hdr + 4, version); + wp11_Stateful_WriteU32(hdr + 8, schemeParamLen); + if (schemeParamLen > 0) + XMEMCPY(hdr + 12, schemeParamBytes, schemeParamLen); + wp11_Stateful_WriteU64(hdr + 12 + schemeParamLen, o->statefulSigCount); + + /* Fresh GCM nonce per write (NEVER reuse object->iv). */ + ret = WP11_Slot_GenerateRandom(o->slot, iv, WP11_STATEFUL_IV_LEN); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return MEMORY_E; + } + + ret = wp11_EncryptDataAAD(ct, priv, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); + if (ret != 0) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 0, &storage); + if (ret == 0) { + if (wp11_StatefulShouldFsync()) + wp11_storage_set_durable(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 = wp11_storage_close_report(storage); + if (ret == 0) + ret = closeRet; + } + } + + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return ret; +} + +/* Read and decrypt the state file. The full header is verified as AAD by + * the AES-GCM tag; tampering with magic, version, scheme params, scheme- + * params length, or the persisted sigCount fails decrypt. The expected + * scheme params are passed by the caller (typically: re-pack from the + * in-memory wolfSSL key); a mismatch returns BAD_FUNC_ARG before decrypt. + * The decrypted sigCount is restored into o->statefulSigCount. */ +static int wp11_Stateful_ReadStateBlob(WP11_Object* o, + const char* schemeFilePrefix, word32 magic, word32 version, + const byte* expectedSchemeParamBytes, word32 schemeParamLen, + byte* priv, word32 privSz) +{ + int ret; + void* storage = NULL; + byte* hdr = NULL; + word32 hdrLen; + byte iv[WP11_STATEFUL_IV_LEN]; + byte* ct = NULL; + word32 m, v, persistedParamLen, ivLen, ctLen; + word64 sigCount = 0; + int tokenId; + + if (o == NULL || o->slot == NULL || priv == NULL || privSz == 0) + return BAD_FUNC_ARG; + if (o->statefulStateId == 0) + return BAD_FUNC_ARG; + if (expectedSchemeParamBytes == NULL && schemeParamLen != 0) + return BAD_FUNC_ARG; + if (privSz > WP11_STATEFUL_PRIV_MAX) + return BAD_FUNC_ARG; + if (schemeParamLen > WP11_STATEFUL_PARAMS_MAX) + return BAD_FUNC_ARG; + + hdrLen = 4 + 4 + 4 + schemeParamLen + 8; + hdr = (byte*)XMALLOC(hdrLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (hdr == NULL) { + wc_ForceZero(priv, privSz); + return MEMORY_E; + } + + tokenId = (int)o->slot->id; + ret = wp11_Stateful_StateFile_Open(schemeFilePrefix, tokenId, + o->statefulStateId, 1, &storage); + if (ret != 0) { + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wc_ForceZero(priv, privSz); + return ret; + } + + ret = wp11_storage_read(storage, hdr, (int)hdrLen); + if (ret == 0) { + m = wp11_Stateful_ReadU32(hdr); + v = wp11_Stateful_ReadU32(hdr + 4); + persistedParamLen = wp11_Stateful_ReadU32(hdr + 8); + sigCount = wp11_Stateful_ReadU64(hdr + 12 + schemeParamLen); + if (m != magic || v != version) { + ret = BAD_FUNC_ARG; + } + else if (persistedParamLen != schemeParamLen) { + /* Scheme-params length mismatch - diagnoses thin-wrapper + * version skew before AES-GCM tag failure. */ + ret = BAD_FUNC_ARG; + } + else if (schemeParamLen > 0 && + XMEMCMP(hdr + 12, expectedSchemeParamBytes, schemeParamLen) + != 0) { + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ivLen = wp11_Stateful_ReadU32(buf); + if (ivLen != WP11_STATEFUL_IV_LEN) + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) + ret = wp11_storage_read(storage, iv, WP11_STATEFUL_IV_LEN); + if (ret == 0) { + byte buf[4]; + ret = wp11_storage_read(storage, buf, 4); + if (ret == 0) { + ctLen = wp11_Stateful_ReadU32(buf); + if (ctLen < AES_BLOCK_SIZE || + ctLen - AES_BLOCK_SIZE != privSz) { + ret = BAD_FUNC_ARG; + } + } + } + if (ret == 0) { + ct = (byte*)XMALLOC(ctLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) + ret = MEMORY_E; + } + if (ret == 0) + ret = wp11_storage_read(storage, ct, (int)ctLen); + if (ret == 0) { + ret = wp11_DecryptDataAAD(priv, ct, (int)privSz, + o->slot->token.key, (int)sizeof(o->slot->token.key), + iv, WP11_STATEFUL_IV_LEN, hdr, (int)hdrLen, o->devId); + /* AES-GCM authentication failure manifests here. */ + } + wp11_storage_close(storage); + if (ct != NULL) { + wc_ForceZero(ct, ctLen); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + XFREE(hdr, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ret == 0) { + /* AES-GCM verified the header; sigCount cannot have been rolled + * back without detection. Restore into the in-memory object. */ + o->statefulSigCount = sigCount; + } + if (ret != 0) + wc_ForceZero(priv, privSz); + return ret; +} + +/* Recover the per-key stateId from the on-disk shell file. The shell + * format is otherwise scheme-specific, but by contract the LAST 8 bytes + * are always the u64 stateId. Used by Unstore to find the state file + * before deleting it, when the in-memory object hasn't been decoded yet. + * + * Validates the shell's magic + version against caller-supplied values + * before returning the trailer. Without this check, a truncated, replaced, + * or wrong-scheme file at the same (tokenId, objId) would yield a garbage + * stateId that the caller then passes to Store_Remove - silently no-op'ing + * or deleting the wrong file while the real state file orphans on disk. + * + * Returns BAD_FUNC_ARG if magic or version don't match (e.g., wrong scheme, + * file truncated below the header size, or the file is something else). */ +static int wp11_Stateful_PeekStateIdFromShell(int tokenId, int objId, + int storeTypeShell, word32 expectedShellMagic, word32 expectedShellVersion, + word64* outStateId) +{ + int ret; + void* storage = NULL; + unsigned char* buf = NULL; + int bufLen = 0; + + if (outStateId == NULL) + return BAD_FUNC_ARG; + *outStateId = 0; + + ret = wp11_storage_open_readonly(storeTypeShell, + (CK_ULONG)tokenId, (CK_ULONG)objId, &storage); + if (ret != 0) + return ret; + ret = wp11_storage_read_alloc_array(storage, &buf, &bufLen); + wp11_storage_close(storage); + if (ret != 0) + return ret; + + /* Need at least [u32 magic][u32 version] + [u64 stateId trailer]. */ + if (bufLen < 16) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + if (wp11_Stateful_ReadU32(buf) != expectedShellMagic || + wp11_Stateful_ReadU32(buf + 4) != expectedShellVersion) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return BAD_FUNC_ARG; + } + *outStateId = wp11_Stateful_ReadU64(buf + (bufLen - 8)); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return 0; +} +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE */ + +#ifdef WOLFPKCS11_LMS +/* =========================================================================== + * LMS/HSS (RFC 8554): verification and public-key import. + * + * Verify-only: this build imports raw HSS public keys and verifies + * signatures. There is no private-key/keygen/sign or persisted signature + * state here. The public key is self-describing, so wolfSSL derives the + * parameter set from the key bytes on import; the CKA_HSS_LEVELS / + * CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes are optional and, when + * supplied, are validated against the imported key. + * ========================================================================= */ + +/* Map tree height -> RFC 8554 LMS typecode. Returns 0 on unknown height. */ +static CK_LMS_TYPE wp11_HssHeightToLmsType(int h) +{ + switch (h) { + case 5: return CKL_LMS_SHA256_M32_H5; + case 10: return CKL_LMS_SHA256_M32_H10; + case 15: return CKL_LMS_SHA256_M32_H15; + case 20: return CKL_LMS_SHA256_M32_H20; + case 25: return CKL_LMS_SHA256_M32_H25; + default: return 0; + } +} + +/* Map Winternitz -> LMOTS typecode. Returns 0 on unknown. */ +static CK_LMOTS_TYPE wp11_HssWToLmotsType(int w) +{ + switch (w) { + case 1: return CKL_LMOTS_SHA256_N32_W1; + case 2: return CKL_LMOTS_SHA256_N32_W2; + case 4: return CKL_LMOTS_SHA256_N32_W4; + case 8: return CKL_LMOTS_SHA256_N32_W8; + default: return 0; + } +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Forward parameter maps (RFC 8554 typecode -> height / Winternitz), used by + * keygen to translate the CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes. + * Return 0 on unknown. */ +static int wp11_HssLmsTypeToHeight(CK_LMS_TYPE t) +{ + switch (t) { + case CKL_LMS_SHA256_M32_H5: return 5; + case CKL_LMS_SHA256_M32_H10: return 10; + case CKL_LMS_SHA256_M32_H15: return 15; + case CKL_LMS_SHA256_M32_H20: return 20; + case CKL_LMS_SHA256_M32_H25: return 25; + default: return 0; + } +} + +static int wp11_HssLmotsTypeToW(CK_LMOTS_TYPE t) +{ + switch (t) { + case CKL_LMOTS_SHA256_N32_W1: return 1; + case CKL_LMOTS_SHA256_N32_W2: return 2; + case CKL_LMOTS_SHA256_N32_W4: return 4; + case CKL_LMOTS_SHA256_N32_W8: return 8; + default: return 0; + } +} + +/* On-disk magic + version for the HSS private-key shell file: non-secret + * metadata (parameters + cached public key) with the 8-byte stateId nonce as + * the last 8 bytes. Persisted at keygen so token load can call + * wc_LmsKey_SetParameters before wc_LmsKey_Reload and locate the state file. */ +#define WP11_HSS_SHELL_MAGIC 0x48535350UL /* "HSSP" */ +#define WP11_HSS_SHELL_VERSION 1U +/* Fixed part of the shell, ahead of the public key: magic, version, levels, + * height, winternitz and the public-key length, each a big-endian u32. */ +#define WP11_HSS_SHELL_HDR_LEN (6 * 4) +/* The stateId trailer that wp11_Stateful_PeekStateIdFromShell relies on being + * the last bytes of the shell. */ +#define WP11_HSS_SHELL_TRAILER_LEN 8 + +/* On-disk magic + version for the encrypted HSS state file. The header + * (levels/height/winternitz + persisted signature counter) is bound into the + * AES-GCM tag via AAD, so tampering is detected at decrypt time. */ +#define WP11_HSS_STATE_MAGIC 0x48535353UL /* "HSSS" */ +#define WP11_HSS_STATE_VERSION 1U + +/* HSS scheme param region inside the AAD-bound state header: 3 u32s. */ +#define WP11_HSS_SCHEME_PARAM_LEN 12U + +/* Serialize the private-key shell header: + * [u32 magic][u32 version][u32 levels][u32 height][u32 winternitz] + * [u32 pubLen][pub]. The 8-byte stateId trailer is appended by the caller. + * Returns bytes written, or BUFFER_E. */ +static int wp11_HssPackShellHeader(byte* out, word32 outLen, int levels, + int height, int winternitz, const byte* pub, word32 pubLen) +{ + word32 needed = WP11_HSS_SHELL_HDR_LEN + pubLen; + word32 idx = 0; + + if (out == NULL || outLen < needed) + return BUFFER_E; + wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_MAGIC); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)WP11_HSS_SHELL_VERSION); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)levels); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)height); idx += 4; + wp11_Stateful_WriteU32(out + idx, (word32)winternitz); idx += 4; + wp11_Stateful_WriteU32(out + idx, pubLen); idx += 4; + if (pub != NULL && pubLen > 0) + XMEMCPY(out + idx, pub, pubLen); + idx += pubLen; + return (int)idx; +} + +/* Parse the private-key shell header. On success *pub points into the input + * buffer (no allocation) and *pubLen is its length. */ +static int wp11_HssParseShellHeader(const byte* in, word32 inLen, + int* levels, int* height, int* winternitz, + const byte** pub, word32* pubLen) +{ + word32 idx = 0; + word32 magic, version, pl; + + if (in == NULL || inLen < WP11_HSS_SHELL_HDR_LEN) + return BAD_FUNC_ARG; + magic = wp11_Stateful_ReadU32(in + idx); idx += 4; + version = wp11_Stateful_ReadU32(in + idx); idx += 4; + if (magic != WP11_HSS_SHELL_MAGIC || version != WP11_HSS_SHELL_VERSION) + return BAD_FUNC_ARG; + *levels = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *height = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + *winternitz = (int)wp11_Stateful_ReadU32(in + idx); idx += 4; + pl = wp11_Stateful_ReadU32(in + idx); idx += 4; + /* Cap pubLen against the max HSS public-key size so a malformed shell + * cannot force a giant ImportPubRaw. */ + if (pl > HSS_MAX_PUBLIC_KEY_LEN) + return BAD_FUNC_ARG; + if (idx + pl > inLen) + return BAD_FUNC_ARG; + *pub = in + idx; + *pubLen = pl; + return 0; +} + +/* Pack the HSS scheme params (levels|height|winternitz, big-endian u32s) into + * the AAD-bound region of the state header. */ +static void wp11_Hss_PackSchemeParams(byte out[WP11_HSS_SCHEME_PARAM_LEN], + int levels, int height, int winternitz) +{ + wp11_Stateful_WriteU32(out + 0, (word32)levels); + wp11_Stateful_WriteU32(out + 4, (word32)height); + wp11_Stateful_WriteU32(out + 8, (word32)winternitz); +} + +/* HSS state-file write, keyed on disk by (slot_id, statefulStateId). Thin + * wrapper over the generic durable state-blob writer. */ +static int wp11_Hss_WriteStateBlob(WP11_Object* o, const byte* priv, + word32 privSz) +{ + int levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_WriteStateBlob(o, "hsskey", + (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); +} + +/* HSS state-file read. Validates the in-memory key's params against the + * AAD-bound header before AES-GCM decrypt. */ +static int wp11_Hss_ReadStateBlob(WP11_Object* o, byte* priv, word32 privSz) +{ + int levels = 0, height = 0, winternitz = 0; + byte schemeParams[WP11_HSS_SCHEME_PARAM_LEN]; + int ret; + + if (o == NULL || o->data.lmsKey == NULL) + return BAD_FUNC_ARG; + ret = wc_LmsKey_GetParameters(o->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + wp11_Hss_PackSchemeParams(schemeParams, levels, height, winternitz); + return wp11_Stateful_ReadStateBlob(o, "hsskey", + (word32)WP11_HSS_STATE_MAGIC, WP11_HSS_STATE_VERSION, + schemeParams, WP11_HSS_SCHEME_PARAM_LEN, priv, privSz); +} + +/* wolfSSL write callback: invoked by wc_LmsKey_MakeKey and wc_LmsKey_Sign + * after each state advance. Must return WC_LMS_RC_SAVED_TO_NV_MEMORY on + * success; any other value aborts the sign (wolfSSL zeroizes the signature). */ +static int wp11_Hss_WriteState_Cb(const byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + + if (o == NULL || priv == NULL || privSz == 0) + return WC_LMS_RC_BAD_ARG; + /* Stateful HSS keys are always token-resident in this build. */ + if (!o->onToken || o->statefulStateId == 0) + return WC_LMS_RC_WRITE_FAIL; + if (wp11_Hss_WriteStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_WRITE_FAIL; + return WC_LMS_RC_SAVED_TO_NV_MEMORY; +} + +static int wp11_Hss_ReadState_Cb(byte* priv, word32 privSz, void* ctx) +{ + WP11_Object* o = (WP11_Object*)ctx; + + if (o == NULL) + return WC_LMS_RC_BAD_ARG; + if (wp11_Hss_ReadStateBlob(o, priv, privSz) != 0) + return WC_LMS_RC_READ_FAIL; + return WC_LMS_RC_READ_TO_MEMORY; +} + +/* Recover the statefulStateId from the on-disk HSS shell (last 8 bytes), + * validating the shell magic+version first. Used by orphan cleanup when the + * object has not been decoded into memory. */ +static int wp11_Hss_PeekStateIdFromShell(int tokenId, int objId, + word64* outStateId) +{ + return wp11_Stateful_PeekStateIdFromShell(tokenId, objId, + WOLFPKCS11_STORE_HSSKEY_PRIV, (word32)WP11_HSS_SHELL_MAGIC, + WP11_HSS_SHELL_VERSION, outStateId); +} + +/* Delete the signature-state file backing a stored HSS private key, called + * when the object itself is removed from the token. The per-key nonce comes + * from the in-memory object when it has been decoded, otherwise from the + * on-disk shell. Best effort: a state file that cannot be located or removed + * must not stop the object from being destroyed. */ +void wp11_Hss_RemoveStoredState(WP11_Object* object, int tokenId, int objId) +{ + word64 stateId; + + if (object == NULL) + return; + + stateId = object->statefulStateId; + if (stateId == 0) + (void)wp11_Hss_PeekStateIdFromShell(tokenId, objId, &stateId); + if (stateId != 0) + (void)wp11_Stateful_StateFile_Remove("hsskey", tokenId, stateId); +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Import a raw HSS public key into the given wolfSSL LmsKey. wolfSSL derives + * the parameter set from the key bytes (RFC 8554 self-describing public key), + * so no parameters need to be set first. On failure the (partially + * initialized) key is NOT freed here; the caller owns it. */ +static int wp11_HssImportPub(LmsKey* key, int devId, const byte* pub, + word32 pubLen) +{ + int ret; + + ret = wc_LmsKey_Init(key, NULL, devId); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(key, pub, pubLen); + return ret; +} + +/* Validate caller-supplied HSS parameter attributes against the parameters + * wolfSSL derived from the just-imported public key. Per the PKCS#11 v3.3 HSS + * profile the public-key parameters are the individual CK_ULONG attributes + * CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE (top-level tree height encoding) and + * CKA_HSS_LMOTS_TYPE (Winternitz encoding). Each is optional; a supplied value + * that does not match the key is rejected with BAD_FUNC_ARG. levelsAttr, + * lmsAttr and lmotsAttr are the raw attribute buffers (NULL when absent) with + * lengths levelsLen/lmsLen/lmotsLen. */ +static int wp11_HssCheckParamAttrs(LmsKey* key, + const byte* levelsAttr, word32 levelsLen, + const byte* lmsAttr, word32 lmsLen, + const byte* lmotsAttr, word32 lmotsLen) +{ + int ret, levels = 0, height = 0, winternitz = 0; + CK_ULONG v; + + ret = wc_LmsKey_GetParameters(key, &levels, &height, &winternitz); + if (ret != 0) + return ret; + if (levelsAttr != NULL) { + if (levelsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, levelsAttr, sizeof(v)); + if (v != (CK_ULONG)levels) + return BAD_FUNC_ARG; + } + if (lmsAttr != NULL) { + if (lmsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, lmsAttr, sizeof(v)); + if (v != (CK_ULONG)wp11_HssHeightToLmsType(height)) + return BAD_FUNC_ARG; + } + if (lmotsAttr != NULL) { + if (lmotsLen != sizeof(v)) + return BAD_FUNC_ARG; + XMEMCPY(&v, lmotsAttr, sizeof(v)); + if (v != (CK_ULONG)wp11_HssWToLmotsType(winternitz)) + return BAD_FUNC_ARG; + } + return 0; +} + +/* Serialize the public key: keyData holds the raw RFC 8554 HSS public key. */ +int wp11_Object_Encode_HssKey(WP11_Object* object) +{ + int ret; + word32 pubLen = 0; + + if (object->objClass == CKO_PUBLIC_KEY) { + /* Public key: raw RFC 8554 public key, unencrypted (Step-1 format). */ + ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); + if (ret == 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, object->keyData, + &pubLen); + if (ret == 0) + object->keyDataLen = (int)pubLen; + } + /* On failure release the buffer so a later store does not skip + * re-encoding and persist an uninitialised buffer. Public key material + * only, so no zeroization needed. */ + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + /* Private key: a non-secret metadata shell (params + cached public + * key + 8-byte stateId trailer). The secret one-time-signature state + * is NEVER written to keyData - it lives only in the AES-GCM state + * file written by the wolfSSL write callback. + * + * The shell is immutable after keygen. Once materialized (built at + * keygen, or loaded from disk), reuse it: a reloaded signing key + * cannot re-export its public root (wolfSSL keeps a key in either + * signing or verify state), so rebuilding here would fail. */ + int levels = 0, height = 0, winternitz = 0; + byte pub[HSS_MAX_PUBLIC_KEY_LEN]; + int hdrLen; + word32 needed; + + if (object->keyData != NULL && object->keyDataLen > 0) + return 0; + + pubLen = sizeof(pub); + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, + &winternitz); + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, pub, &pubLen); + if (ret == 0 && object->statefulStateId == 0) + ret = BAD_FUNC_ARG; + if (ret == 0) { + needed = WP11_HSS_SHELL_HDR_LEN + pubLen + + WP11_HSS_SHELL_TRAILER_LEN; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(needed, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + hdrLen = wp11_HssPackShellHeader(object->keyData, needed, levels, + height, winternitz, pub, pubLen); + if (hdrLen < 0) { + ret = hdrLen; + } + else { + wp11_Stateful_WriteU64(object->keyData + hdrLen, + object->statefulStateId); + object->keyDataLen = hdrLen + 8; + } + } + wc_ForceZero(pub, sizeof(pub)); + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; + } +#endif + return NOT_AVAILABLE_E; +} + +/* Reconstruct the live wolfSSL key from keyData. Public keys import the raw + * key directly; private keys parse the shell, reload the durable signature + * state (via the read callback), and mark the state valid. */ +int wp11_Object_Decode_HssKey(WP11_Object* object) +{ + int ret; + + if (object->objClass == CKO_PUBLIC_KEY) { + /* Raw RFC 8554 public key; wolfSSL re-derives the parameter set. */ + ret = wp11_HssImportPub(object->data.lmsKey, object->devId, + object->keyData, (word32)object->keyDataLen); + object->encoded = (ret != 0) ? 1 : 0; + return ret; + } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + int levels = 0, height = 0, winternitz = 0; + const byte* pub = NULL; + word32 pubLen = 0; + word32 idx = 0; + + ret = wp11_HssParseShellHeader(object->keyData, + (word32)object->keyDataLen, &levels, &height, &winternitz, + &pub, &pubLen); + if (ret == 0) { + idx = WP11_HSS_SHELL_HDR_LEN + pubLen; /* trailer follows pub */ + if (idx + WP11_HSS_SHELL_TRAILER_LEN > (word32)object->keyDataLen) + ret = BAD_FUNC_ARG; + } + if (ret == 0) { + object->statefulStateId = + wp11_Stateful_ReadU64(object->keyData + idx); + if (object->statefulStateId == 0) + ret = BAD_FUNC_ARG; + } + /* INVALID_DEVID unconditionally (not object->devId): HSS stateful + * signing is software-only by design - HSM/device-backed LMS is out of + * scope. This must hold even when the slot has a real devId (a hardware- + * offload build), so it is pinned here rather than inheriting the slot + * default. A real devId would route to the crypto callback and make + * wc_LmsKey_Reload treat the key as HSM-backed and skip the read + * callback (a no-op), leaving the reloaded key with no private state - + * any later Sign would then reuse leaf index 0 or crash. */ + if (ret == 0) + ret = wc_LmsKey_Init(object->data.lmsKey, NULL, INVALID_DEVID); + if (ret == 0) + ret = wc_LmsKey_SetParameters(object->data.lmsKey, levels, height, + winternitz); + /* Wire callbacks before Reload: Reload reads the encrypted state via + * the read callback; the write callback is needed for later signing. */ + if (ret == 0) + ret = wc_LmsKey_SetWriteCb(object->data.lmsKey, + wp11_Hss_WriteState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetReadCb(object->data.lmsKey, + wp11_Hss_ReadState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetContext(object->data.lmsKey, object); + if (ret == 0) + ret = wc_LmsKey_Reload(object->data.lmsKey); + /* Do NOT ImportPubRaw here: wolfSSL keeps an LmsKey in either signing + * (reloaded) or verify (pub) state, not both, and rejects a pub import + * after Reload with BAD_STATE_E. The public root is not needed for + * signing, and the cached shell (kept in keyData) preserves it for + * re-storage. */ + (void)pub; + if (ret == 0) { + object->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + object->encoded = 0; + } + else { + /* Clear the validity flag so a later Sign cannot use a half-built + * key, and free the wolfSSL key. encoded=1 stops Object_Free from + * freeing the wolfSSL key a second time. */ + object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + wc_LmsKey_Free(object->data.lmsKey); + object->encoded = 1; + } + return ret; + } +#endif + return NOT_AVAILABLE_E; +} + +int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; +#endif + ret = wp11_storage_open_readonly(storeType, tokenId, objId, &storage); + if (ret == 0) { + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret = 0; + void* storage = NULL; + int storeType = WOLFPKCS11_STORE_HSSKEY_PUB; + +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + storeType = WOLFPKCS11_STORE_HSSKEY_PRIV; +#endif + if (object->keyData == NULL) + ret = wp11_Object_Encode_HssKey(object); + if (ret == 0) + ret = wp11_storage_open(storeType, tokenId, objId, + object->keyDataLen, &storage); + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +/* Import an HSS public key from a C_CreateObject template. Following the + * PKCS#11 v3.3 HSS profile the collected attributes are data[0]=CKA_HSS_LEVELS, + * data[1]=CKA_HSS_LMS_TYPE, data[2]=CKA_HSS_LMOTS_TYPE (all optional CK_ULONG) + * and data[3]=CKA_VALUE, the raw RFC 8554 public key. wolfSSL derives the + * parameter set from the key bytes; any supplied CKA_HSS_* attribute is + * validated against the derived parameters and a mismatch is rejected. + * Private-key import is rejected: caller-supplied stateful private material is + * a forgery vector and out of scope for verify-only support. */ +int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) +{ + int ret = 0; + LmsKey* newKey = NULL; + + if (object == NULL || data == NULL || len == NULL) + return BAD_FUNC_ARG; +#ifdef WOLFPKCS11_LMS_PRIVATE + /* Keygen (C_GenerateKeyPair) creates both objects with no CKA_VALUE and + * populates them in WP11_Hss_GenerateKeyPair, so a missing value is a + * deferred keygen object, not an error. A private key must never be + * imported WITH a value: caller-supplied one-time-signature state is a + * forgery vector. */ + if (object->objClass == CKO_PRIVATE_KEY) { + if (data[3] != NULL) + return BAD_FUNC_ARG; + return 0; + } + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + if (data[3] == NULL) + return 0; +#else + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + /* A public key must carry its value. There is no keygen path in a + * verify-only build that would populate it later, so an absent + * CKA_VALUE is an incomplete template rather than a deferred import; + * reject it instead of creating an unusable object. */ + if (data[3] == NULL) + return BAD_FUNC_ARG; +#endif + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + /* Import into a temporary key and swap it in only after it fully validates, + * so a failed re-import (C_SetAttributeValue with a new CKA_VALUE) leaves + * the existing key unchanged as PKCS#11 requires. */ + newKey = (LmsKey*)XMALLOC(sizeof(LmsKey), NULL, DYNAMIC_TYPE_KEY); + if (newKey == NULL) + ret = MEMORY_E; + if (ret == 0) { + XMEMSET(newKey, 0, sizeof(LmsKey)); + ret = wp11_HssImportPub(newKey, object->devId, data[3], + (word32)len[3]); + } + if (ret == 0) { + ret = wp11_HssCheckParamAttrs(newKey, data[0], (word32)len[0], + data[1], (word32)len[1], data[2], (word32)len[2]); + } + if (ret == 0) { + /* Replace the live key and drop the cached serialization so a later + * store re-encodes from the new key instead of persisting stale bytes. */ + if (object->data.lmsKey != NULL) { + if (!object->encoded) + wc_LmsKey_Free(object->data.lmsKey); + XFREE(object->data.lmsKey, NULL, DYNAMIC_TYPE_KEY); + } + object->data.lmsKey = newKey; + object->encoded = 0; + newKey = NULL; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + if (newKey != NULL) { + wc_LmsKey_Free(newKey); + XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); + } + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + return ret; +} + +/* Verify an HSS signature over a raw message. Stateless on the verifier. */ +int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, + word32 dataLen, int* stat, WP11_Object* pub) +{ + int ret; + word32 expSigLen = 0; + + if (pub == NULL || pub->data.lmsKey == NULL || stat == NULL) + return BAD_FUNC_ARG; + + /* Verification only reads the key: wc_LmsKey_Verify works from a locally + * allocated LmsState, so a shared RO lock is safe for concurrent verifies + * on the same token object. */ + if (pub->onToken) + WP11_Lock_LockRO(pub->lock); + + /* A signature whose length does not match the key's parameter set cannot + * be valid; report it as an invalid signature (stat = 0 -> + * CKR_SIGNATURE_INVALID) rather than letting wolfSSL's length check + * surface as a function failure. */ + if (wc_LmsKey_GetSigLen(pub->data.lmsKey, &expSigLen) == 0 && + sigLen != expSigLen) { + *stat = 0; + ret = 0; + } + else { + /* Unlike XMSS, whose wolfSSL verify rejects a zero-length message with + * BAD_FUNC_ARG, wc_LmsKey_Verify accepts an empty message, so HSS needs + * no dataLen == 0 special-case here (see WP11_Xmss_Verify). */ + ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, + (int)dataLen); + /* Only a genuine verification mismatch (SIG_VERIFY_E) maps to stat=0. + * Remaining argument/state errors surface as a function failure so + * they are not silently reported as an invalid signature. */ + if (ret == 0) { + *stat = 1; + } + else if (ret == SIG_VERIFY_E) { + *stat = 0; + ret = 0; + } + else { + *stat = 0; + } + } + + if (pub->onToken) + WP11_Lock_UnlockRO(pub->lock); + + return ret; +} + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Generate an HSS key pair. levels/lmsType/lmotsType come from the v3.3 + * keygen template attributes; wolfSSL requires uniform parameters across + * levels. The private key's one-time-signature state is durably persisted at + * genesis (via the write callback invoked inside wc_LmsKey_MakeKey) before + * this returns success. */ +int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + CK_ULONG levels, CK_LMS_TYPE lmsType, + CK_LMOTS_TYPE lmotsType, WP11_Slot* slot) +{ + int ret; + int nLevels, height, winternitz; + WC_RNG rng; + byte pubBuf[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubLen = sizeof(pubBuf); + int stateWritten = 0; + + if (pub == NULL || priv == NULL || slot == NULL || + pub->data.lmsKey == NULL || priv->data.lmsKey == NULL) { + return BAD_FUNC_ARG; + } + + /* Translate the v3.3 typecodes to wolfSSL (levels, height, winternitz), + * enforcing the wolfSSL level limit. */ + nLevels = (int)levels; + height = wp11_HssLmsTypeToHeight(lmsType); + winternitz = wp11_HssLmotsTypeToW(lmotsType); + if (nLevels < 1 || nLevels > LMS_MAX_LEVELS || height == 0 || + winternitz == 0) { + return BAD_FUNC_ARG; + } + + /* Generate the per-key 64-bit nonce keying the state file. Must be + * non-zero (0 is the "none" sentinel); retry on the vanishing chance of + * zero, else treat as an RNG failure. */ + { + byte nonceBuf[8]; + int tries; + for (tries = 0; tries < 4; tries++) { + ret = WP11_Slot_GenerateRandom(slot, nonceBuf, sizeof(nonceBuf)); + if (ret != 0) + return ret; + priv->statefulStateId = wp11_Stateful_ReadU64(nonceBuf); + if (priv->statefulStateId != 0) + break; + } + if (priv->statefulStateId == 0) + return RNG_FAILURE_E; + } + + /* Mark on-token so the write CB commits; AddObject formalizes the + * registration shortly. priv->lock is assigned later by Session_AddObject; + * during keygen no other thread holds a handle, so the CB fires unlocked. */ + priv->onToken = 1; + priv->statefulSigCount = 0; + + /* INVALID_DEVID unconditionally (not priv->devId): HSS stateful signing is + * software-only by design - HSM/device-backed LMS is out of scope - so this + * is pinned even when the slot has a real devId (a hardware-offload build), + * mirroring the reload path in wp11_Object_Decode_HssKey. A real devId would + * route MakeKey/Sign through the crypto callback and, critically, make a + * later wc_LmsKey_Reload a no-op, so the persisted state could never be + * restored. */ + ret = wc_LmsKey_Init(priv->data.lmsKey, NULL, INVALID_DEVID); + if (ret == 0) + ret = wc_LmsKey_SetParameters(priv->data.lmsKey, nLevels, height, + winternitz); + /* Wire callbacks BEFORE MakeKey so genesis state is persisted durably. */ + if (ret == 0) + ret = wc_LmsKey_SetWriteCb(priv->data.lmsKey, wp11_Hss_WriteState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetReadCb(priv->data.lmsKey, wp11_Hss_ReadState_Cb); + if (ret == 0) + ret = wc_LmsKey_SetContext(priv->data.lmsKey, priv); + + if (ret == 0) + ret = Rng_New(&slot->token.rng, &slot->token.rngLock, &rng); + if (ret == 0) { + /* MakeKey invokes the write CB to persist genesis state; on success + * the genesis state file is durable on disk. */ + ret = wc_LmsKey_MakeKey(priv->data.lmsKey, &rng); + Rng_Free(&rng); + if (ret == 0) + stateWritten = 1; + } + + /* Mirror parameters + public key into the public-key object. */ + if (ret == 0) + ret = wc_LmsKey_ExportPubRaw(priv->data.lmsKey, pubBuf, &pubLen); + if (ret == 0) + ret = wc_LmsKey_Init(pub->data.lmsKey, NULL, pub->devId); + if (ret == 0) + ret = wc_LmsKey_SetParameters(pub->data.lmsKey, nLevels, height, + winternitz); + if (ret == 0) + ret = wc_LmsKey_ImportPubRaw(pub->data.lmsKey, pubBuf, pubLen); + + if (ret == 0) { + priv->local = pub->local = 1; + priv->keyGenMech = pub->keyGenMech = CKM_HSS_KEY_PAIR_GEN; + priv->opFlag |= WP11_FLAG_STATEFUL_STATE_VALID; + } + else { + wc_LmsKey_Free(priv->data.lmsKey); + /* Mark encoded so the C_GenerateKeyPair cleanup (WP11_Object_Free) does + * not free the wolfSSL key a second time; mirrors the failure path in + * wp11_Object_Decode_HssKey. */ + priv->encoded = 1; + if (stateWritten && priv->statefulStateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, + priv->statefulStateId); + } + priv->statefulStateId = 0; + } + + wc_ForceZero(pubBuf, sizeof(pubBuf)); + return ret; +} + +/* Remove the durable signature-state file backing an HSS private key that was + * generated but never committed to the token - e.g. a later C_GenerateKeyPair + * step (AddObject/SetInitialStates/store) failed after keygen already wrote the + * genesis state to disk. Without this the encrypted state file is orphaned. + * No-op unless priv is a CKK_HSS private key with a live state id; safe to call + * on any object and more than once. */ +void WP11_Hss_RemoveOrphanState(WP11_Object* priv, WP11_Slot* slot) +{ + if (priv != NULL && slot != NULL && priv->type == CKK_HSS && + priv->objClass == CKO_PRIVATE_KEY && priv->statefulStateId != 0) { + (void)wp11_Stateful_StateFile_Remove("hsskey", (int)slot->id, + priv->statefulStateId); + priv->statefulStateId = 0; + } +} + +/* Sign a raw message with an HSS private key. State is advanced and persisted + * (via the write CB inside wc_LmsKey_Sign) BEFORE the signature is returned. + * On any failure the in-memory state is poisoned (STATE_VALID cleared) to + * force a reload from durable storage before the next attempt. Token-resident + * keys always have priv->lock == &token->lock. */ +int WP11_Hss_Sign(unsigned char* data, word32 dataLen, unsigned char* sig, + word32* sigLen, WP11_Object* priv) +{ + int ret = 0; + word32 outLen = 0; + + if (priv == NULL || priv->data.lmsKey == NULL || sig == NULL || + sigLen == NULL || priv->lock == NULL) { + return BAD_FUNC_ARG; + } + + WP11_Lock_LockRW(priv->lock); + + if ((priv->opFlag & WP11_FLAG_STATEFUL_STATE_VALID) == 0) { + ret = NOT_AVAILABLE_E; /* C-layer maps to CKR_DEVICE_ERROR */ + } + if (ret == 0) { + word64 prevCount = priv->statefulSigCount; + priv->statefulSigCount = prevCount + 1; + outLen = *sigLen; + ret = wc_LmsKey_Sign(priv->data.lmsKey, sig, &outLen, data, + (int)dataLen); + if (ret == 0) { + *sigLen = outLen; + } + else { + /* Write CB failed (durable state may lag the in-memory advance) + * or the key is exhausted. Poison until reloaded; zero any bytes + * wolfSSL wrote and revert the in-memory counter. */ + priv->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + if (outLen > 0) + XMEMSET(sig, 0, outLen); + priv->statefulSigCount = prevCount; + } + } + + WP11_Lock_UnlockRW(priv->lock); + return ret; +} + +/* Signature length for an HSS private key (parameter-set dependent). Returns + * 0 on error. */ +word32 WP11_Hss_SigLen(WP11_Object* key) +{ + word32 sigLen = 0; + + if (key == NULL || key->data.lmsKey == NULL) + return 0; + if (wc_LmsKey_GetSigLen(key->data.lmsKey, &sigLen) != 0) + return 0; + return sigLen; +} + +/* Remaining one-time keys for an HSS private key. The count comes from the + * wp11-layer statefulSigCount (persisted in the GCM-authenticated state + * header) rather than from wolfSSL LmsKey internals, whose layout is not + * public API: total = 2^(levels*height), saturating at UINT32_MAX for large + * parameter sets. + * + * wolfSSL's own view is used as a cross-check. wc_LmsKey_SigsLeft only reports + * whether any key is left, so the two can be compared but not reconciled; on + * disagreement the answer is always the pessimistic one, since over-reporting + * would invite a caller to attempt a signature that must not happen. */ +int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining) +{ + int levels = 0, height = 0, winternitz = 0; + int totalH; + word64 used, total; + + if (key == NULL || key->data.lmsKey == NULL || remaining == NULL) + return BAD_FUNC_ARG; + + *remaining = 0; + + /* wolfSSL says the key is spent: report 0 whatever the counter holds. */ + if (wc_LmsKey_SigsLeft(key->data.lmsKey) == 0) + return 0; + + if (wc_LmsKey_GetParameters(key->data.lmsKey, &levels, &height, + &winternitz) != 0) { + return BAD_FUNC_ARG; + } + totalH = levels * height; + if (totalH <= 0) + return BAD_FUNC_ARG; + if (totalH >= 64) { + *remaining = 0xFFFFFFFFU; + return 0; + } + total = ((word64)1) << totalH; + used = key->statefulSigCount; + /* The counter says spent while wolfSSL still reports keys left. The two + * views of the state have drifted, so keep the pessimistic 0. */ + if (used >= total) + return 0; + + if ((total - used) > 0xFFFFFFFFULL) + *remaining = 0xFFFFFFFFU; + else + *remaining = (word32)(total - used); + return 0; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Serve CKA_HSS_KEYS_REMAINING for a private key. Follows the usual attribute + * convention: data == NULL asks for the size only, a short buffer returns + * BUFFER_E with the required size in len. */ +static int wp11_HssGetKeysRemaining(WP11_Object* object, byte* data, + CK_ULONG* len) +{ + word32 remaining = 0; + CK_ULONG v; + + if (WP11_Hss_SigsLeft(object, &remaining) != 0) + return NOT_AVAILABLE_E; + + v = (CK_ULONG)remaining; + if (data == NULL) { + *len = sizeof(v); + return 0; + } + if (*len < sizeof(v)) { + *len = sizeof(v); + return BUFFER_E; + } + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + return 0; +} +#endif /* WOLFPKCS11_LMS_PRIVATE */ + +/* Serve HSS-specific attributes from a public-key object. */ +int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len) +{ + int ret = 0; + int levels = 0, height = 0, winternitz = 0; + + if (object == NULL || object->data.lmsKey == NULL || len == NULL) + return BAD_FUNC_ARG; + + switch (type) { + case CKA_HSS_LEVELS: { + CK_ULONG v; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + v = (CK_ULONG)levels; + if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + } + break; + } + case CKA_HSS_LMS_TYPE: { + CK_LMS_TYPE v; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + v = wp11_HssHeightToLmsType(height); + if (v == 0) + ret = NOT_AVAILABLE_E; + else if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + } + break; + } + case CKA_HSS_LMOTS_TYPE: { + CK_LMOTS_TYPE v; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + v = wp11_HssWToLmotsType(winternitz); + if (v == 0) + ret = NOT_AVAILABLE_E; + else if (data == NULL) + *len = sizeof(v); + else if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + } + else { + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + } + break; + } + case CKA_HSS_LMS_TYPES: + case CKA_HSS_LMOTS_TYPES: { + /* wolfSSL uses uniform LMS/LMOTS parameters across all HSS levels, so + * every array entry is the top-level type. Per-level types (allowed + * by the PKCS#11 v3.3 profile) are not expressible by wolfSSL and are + * not supported here. */ + CK_ULONG total; + CK_ULONG fill; + int i; + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, + &height, &winternitz); + if (ret == 0) { + total = (CK_ULONG)(sizeof(CK_ULONG) * levels); + fill = (type == CKA_HSS_LMS_TYPES) + ? wp11_HssHeightToLmsType(height) + : wp11_HssWToLmotsType(winternitz); + if (fill == 0) + ret = NOT_AVAILABLE_E; + else if (data == NULL) + *len = total; + else if (*len < total) { + *len = total; + ret = BUFFER_E; + } + else { + for (i = 0; i < levels; i++) + XMEMCPY(data + i * sizeof(CK_ULONG), &fill, + sizeof(CK_ULONG)); + *len = total; + } + } + break; + } + case CKA_HSS_KEYS_REMAINING: + /* Remaining-signature count is a private-key/state property. In a + * verify-only build (or for a public key) it is unavailable. */ + ret = NOT_AVAILABLE_E; +#ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) + ret = wp11_HssGetKeysRemaining(object, data, len); +#endif + break; + case CKA_VALUE: + if (object->objClass == CKO_PUBLIC_KEY) { + word32 pubLen = 0; + ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); + if (ret == 0) { + if (data == NULL) + *len = pubLen; + else if (*len < pubLen) { + *len = pubLen; + ret = BUFFER_E; + } + else { + ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, + data, &pubLen); + if (ret == 0) + *len = pubLen; + } + } + } + else { + *len = CK_UNAVAILABLE_INFORMATION; + ret = NOT_AVAILABLE_E; + } + break; + default: + ret = NOT_AVAILABLE_E; + break; + } + return ret; +} + +/* Validate a supplied CKA_HSS_LMS_TYPES / CKA_HSS_LMOTS_TYPES array against the + * imported public key. wolfSSL uses uniform LMS/LMOTS parameters across all HSS + * levels, so a conforming array holds one CK_ULONG per level, each equal to the + * single top-level type served by HssObject_GetAttr. A wrong length or any + * differing entry is rejected so a malformed or mismatched template is not + * silently accepted; a matching array (as read back from this key) is accepted. + * Returns 0 when the array matches, BAD_FUNC_ARG otherwise. The caller holds the + * object lock. */ +int wp11_HssCheckTypesAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + const byte* data, CK_ULONG len) +{ + int ret; + int levels = 0; + int height = 0; + int winternitz = 0; + int i; + CK_ULONG expected; + CK_ULONG entry; + + if (object->data.lmsKey == NULL || data == NULL) + return BAD_FUNC_ARG; + + ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, + &winternitz); + if (ret != 0) + return ret; + + expected = (type == CKA_HSS_LMS_TYPES) + ? (CK_ULONG)wp11_HssHeightToLmsType(height) + : (CK_ULONG)wp11_HssWToLmotsType(winternitz); + if (expected == 0) + return BAD_FUNC_ARG; + if (len != (CK_ULONG)(sizeof(CK_ULONG) * levels)) + return BAD_FUNC_ARG; + + for (i = 0; i < levels; i++) { + XMEMCPY(&entry, data + i * sizeof(CK_ULONG), sizeof(entry)); + if (entry != expected) + return BAD_FUNC_ARG; + } + + return 0; +} +#endif /* WOLFPKCS11_LMS */ + +#ifdef WOLFPKCS11_XMSS +/* =========================================================================== + * XMSS/XMSS^MT (RFC 8391): verification and public-key import. + * + * Verify-only. One wolfSSL XmssKey type serves both XMSS and XMSS^MT; the + * PKCS#11 key type (CKK_XMSS vs CKK_XMSSMT) selects which via the is_xmssmt + * flag. The raw public key carries the parameter-set OID in its leading + * bytes, so wc_XmssKey_ImportPubRaw_ex derives the parameter set on import. + * A CKA_PARAMETER_SET attribute is optional and, when supplied, is validated + * against the OID in the key. + * ========================================================================= */ + +/* Length of the parameter-set OID prefix in a raw RFC 8391 public key. wolfSSL + * only exposes XMSS_OID_LEN when built with WOLFSSL_HAVE_XMSS and it is absent + * from some released headers, so define our own to keep the build independent + * of the wolfSSL version. */ +#define WP11_XMSS_OID_LEN 4 + +/* Big-endian 32-bit read from a byte buffer (used to read the XMSS OID). */ +static word32 wp11_HbsReadU32(const byte* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) + | ((word32)p[2] << 8) | (word32)p[3]; +} + +/* Import a raw XMSS/XMSS^MT public key into the given wolfSSL XmssKey, deriving + * the parameter set from the embedded OID. is_xmssmt disambiguates the XMSS and + * XMSS^MT OID namespaces (which overlap) and comes from the PKCS#11 key type. + * When the caller supplies a CKA_PARAMETER_SET (params != NULL, the OID as a + * CK_ULONG) it must match the OID in the public key. On failure the key is NOT + * freed here; the caller owns it. */ +static int wp11_XmssImportPub(XmssKey* key, int is_xmssmt, int devId, + const byte* pub, word32 pubLen, const byte* params, word32 paramsLen) +{ + int ret; + + ret = wc_XmssKey_Init(key, NULL, devId); + if (ret == 0 && params != NULL) { + CK_ULONG userOid; + if (paramsLen != sizeof(CK_ULONG) || pubLen < WP11_XMSS_OID_LEN) { + ret = BAD_FUNC_ARG; + } + else { + XMEMCPY(&userOid, params, sizeof(userOid)); + if (userOid != (CK_ULONG)wp11_HbsReadU32(pub)) + ret = BAD_FUNC_ARG; + } + } + if (ret == 0) + ret = wc_XmssKey_ImportPubRaw_ex(key, pub, pubLen, is_xmssmt); + return ret; +} + +/* Serialize the public key: keyData holds the raw RFC 8391 public key. */ +int wp11_Object_Encode_XmssKey(WP11_Object* object) +{ + int ret; + word32 pubLen = 0; + + if (object->objClass != CKO_PUBLIC_KEY) + return NOT_AVAILABLE_E; + + ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); + if (ret == 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (object->keyData == NULL) + ret = MEMORY_E; + } + if (ret == 0) { + ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, object->keyData, + &pubLen); + if (ret == 0) + object->keyDataLen = (int)pubLen; + } + /* On failure release the buffer so a later store does not skip re-encoding + * (keyData != NULL) and persist an uninitialised buffer with a stale + * length. This is public key material only, so no zeroization is needed. */ + if (ret != 0) { + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + return ret; +} + +int wp11_Object_Decode_XmssKey(WP11_Object* object) +{ + int ret; + + if (object->objClass != CKO_PUBLIC_KEY) + return NOT_AVAILABLE_E; + + /* Reload from the stored raw public key; wolfSSL re-derives the parameter + * set from the embedded OID (no separate parameter set is persisted). */ + ret = wp11_XmssImportPub(object->data.xmssKey, + (object->type == CKK_XMSSMT) ? 1 : 0, object->devId, object->keyData, + (word32)object->keyDataLen, NULL, 0); + /* Object owns the key on all paths; encoded stays 0 for the single free in + * WP11_Object_Free. See wp11_Object_Decode_HssKey. */ + object->encoded = 0; + return ret; +} + +int wp11_Object_Load_XmssKey(WP11_Object* object, int tokenId, int objId) +{ + int ret; + void* storage = NULL; + + ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, + objId, &storage); + if (ret == 0) { + ret = wp11_storage_read_alloc_array(storage, &object->keyData, + &object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +int wp11_Object_Store_XmssKey(WP11_Object* object, int tokenId, + int objId) +{ + int ret = 0; + void* storage = NULL; + + if (object->keyData == NULL) + ret = wp11_Object_Encode_XmssKey(object); + if (ret == 0) + ret = wp11_storage_open(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, objId, + object->keyDataLen, &storage); + if (ret == 0) { + ret = wp11_storage_write_array(storage, object->keyData, + object->keyDataLen); + wp11_storage_close(storage); + } + return ret; +} + +/* Import an XMSS/XMSS^MT public key from a C_CreateObject template. data[1] is + * the raw RFC 8391 public key; data[0] (CKA_PARAMETER_SET, the OID as a + * CK_ULONG) is optional and, when present, is validated against the key. + * Private-key import is rejected. */ +int WP11_Object_SetXmssKey(WP11_Object* object, unsigned char** data, + CK_ULONG* len) +{ + int ret = 0; + XmssKey* newKey = NULL; + + if (object == NULL || data == NULL || len == NULL) + return BAD_FUNC_ARG; + if (object->objClass != CKO_PUBLIC_KEY) + return BAD_FUNC_ARG; + /* A public key must carry its value; an absent CKA_VALUE is an + * incomplete template (no keygen path fills it later). Reject it. */ + if (data[1] == NULL) + return BAD_FUNC_ARG; + + if (object->onToken) + WP11_Lock_LockRW(object->lock); + + /* Import into a temporary key and swap it in only after it validates, so a + * failed re-import leaves the existing key unchanged (PKCS#11 requires a + * failed C_SetAttributeValue to be a no-op). */ + newKey = (XmssKey*)XMALLOC(sizeof(XmssKey), NULL, DYNAMIC_TYPE_KEY); + if (newKey == NULL) + ret = MEMORY_E; + if (ret == 0) { + XMEMSET(newKey, 0, sizeof(XmssKey)); + ret = wp11_XmssImportPub(newKey, (object->type == CKK_XMSSMT) ? 1 : 0, + object->devId, data[1], (word32)len[1], data[0], (word32)len[0]); + } + if (ret == 0) { + /* Replace the live key and drop the cached serialization so a later + * store re-encodes from the new key. */ + if (object->data.xmssKey != NULL) { + if (!object->encoded) + wc_XmssKey_Free(object->data.xmssKey); + XFREE(object->data.xmssKey, NULL, DYNAMIC_TYPE_KEY); + } + object->data.xmssKey = newKey; + object->encoded = 0; + newKey = NULL; + XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); + object->keyData = NULL; + object->keyDataLen = 0; + } + if (newKey != NULL) { + wc_XmssKey_Free(newKey); + XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); + } + + if (object->onToken) + WP11_Lock_UnlockRW(object->lock); + return ret; +} + +/* Verify an XMSS/XMSS^MT signature over a raw message. Stateless. */ +int WP11_Xmss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, + word32 dataLen, int* stat, WP11_Object* pub) +{ + int ret; + word32 expSigLen = 0; + + if (pub == NULL || pub->data.xmssKey == NULL || stat == NULL) + return BAD_FUNC_ARG; + + /* Verification only reads the key: wc_XmssKey_Verify works from a locally + * allocated XmssState, so a shared RO lock is safe for concurrent verifies + * on the same token object. */ + if (pub->onToken) + WP11_Lock_LockRO(pub->lock); + + /* A wrong-length signature cannot be valid for this key; report it as an + * invalid signature rather than a function failure. wolfSSL's XMSS verify + * also rejects a zero-length message with BAD_FUNC_ARG; treat that the same + * way (an invalid verification, not an internal failure). */ + if (wc_XmssKey_GetSigLen(pub->data.xmssKey, &expSigLen) == 0 && + sigLen != expSigLen) { + *stat = 0; + ret = 0; + } + else if (dataLen == 0) { + *stat = 0; + ret = 0; + } + else { + ret = wc_XmssKey_Verify(pub->data.xmssKey, sig, sigLen, data, + (int)dataLen); + /* Only SIG_VERIFY_E maps to stat=0; other argument/state errors + * surface as a function failure so they are not silently reported as + * an invalid signature. */ + if (ret == 0) { + *stat = 1; + } + else if (ret == SIG_VERIFY_E) { + *stat = 0; + ret = 0; + } + else { + *stat = 0; + } + } + + if (pub->onToken) + WP11_Lock_UnlockRO(pub->lock); + + return ret; +} + +/* Serve XMSS-specific attributes from a public-key object. */ +int XmssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len) +{ + int ret = 0; + + if (object == NULL || object->data.xmssKey == NULL || len == NULL) + return BAD_FUNC_ARG; + + switch (type) { + case CKA_VALUE: + if (object->objClass == CKO_PUBLIC_KEY) { + word32 pubLen = 0; + ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); + if (ret == 0) { + if (data == NULL) + *len = pubLen; + else if (*len < pubLen) { + *len = pubLen; + ret = BUFFER_E; + } + else { + ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, + data, &pubLen); + if (ret == 0) + *len = pubLen; + } + } + } + else { + *len = CK_UNAVAILABLE_INFORMATION; + ret = NOT_AVAILABLE_E; + } + break; + case CKA_PARAMETER_SET: { + /* The XMSS parameter set is identified by the OID in the leading + * bytes of the raw public key; return it as a CK_ULONG. */ + CK_ULONG v; + word32 pubLen = 0; + byte* raw; + + if (object->objClass != CKO_PUBLIC_KEY) { + *len = CK_UNAVAILABLE_INFORMATION; + ret = NOT_AVAILABLE_E; + break; + } + if (data == NULL) { + *len = sizeof(v); + break; + } + if (*len < sizeof(v)) { + *len = sizeof(v); + ret = BUFFER_E; + break; + } + ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); + if (ret == 0 && pubLen < WP11_XMSS_OID_LEN) + ret = NOT_AVAILABLE_E; + if (ret == 0) { + raw = (byte*)XMALLOC(pubLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (raw == NULL) { + ret = MEMORY_E; + } + else { + ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, raw, + &pubLen); + if (ret == 0) { + v = (CK_ULONG)wp11_HbsReadU32(raw); + XMEMCPY(data, &v, sizeof(v)); + *len = sizeof(v); + } + XFREE(raw, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + } + break; + } + default: + ret = NOT_AVAILABLE_E; + break; + } + return ret; +} +#endif /* WOLFPKCS11_XMSS */ diff --git a/src/hbs.h b/src/hbs.h new file mode 100644 index 00000000..31e2b6d1 --- /dev/null +++ b/src/hbs.h @@ -0,0 +1,77 @@ +/* hbs.h + * + * Copyright (C) 2006-2023 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfPKCS11 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Hash-based signature schemes (hbs.c), private to src/. + * + * The object encode/decode/load/store entry points below are dispatched from + * the per-key-type switches in internal.c. Everything the rest of the library + * calls - WP11_Hss_Verify, WP11_Hss_Sign, WP11_Object_SetHssKey and friends - + * is declared in wolfpkcs11/internal.h instead. + */ + +#ifndef WOLFPKCS11_HBS_H +#define WOLFPKCS11_HBS_H + +#include "wp11_types.h" + +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* Read WOLFPKCS11_STATEFUL_RELAX_FSYNC once during library init, before any + * signing thread exists. */ +WP11_LOCAL void WP11_Stateful_InitRelaxFsync(void); +#endif + +#ifdef WOLFPKCS11_LMS + +WP11_LOCAL int wp11_Object_Encode_HssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Decode_HssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len); +WP11_LOCAL int wp11_HssCheckTypesAttr(WP11_Object* object, + CK_ATTRIBUTE_TYPE type, const byte* data, + CK_ULONG len); + +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Delete the signature-state file of a stored HSS private key, for + * wp11_Object_Unstore. Works whether or not the object has been decoded. */ +WP11_LOCAL void wp11_Hss_RemoveStoredState(WP11_Object* object, int tokenId, + int objId); +#endif + +#endif /* WOLFPKCS11_LMS */ + +#ifdef WOLFPKCS11_XMSS + +WP11_LOCAL int wp11_Object_Encode_XmssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Decode_XmssKey(WP11_Object* object); +WP11_LOCAL int wp11_Object_Load_XmssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int wp11_Object_Store_XmssKey(WP11_Object* object, int tokenId, + int objId); +WP11_LOCAL int XmssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, + byte* data, CK_ULONG* len); + +#endif /* WOLFPKCS11_XMSS */ + +#endif /* WOLFPKCS11_HBS_H */ diff --git a/src/include.am b/src/include.am index f54ea410..44b241c8 100644 --- a/src/include.am +++ b/src/include.am @@ -8,7 +8,12 @@ src_libwolfpkcs11_la_SOURCES = \ src/internal.c \ src/wolfpkcs11.c \ src/slot.c \ - src/crypto.c + src/crypto.c \ + src/hbs.c + +noinst_HEADERS += \ + src/wp11_types.h \ + src/hbs.h src_libwolfpkcs11_la_CFLAGS = -DBUILDING_WOLFPKCS11 $(AM_CFLAGS) src_libwolfpkcs11_la_CPPFLAGS = -DBUILDING_WOLFPKCS11 $(AM_CPPFLAGS) diff --git a/src/internal.c b/src/internal.c index 3423de7a..f01ef2f1 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19,177 +19,8 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ -#ifdef HAVE_CONFIG_H - #include -#endif -#include "wolfpkcs11/pkcs11.h" - -#ifndef WOLFSSL_USER_SETTINGS -#include -#else -#include "user_settings.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef WOLFPKCS11_MLKEM -#include -#endif - -#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) -/* OS-specific includes for directory creation */ -#if defined(_WIN32) || defined(_MSC_VER) - #include - #include - #define MKDIR(path) _mkdir(path) -#else - #include - #define MKDIR(path) mkdir(path, 0700) -#endif -#endif - -#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) - #include - #include - #if defined(_WIN32) || defined(_MSC_VER) - #include - #include - #include - #include - #else - #include - #include - #include - #endif -#endif - -#include -#include - -#ifdef WOLFPKCS11_TPM - #include - #include - - #ifndef WOLFPKCS11_TPM_CUST_IO - #include - #ifndef TPM2_IOCB_CTX - #define TPM2_IOCB_CTX NULL - #endif - #endif -#endif - -#ifdef WOLFSSL_MAXQ10XX_CRYPTO -#include -#include -#define MAX_CERT_DATASIZE 2048 -#define MAX_SIG_DATASIZE 64 -#define ECC_KEYCOMPLEN 32 -#endif /* WOLFSSL_MAXQ10XX_CRYPTO */ - -#if defined(WC_RSA_BLINDING) && (!defined(HAVE_FIPS) || \ - (defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION > 2))) -#define WOLFPKCS11_NEED_RSA_RNG -#endif - -#if defined(WOLFPKCS11_TPM) && defined(WOLFSSL_MAXQ10XX_CRYPTO) - #error "wolfTPM and MAXQ10XX are incompatible with each other." -#endif - -/* Helper to get size of struct field */ -#define FIELD_SIZE(type, field) (sizeof(((type *)0)->field)) - -/* Size of hash calculated from PIN. */ -#define PIN_HASH_SZ 32 -/* Size of seed used when calculating hash from PIN. */ -#define PIN_SEED_SZ 16 -/* Size of token's label. */ -#define LABEL_SZ 32 - -/* Length of seed from global random to seed local random. */ -#define RNG_SEED_SZ 32 - - - -/* Maximum size of storage for generated/derived DH key. */ -#ifdef WOLFPKCS11_NSS -#define WP11_MAX_DH_KEY_SZ (8192/8) -#else -#define WP11_MAX_DH_KEY_SZ (4096/8) -#endif - -/* Maximum size of storage for generated/derived symmetric key. */ -#ifdef WOLFPKCS11_NSS -#define WP11_MAX_SYM_KEY_SZ (2048) -#elif !defined(NO_DH) -#define WP11_MAX_SYM_KEY_SZ (4096/8) -#elif defined(HAVE_ECC) -#define WP11_MAX_SYM_KEY_SZ ((521+7)/8) -#else -#define WP11_MAX_SYM_KEY_SZ 64 -#endif - -#ifndef WP11_MAX_CERT_SZ -#define WP11_MAX_CERT_SZ 4096 -#endif - -#define PKCS11_CHECK_VALUE_SIZE 3 - -/* Sizes for storage. */ -#define WP11_MAX_IV_SZ 16 -#define WP11_MAX_GCM_NONCE_SZ 16 -#define WP11_MAX_GCM_TAG_SZ 16 -#define WP11_MAX_GCM_TAG_BITS 128 - -/* wolfSSL ECC signatures are ASN.1 encoding - need to encode and decode. */ -#define ASN_INTEGER 0x02 -#define ASN_OCTET_STRING 0x04 -#define ASN_OBJECT_ID 0x06 -#define ASN_SEQUENCE 0x10 -#define ASN_CONSTRUCTED 0x20 -#define ASN_LONG_LENGTH 0x80 - -/* Create a session handle from slot id and session id. */ -#define SESS_HANDLE(slot, s) (((slot) << 16) | (s)) -/* Determine slot id from session handle. */ -#define SESS_HANDLE_SLOT_ID(s) ((CK_SLOT_ID)((s) >> 16)) -/* Determine session id from session handle. */ -#define SESS_HANDLE_SESS_ID(s) ((s) & 0xffff) - -/* Create an object handle from a onToken and object id. */ -#define OBJ_HANDLE(on, i) (((on) << 28) | (i)) -/* Determine whether object is onToken from object handle. */ -#define OBJ_HANDLE_ON_TOKEN(h) ((int)((h) >> 28)) -/* Determine object id from object handle. */ -#define OBJ_HANDLE_OBJ_ID(h) ((h) & 0xfffffff) - -#ifdef SINGLE_THREADED -/* Disable locking. */ -typedef int WP11_Lock; - -#define WP11_Lock_Init(l) ({ 0; }) -#define WP11_Lock_Free(l) -#define WP11_Lock_LockRW(l) ({ 0; }) -#define WP11_Lock_UnlockRW(l) ({ 0; }) -#define WP11_Lock_LockRO(l) ({ 0; }) -#define WP11_Lock_UnlockRO(l) ({ 0; }) - -#else -typedef struct WP11_Lock { - wolfSSL_Mutex read; /* Mutex for accessing count */ - wolfSSL_Mutex write; /* Mutex for writing */ - int cnt; /* Count of readers */ -} WP11_Lock; -#endif +#include "wp11_types.h" +#include "hbs.h" #ifdef DEBUG_WOLFPKCS11 int wolfpkcs11_debugging = 0; @@ -208,367 +39,6 @@ WP11_API void wolfPKCS11_Debugging_Off(void) #endif -/* Symmetric key data. */ -typedef struct WP11_Data { - byte data[WP11_MAX_SYM_KEY_SZ]; /* Key data */ - word32 len; /* Length of key data in bytes */ -} WP11_Data; - -typedef struct WP11_GenericData { - byte *data; - word32 dataLen; - byte *application; - word32 applicationLen; - byte *objectId; /* CKA_OBJECT_ID, a DER encoded ID */ - word32 objectIdLen; -} WP11_GenericData; - -/* Certificate */ -typedef struct WP11_Cert { - byte *data; /* Certificate data */ - word32 len; /* Length of certificate data in bytes */ - CK_CERTIFICATE_TYPE type; -} WP11_Cert; - -typedef struct WP11_Trust { - byte sha1Hash[WC_SHA_DIGEST_SIZE]; - byte md5Hash[WC_MD5_DIGEST_SIZE]; - CK_ULONG serverAuth; - CK_ULONG clientAuth; - CK_ULONG emailProtection; - CK_ULONG codeSigning; - CK_BBOOL stepUpApproved; -} WP11_Trust; - -#ifndef NO_DH -typedef struct WP11_DhKey { - byte key[WP11_MAX_DH_KEY_SZ]; /* Public or private key */ - word32 len; /* Length of public key */ - DhKey params; /* DH parameters object */ -} WP11_DhKey; -#endif - -/* When adding/modifying new members, add support in WP11_Object_Copy */ -struct WP11_Object { - union { - #ifndef NO_RSA - RsaKey* rsaKey; /* RSA key object */ - #endif - #ifdef HAVE_ECC - ecc_key* ecKey; /* EC key object */ - #endif - #ifdef WOLFPKCS11_MLDSA - wc_MlDsaKey* mldsaKey; /* ML-DSA key object */ - #endif - #ifndef NO_DH - WP11_DhKey* dhKey; /* DH parameters object */ - #endif - #ifdef WOLFPKCS11_MLKEM - MlKemKey* mlKemKey; /* ML-KEM key object */ - #endif - #ifdef WOLFPKCS11_LMS - LmsKey* lmsKey; /* LMS/HSS key object (verify) */ - #endif - #ifdef WOLFPKCS11_XMSS - XmssKey* xmssKey; /* XMSS/XMSS^MT key object (verify) */ - #endif - WP11_Data* symmKey; /* Symmetric key object */ - WP11_GenericData genericData; /* Generic data object */ - WP11_Cert cert; /* Certificate object */ - #ifdef WOLFPKCS11_NSS - WP11_Trust trust; /* Trust object */ - #endif - } data; -#ifdef WOLFPKCS11_TPM - WOLFTPM2_KEYBLOB* tpmKey; -#endif -#ifdef WOLFSSL_STM32U5_DHUK - unsigned char* dhukIv; /* IV used with wrapping and unwrapping AES key. */ - int dhukIvLen; -#endif - CK_KEY_TYPE type; /* Key type of this object */ - word32 size; /* Size of the key in bits or bytes */ -#ifndef WOLFPKCS11_NO_STORE - unsigned char* keyData; /* Encoded key data */ - int keyDataLen; /* Length of encoded key data */ - byte iv[GCM_NONCE_MID_SZ]; /* IV/nonce for encrypt/decrypt */ - byte encoded:1; /* Key isn't in decoded form */ -#endif - - WP11_Session* session; /* Session object belongs to */ - WP11_Slot* slot; /* Slot object belongs to */ - - CK_OBJECT_HANDLE handle; /* Handle of this object */ - CK_OBJECT_CLASS objClass; /* Object class */ - CK_MECHANISM_TYPE keyGenMech; /* Key Gen mechanism created with */ - byte onToken:1; /* Object on token or session */ - byte local:1; /* Locally created object */ - word32 opFlag; /* Flags of operations allowed */ - - char startDate[8]; /* Start date of usage */ - char endDate[8]; /* End data of usage */ - - unsigned char* keyId; /* Key identifier */ - int keyIdLen; /* Length of key identifier */ - unsigned char* label; /* Object label */ - int labelLen; /* Length of object label */ - unsigned char* issuer; /* Certificate issuer */ - int issuerLen; /* Length of certificate issuer */ - unsigned char* serial; /* Certificate serial number */ - int serialLen; /* Length of certificate serial number */ - unsigned char* subject; /* Subject of the object */ - int subjectLen; /* Length of subject */ -#ifdef WOLFPKCS11_NSS - unsigned char* email; /* Certificate email (NSS extension) */ - int emailLen; /* Length of certificate email */ -#endif - - word32 category; /* Category of certificate */ - - WP11_Lock* lock; /* Object specific lock */ - - int devId; - WP11_Object* next; /* Next object in linked list */ -}; - -typedef struct WP11_Find { - int state; /* Whether operation is initialized */ - CK_OBJECT_HANDLE found[WP11_FIND_MAX]; - /* List of object handles found */ - int count; /* Count of object handles */ - int curr; /* Index of last object returned */ -} WP11_Find; - -#ifndef NO_RSA -#ifndef WC_NO_RSA_OAEP -typedef struct WP11_OaepParams { - enum wc_HashType hashType; /* Type of hash algorithm */ - int mgf; /* Mask Generation Function */ - byte* label; /* Label or AAD */ - int labelSz; /* Size of label in bytes */ -} WP11_OaepParams; -#endif - -#ifdef WC_RSA_PSS -typedef struct WP11_PssParams { - enum wc_HashType hashType; - int mgf; - int saltLen; -} WP11_PssParams; -#endif -#endif - -#ifdef WOLFPKCS11_MLDSA -typedef struct WP11_MldsaParams { - enum wc_HashType preHashType; - word32 hedgeType; - byte ctx[256]; - byte ctxSz; -} WP11_MldsaParams; -#endif - -#ifndef NO_AES -#ifdef HAVE_AES_CBC -typedef struct WP11_CbcParams { - unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ - Aes aes; /* AES object from wolfCrypt */ - unsigned char partial[AES_BLOCK_SIZE]; - /* Partial block when streaming */ - byte partialSz; /* Size of partial block data */ -} WP11_CbcParams; -#endif - -#ifdef HAVE_AESCTR -typedef struct WP11_CtrParams { - Aes aes; /* AES object from wolfCrypt */ -} WP11_CtrParams; -#endif - -#ifdef HAVE_AESGCM -typedef struct WP11_GcmParams { - unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; - /* IV/nonce data */ - int ivSz; /* IV/nonce size in bytes */ - unsigned char* aad; /* Additional Authentication Data */ - int aadSz; /* AAD size in bytes */ - int tagBits; /* Authentication tag size in bits */ - unsigned char authTag[WP11_MAX_GCM_TAG_SZ]; - /* Authentication tag calculated */ - unsigned char* enc; /* Cached data for multi-part: buffered */ - /* ciphertext (decrypt) or, without */ - /* streaming GCM, plaintext (encrypt) */ - int encSz; /* Size of cached data in bytes */ -#ifdef WOLFSSL_AESGCM_STREAM - Aes aes; /* Streaming context for multi-part */ - /* encrypt */ - int streamInit; /* Streaming context is initialized */ -#endif -} WP11_GcmParams; -#endif - -#ifdef HAVE_AESCCM -typedef struct WP11_CcmParams { - int dataSz; /* Data size in bytes */ - unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; - /* IV/nonce data */ - int ivSz; /* IV/nonce size in bytes */ - unsigned char* aad; /* Additional Authentication Data */ - int aadSz; /* AAD size in bytes */ - int macSz; /* Size of MAC data in bytes */ -} WP11_CcmParams; -#endif - -#ifdef HAVE_AES_KEYWRAP -typedef struct WP11_KeyWrapParams { - Aes aes; /* AES object from wolfCrypt */ - unsigned char iv[8]; /* IV/nonce data */ - word32 ivSz; /* IV/nonce size in bytes */ -} WP11_KeyWrapParams; -#endif - -#ifdef HAVE_AESCTS -typedef struct WP11_CtsParams { - unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ - Aes aes; /* AES object from wolfCrypt */ -} WP11_CtsParams; -#endif -#endif - -#ifndef NO_HMAC -typedef struct WP11_Hmac { - Hmac hmac; - word32 hmacSz; -} WP11_Hmac; -#endif - -typedef struct WP11_Digest { - wc_HashAlg hash; - enum wc_HashType hashType; -} WP11_Digest; - -#ifdef HAVE_AESCMAC -typedef struct WP11_Cmac { - Cmac cmac; - byte sigLen; -} WP11_Cmac; -#endif - -typedef struct WP11_TlsMacParams { - enum wc_MACAlgorithm mac; - word32 macSz; - CK_OBJECT_HANDLE key; - byte server:1; - byte isTlsPrf:1; -} WP11_TlsMacParams; - -struct WP11_Session { - unsigned char inUse; /* Indicates session has been opened */ - CK_SESSION_HANDLE handle; /* CryptoKi API session handle value */ - CK_MECHANISM_TYPE mechanism; /* Op that is being performed */ - CK_SLOT_ID slotId; /* Id of slot that session is on */ - WP11_Slot* slot; /* Slot that session is on */ - WP11_Object* object; /* Linked list of objects on session */ - int objCnt; /* Count of objects in session */ - WP11_Object* curr; /* Current object */ - WP11_Find find; /* Find data */ - int init; /* Which op is initialized */ - union { -#ifndef NO_RSA - #ifndef WC_NO_RSA_OAEP - WP11_OaepParams oaep; /* RSA-OAEP parameters */ - #endif - #ifdef WC_RSA_PSS - WP11_PssParams pss; /* RSA-PSS parameters */ - #endif -#endif -#ifdef WOLFPKCS11_MLDSA - WP11_MldsaParams mldsa; /* ML-DSA parameters */ -#endif -#ifndef NO_AES - #ifdef HAVE_AES_CBC - WP11_CbcParams cbc; /* AES-CBC parameters */ - #endif - #ifdef HAVE_AESCTR - WP11_CtrParams ctr; /* AES-CTR parameters */ - #endif - #ifdef HAVE_AESGCM - WP11_GcmParams gcm; /* AES-GCM parameters */ - #endif - #ifdef HAVE_AESCCM - WP11_CcmParams ccm; /* AES-CCM parameters */ - #endif - #ifdef HAVE_AESCTS - WP11_CtsParams cts; /* AES-CTS parameters */ - #endif - #ifdef HAVE_AES_KEYWRAP - WP11_KeyWrapParams kw; /* AES-KW parameters */ - #endif -#endif -#ifndef NO_HMAC - WP11_Hmac hmac; /* HMAC parameters */ -#endif - WP11_Digest digest; /* Digest parameters */ -#ifdef HAVE_AESCMAC - WP11_Cmac cmac; /* CMAC parameters */ -#endif - WP11_TlsMacParams tlsMac; /* TLS MAC parameters */ - } params; - - byte* data; /* Held data for one-shot mechs */ - word32 dataSz; /* Size of held data */ - - int devId; - WP11_Session* next; /* Next session for slot */ -}; - -typedef struct WP11_Token { - char label[LABEL_SZ]; /* Token label */ - int state; /* Token initialize state */ - byte soPin[PIN_HASH_SZ]; /* SO's PIN hashed with seed */ - int soPinLen; /* Used to indicate PIN set */ - byte soPinSeed[PIN_SEED_SZ]; /* Seed for calculating SO's PIN */ - int soFailedLogin; /* Count of consecutive failed logins */ - time_t soLastFailedLogin; /* Time of last login if it failed */ - time_t soFailLoginTimeout; /* Timeout after max login fails */ - byte userPin[PIN_HASH_SZ]; /* User's PIN hashed with seed */ - int userPinLen; /* Used to indicate PIN set */ - byte userPinSeed[PIN_SEED_SZ]; /* Seed for calculating user's PIN */ - int userFailedLogin; /* Count of consecutive failed logins */ - time_t userLastFailedLogin; /* Time of last login if it failed */ - time_t userFailLoginTimeout; /* Timeout after max login fails */ -#ifndef WOLFPKCS11_NO_STORE - byte seed[PIN_SEED_SZ]; /* Seed used to calculate key */ - byte key[AES_256_KEY_SIZE]; /* Key to en/decrypt private data */ -#endif - WC_RNG rng; /* Random number generator */ - WP11_Lock rngLock; /* Lock for random access */ - WP11_Lock lock; /* Lock for object access */ - int loginState; /* Login state of the token */ - WP11_Object* object; /* Linked list of token objects */ - int objCnt; /* Count of objects on token */ - int tokenFlags; /* Flags for token */ - int nextObjId; - byte userPinEmpty:2; /* Indicates user PIN is empty - * 0 = not set - * 1 = empty - * 2 = not empty */ -} WP11_Token; - -struct WP11_Slot { - CK_SLOT_ID id; /* CryptoKi API slot id value */ - WP11_Token token; /* Token information for slot */ - WP11_Session* session; /* Linked list of sessions */ - WP11_Lock lock; /* Lock for access to slot info */ - - int devId; -#ifdef WOLFPKCS11_TPM - WOLFTPM2_DEV tpmDev; - WOLFTPM2_KEY tpmSrk; - WOLFTPM2_SESSION tpmSession; - TpmCryptoDevCtx tpmCtx; -#endif -}; - #if defined(HAVE_FIPS) && FIPS_VERSION_LT(6,0) #define USE_LOCAL_CURVE_OID_LOOKUP typedef struct WP11_Ecc_Curve @@ -692,7 +162,7 @@ static void WP11_Lock_Free(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_LockRW(WP11_Lock* lock) +int WP11_Lock_LockRW(WP11_Lock* lock) { int ret; @@ -711,7 +181,7 @@ static int WP11_Lock_LockRW(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_UnlockRW(WP11_Lock* lock) +int WP11_Lock_UnlockRW(WP11_Lock* lock) { int ret; @@ -732,7 +202,7 @@ static int WP11_Lock_UnlockRW(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_LockRO(WP11_Lock* lock) +int WP11_Lock_LockRO(WP11_Lock* lock) { int ret; @@ -759,7 +229,7 @@ static int WP11_Lock_LockRO(WP11_Lock* lock) * @return BAD_MUTEX_E on failure. * 0 on success. */ -static int WP11_Lock_UnlockRO(WP11_Lock* lock) +int WP11_Lock_UnlockRO(WP11_Lock* lock) { int ret; @@ -780,7 +250,7 @@ static int WP11_Lock_UnlockRO(WP11_Lock* lock) } #endif -static int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng) +int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng) { int ret; unsigned char seed[RNG_SEED_SZ]; @@ -796,7 +266,7 @@ static int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng) return ret; } -static void Rng_Free(WC_RNG* rng) +void Rng_Free(WC_RNG* rng) { wc_FreeRng(rng); } @@ -1173,10 +643,66 @@ typedef struct WP11_FileStoreCtx { XFILE file; int is_write; int has_temp; +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + int durable; /* fsync file + directory before commit succeeds */ +#endif char final_name[WP11_STORE_MAX_PATH]; char temp_name[WP11_STORE_MAX_PATH]; } WP11_FileStoreCtx; +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +/* Mark a write-mode file storage context as requiring full durability (fsync + * of file data + parent directory) before the close path returns success. + * Call after the store is opened for write, before writing. */ +void wp11_storage_set_durable(void* store, int durable) +{ +#ifndef WOLFPKCS11_TPM_STORE + WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; + if (ctx != NULL) + ctx->durable = durable ? 1 : 0; +#else + (void)store; + (void)durable; +#endif +} +#endif + +/* Copy the directory part of path into dirPath, without the trailing + * separator. A path with no separator has no directory to return, and one + * whose only separator is the leading character names the filesystem root, + * which is never a usable token directory - both are rejected. + * + * @param path [in] Full path to a file. + * @param dirPath [out] Buffer receiving the directory part. + * @param dirPathLen [in] Size of dirPath in bytes. + * @return READ_ONLY_E when there is no directory part or it does not fit. + * 0 on success. + */ +static int wolfPKCS11_StoreDirName(const char* path, char* dirPath, + int dirPathLen) +{ + int len; + int slash = -1; + int i; + + if (path == NULL || dirPath == NULL) + return READ_ONLY_E; + + len = (int)XSTRLEN(path); + for (i = len - 1; i >= 0; i--) { + if (path[i] == '/' || path[i] == '\\') { + slash = i; + break; + } + } + if (slash <= 0 || slash >= dirPathLen) + return READ_ONLY_E; + + XMEMCPY(dirPath, path, slash); + dirPath[slash] = '\0'; + return 0; +} + static void wolfPKCS11_StoreAbortTemp(WP11_FileStoreCtx* ctx) { if (ctx != NULL && ctx->has_temp) { @@ -1202,6 +728,28 @@ static int wolfPKCS11_StoreCommitTemp(WP11_FileStoreCtx* ctx) if (rename(ctx->temp_name, ctx->final_name) != 0) { ret = READ_ONLY_E; } +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + /* Durable mode: fsync the parent directory so the rename itself is + * persisted (the directory entry, not just the file data). Otherwise a + * crash could lose the rename and roll the signature state back. */ + if (ret == 0 && ctx->durable) { + char dirPath[WP11_STORE_MAX_PATH]; + + ret = wolfPKCS11_StoreDirName(ctx->final_name, dirPath, + (int)sizeof(dirPath)); + if (ret == 0) { + int dirFd = open(dirPath, O_RDONLY); + if (dirFd >= 0) { + if (fsync(dirFd) != 0) + ret = READ_ONLY_E; + close(dirFd); + } + else { + ret = READ_ONLY_E; + } + } + } +#endif #endif if (ret == 0) { @@ -1488,6 +1036,12 @@ static int wolfPKCS11_Store_Name(int type, CK_ULONG id1, CK_ULONG id2, char* nam ret = XSNPRINTF(name, nameLen, "%s/wp11_hsskey_pub_%016lx_%016lx", str, id1, id2); break; +#ifdef WOLFPKCS11_LMS_PRIVATE + case WOLFPKCS11_STORE_HSSKEY_PRIV: + ret = XSNPRINTF(name, nameLen, "%s/wp11_hsskey_priv_%016lx_%016lx", + str, id1, id2); + break; +#endif #endif #ifdef WOLFPKCS11_XMSS case WOLFPKCS11_STORE_XMSSKEY_PUB: @@ -1675,9 +1229,6 @@ int wolfPKCS11_Store_OpenSz(int type, CK_ULONG id1, CK_ULONG id2, int read, if (ret == 0) { char dirPath[WP11_STORE_MAX_PATH]; - const char* lastSlash = NULL; - size_t nameLen = XSTRLEN(name); - size_t i; XMEMSET(ctx, 0, sizeof(*ctx)); ctx->file = XBADFILE; @@ -1700,30 +1251,12 @@ int wolfPKCS11_Store_OpenSz(int type, CK_ULONG id1, CK_ULONG id2, int read, } } else if (ret == 0) { - for (i = 0; i < nameLen; i++) { - if (name[i] == '/' || name[i] == '\\') { - lastSlash = &name[i]; - } - } - - if (lastSlash == NULL) { - ret = READ_ONLY_E; + ret = wolfPKCS11_StoreDirName(name, dirPath, (int)sizeof(dirPath)); + if (ret == 0) { + ret = wolfPKCS11_StoreEnsureDir(dirPath); } - else { - int dirLen = (int)(lastSlash - name); - - if (dirLen <= 0 || dirLen >= (int)sizeof(dirPath)) { - ret = READ_ONLY_E; - } - else { - XMEMCPY(dirPath, name, dirLen); - dirPath[dirLen] = '\0'; - - ret = wolfPKCS11_StoreEnsureDir(dirPath); - if (ret == 0) { - ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); - } - } + if (ret == 0) { + ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); } } } @@ -1773,18 +1306,24 @@ int wolfPKCS11_Store_Open(int type, CK_ULONG id1, CK_ULONG id2, int read, } /** - * Closes access to location being read or written. - * Any dynamic memory associated with the store is freed here. + * Closes access to location being read or written and reports whether the + * commit succeeded. Any dynamic memory associated with the store is freed + * here. A close that cannot report failure would mask a failed fsync or + * rename, which for stateful signature state risks releasing a signature + * whose advanced state never reached stable storage. * * @param [in] store Context for operation. + * @return 0 on success. + * @return Other value when a durable flush or the temp-file commit failed. */ -void wolfPKCS11_Store_Close(void* store) +int wp11_storage_close_report(void* store) { #ifdef WOLFPKCS11_TPM_STORE WP11_TpmStore* tpmStore = (WP11_TpmStore*)store; #else WP11_FileStoreCtx* ctx = (WP11_FileStoreCtx*)store; #endif + int ret = 0; #ifdef WOLFPKCS11_DEBUG_STORE printf("Store close: %p\n", store); @@ -1796,13 +1335,59 @@ void wolfPKCS11_Store_Close(void* store) #else if (ctx != NULL) { int commitRet = 0; + int fsyncFailed = 0; +#if defined(WOLFPKCS11_STATEFUL_SIG_PRIVATE) && \ + !defined(_WIN32) && !defined(_MSC_VER) + int fd; +#endif if (ctx->file != XBADFILE && ctx->file != NULL) { - XFCLOSE(ctx->file); - ctx->file = XBADFILE; +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + /* Durable mode: flush the file data to disk before close so the + * rename in CommitTemp commits a fully-persisted file. POSIX uses + * fsync(); Windows/MSVC uses _commit() (FlushFileBuffers under the + * hood) because MoveFileExA's MOVEFILE_WRITE_THROUGH makes only the + * rename durable, not the file data buffered before close. + * + * Every result here has to be checked. fsync only pushes what the + * kernel already holds, so bytes still sitting in the stdio buffer + * are invisible to it: a failed XFFLUSH followed by a successful + * fsync on an empty queue would otherwise look like a clean + * durable write. XFCLOSE is the last chance to learn about a + * deferred write error. */ + if (ctx->durable && ctx->is_write) { + if (XFFLUSH(ctx->file) != 0) + fsyncFailed = 1; +#if defined(_WIN32) || defined(_MSC_VER) + if (!fsyncFailed && _commit(_fileno(ctx->file)) != 0) + fsyncFailed = 1; +#else + if (!fsyncFailed) { + fd = fileno(ctx->file); + if (fd < 0 || fsync(fd) != 0) + fsyncFailed = 1; + } +#endif + if (XFCLOSE(ctx->file) != 0) + fsyncFailed = 1; + ctx->file = XBADFILE; + } +#endif + if (ctx->file != XBADFILE && ctx->file != NULL) { + XFCLOSE(ctx->file); + ctx->file = XBADFILE; + } } - if (ctx->is_write && ctx->has_temp) { + /* A durable write that did not reach stable storage must not replace + * the file that is already there. Renaming the temp file over it would + * swap a known-good state file for one that may be short or empty, + * turning a transient I/O error into permanent loss of the key. */ + if (fsyncFailed) { + wolfPKCS11_StoreAbortTemp(ctx); + ret = READ_ONLY_E; + } + else if (ctx->is_write && ctx->has_temp) { commitRet = wolfPKCS11_StoreCommitTemp(ctx); if (commitRet != 0) { wolfPKCS11_StoreAbortTemp(ctx); @@ -1810,6 +1395,7 @@ 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) { @@ -1820,6 +1406,18 @@ void wolfPKCS11_Store_Close(void* store) XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); } #endif + return ret; +} + +/* wolfPKCS11_Store_Close is the store backend interface declared in + * wolfpkcs11/store.h and reimplemented by WOLFPKCS11_CUSTOM_STORE builds, so + * it keeps its void return. Internal callers that need the commit result use + * wp11_storage_close_report above, which stays in the wp11_storage_* namespace + * the other internal wrappers use so it cannot be mistaken for part of the + * backend contract. */ +void wolfPKCS11_Store_Close(void* store) +{ + (void)wp11_storage_close_report(store); } /** @@ -1929,7 +1527,7 @@ int wolfPKCS11_Store_Write(void* store, unsigned char* buffer, int len) * @return NOT_AVAILABLE_E when data not available. * @return Other value to indicate failure. */ -static int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, +int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, void** storage) { return wolfPKCS11_Store_Open(type, id1, id2, 1, storage); @@ -1947,7 +1545,7 @@ static int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, * @return NOT_AVAILABLE_E when data not available. * @return Other value to indicate failure. */ -static int wp11_storage_open(int type, CK_ULONG id1, CK_ULONG id2, +int wp11_storage_open(int type, CK_ULONG id1, CK_ULONG id2, int variableSz, void** storage) { #ifdef WOLFPKCS11_TPM_STORE @@ -1969,7 +1567,7 @@ static int wp11_storage_remove(int type, CK_ULONG id1, CK_ULONG id2) * * @param [in] store Context for operation. */ -static void wp11_storage_close(void* storage) +void wp11_storage_close(void* storage) { wolfPKCS11_Store_Close(storage); } @@ -1983,7 +1581,7 @@ static void wp11_storage_close(void* storage) * @return 0 on success. * @return BUFFER_E to indicate failure. */ -static int wp11_storage_read(void* storage, unsigned char* buffer, int len) +int wp11_storage_read(void* storage, unsigned char* buffer, int len) { int ret = 0; unsigned char* p = buffer; @@ -2018,7 +1616,7 @@ static int wp11_storage_read(void* storage, unsigned char* buffer, int len) * @return 0 on success. * @return BUFFER_E to indicate failure. */ -static int wp11_storage_write(void* storage, unsigned char* buffer, int len) +int wp11_storage_write(void* storage, unsigned char* buffer, int len) { int ret = 0; unsigned char* p = buffer; @@ -2333,7 +1931,7 @@ static int wp11_storage_read_array(void* storage, unsigned char* buffer, * @return 0 on success. * @return BUFFER_E to indicate failure. */ -static int wp11_storage_write_array(void* storage, +int wp11_storage_write_array(void* storage, unsigned char* buffer, word32 len) { int ret; @@ -2358,7 +1956,7 @@ static int wp11_storage_write_array(void* storage, * @return BUFFER_E to indicate failure. * @return MEMORY_E when dynamic memory allocation fails. */ -static int wp11_storage_read_alloc_array(void* storage, +int wp11_storage_read_alloc_array(void* storage, unsigned char** buffer, int* len) { int ret; @@ -3208,6 +2806,7 @@ static int wp11_DecryptData(byte* out, byte* data, int len, byte* key, return ret; } + /** * "Decode" the certificate. * @@ -4488,13 +4087,14 @@ static int wp11_Object_Store_EccKey(WP11_Object* object, int tokenId, int objId) #ifdef WOLFPKCS11_MLDSA static int MldsaKeyTryDecode(wc_MlDsaKey* key, byte level, byte* data, - word32 len, CK_OBJECT_CLASS class) + word32 len, CK_OBJECT_CLASS class, int devId) { int ret = 0; word32 idx = 0; - /* Init key */ - ret = wc_MlDsaKey_Init(key, NULL, INVALID_DEVID); + /* Use the object devId, as the import and copy paths do, so a configured + * crypto-callback device is used when there is one. */ + ret = wc_MlDsaKey_Init(key, NULL, devId); if (ret == 0) { /* Set level */ @@ -4548,14 +4148,16 @@ static int wp11_Object_Decode_MldsaKey(WP11_Object* object) if (ret == 0) { /* Decode ML-DSA private key. */ ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_44, - der, len, object->objClass); + der, len, object->objClass, object->devId); if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_65, - der, len, object->objClass); + der, len, object->objClass, + object->devId); } if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_87, - der, len, object->objClass); + der, len, object->objClass, + object->devId); } wc_ForceZero(der, len); } @@ -4566,16 +4168,16 @@ static int wp11_Object_Decode_MldsaKey(WP11_Object* object) /* Decode ML-DSA public key. */ ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_44, object->keyData, object->keyDataLen, - object->objClass); + object->objClass, object->devId); if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_65, object->keyData, object->keyDataLen, - object->objClass); + object->objClass, object->devId); } if (ret != 0) { ret = MldsaKeyTryDecode(object->data.mldsaKey, WC_ML_DSA_87, object->keyData, object->keyDataLen, - object->objClass); + object->objClass, object->devId); } } object->encoded = (ret != 0); @@ -5296,826 +4898,160 @@ static int wp11_Object_Store_MlKemKey(WP11_Object* object, int tokenId, } #endif /* WOLFPKCS11_MLKEM */ -#ifdef WOLFPKCS11_LMS -/* =========================================================================== - * LMS/HSS (RFC 8554): verification and public-key import. - * - * Verify-only: this build imports raw HSS public keys and verifies - * signatures. There is no private-key/keygen/sign or persisted signature - * state here. The public key is self-describing, so wolfSSL derives the - * parameter set from the key bytes on import; the CKA_HSS_LEVELS / - * CKA_HSS_LMS_TYPE / CKA_HSS_LMOTS_TYPE attributes are optional and, when - * supplied, are validated against the imported key. - * ========================================================================= */ - -/* Map tree height -> RFC 8554 LMS typecode. Returns 0 on unknown height. */ -static CK_LMS_TYPE wp11_HssHeightToLmsType(int h) -{ - switch (h) { - case 5: return CKL_LMS_SHA256_M32_H5; - case 10: return CKL_LMS_SHA256_M32_H10; - case 15: return CKL_LMS_SHA256_M32_H15; - case 20: return CKL_LMS_SHA256_M32_H20; - case 25: return CKL_LMS_SHA256_M32_H25; - default: return 0; - } -} - -/* Map Winternitz -> LMOTS typecode. Returns 0 on unknown. */ -static CK_LMOTS_TYPE wp11_HssWToLmotsType(int w) -{ - switch (w) { - case 1: return CKL_LMOTS_SHA256_N32_W1; - case 2: return CKL_LMOTS_SHA256_N32_W2; - case 4: return CKL_LMOTS_SHA256_N32_W4; - case 8: return CKL_LMOTS_SHA256_N32_W8; - default: return 0; - } -} - -/* Import a raw HSS public key into the given wolfSSL LmsKey. wolfSSL derives - * the parameter set from the key bytes (RFC 8554 self-describing public key), - * so no parameters need to be set first. On failure the (partially - * initialized) key is NOT freed here; the caller owns it. */ -static int wp11_HssImportPub(LmsKey* key, int devId, const byte* pub, - word32 pubLen) -{ - int ret; - - ret = wc_LmsKey_Init(key, NULL, devId); - if (ret == 0) - ret = wc_LmsKey_ImportPubRaw(key, pub, pubLen); - return ret; -} +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE -/* Validate caller-supplied HSS parameter attributes against the parameters - * wolfSSL derived from the just-imported public key. Per the PKCS#11 v3.3 HSS - * profile the public-key parameters are the individual CK_ULONG attributes - * CKA_HSS_LEVELS, CKA_HSS_LMS_TYPE (top-level tree height encoding) and - * CKA_HSS_LMOTS_TYPE (Winternitz encoding). Each is optional; a supplied value - * that does not match the key is rejected with BAD_FUNC_ARG. levelsAttr, - * lmsAttr and lmotsAttr are the raw attribute buffers (NULL when absent) with - * lengths levelsLen/lmsLen/lmotsLen. */ -static int wp11_HssCheckParamAttrs(LmsKey* key, - const byte* levelsAttr, word32 levelsLen, - const byte* lmsAttr, word32 lmsLen, - const byte* lmotsAttr, word32 lmotsLen) -{ - int ret, levels = 0, height = 0, winternitz = 0; - CK_ULONG v; +/* 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. + * + * A TPM store is forbidden for stateful private keys (compile-time #error in + * wolfpkcs11/internal.h), so this code path is file-backed-storage only. */ - ret = wc_LmsKey_GetParameters(key, &levels, &height, &winternitz); - if (ret != 0) - return ret; - if (levelsAttr != NULL) { - if (levelsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, levelsAttr, sizeof(v)); - if (v != (CK_ULONG)levels) - return BAD_FUNC_ARG; - } - if (lmsAttr != NULL) { - if (lmsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, lmsAttr, sizeof(v)); - if (v != (CK_ULONG)wp11_HssHeightToLmsType(height)) - return BAD_FUNC_ARG; - } - if (lmotsAttr != NULL) { - if (lmotsLen != sizeof(v)) - return BAD_FUNC_ARG; - XMEMCPY(&v, lmotsAttr, sizeof(v)); - if (v != (CK_ULONG)wp11_HssWToLmotsType(winternitz)) - return BAD_FUNC_ARG; - } - return 0; -} +/* Bytes a state-file name needs on top of the token directory and the scheme + * prefix, both of which vary and are measured by the caller: + * "/wp11_" (6) + "_priv_state_" (12) + two 16-digit hex ids joined by '_' + * (33) + the terminating NUL (1). */ +#define WP11_STATEFUL_STATE_NAME_FIXED_LEN (6 + 12 + 33 + 1) -/* Serialize the public key: keyData holds the raw RFC 8554 HSS public key. */ -static int wp11_Object_Encode_HssKey(WP11_Object* object) +static int wp11_Stateful_StateFile_Path(const char* schemeFilePrefix, + int tokenId, word64 stateId, char* name, int nameLen) { - int ret; - word32 pubLen = 0; + const char* str = NULL; + char homePath[256]; - if (object->objClass != CKO_PUBLIC_KEY) - return NOT_AVAILABLE_E; /* verify-only: no private-key objects */ - - ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); - if (ret == 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) - ret = MEMORY_E; - } - if (ret == 0) { - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, object->keyData, - &pubLen); - if (ret == 0) - object->keyDataLen = (int)pubLen; - } - /* On failure release the buffer so a later store does not skip re-encoding - * (keyData != NULL) and persist an uninitialised buffer with a stale - * length. This is public key material only, so no zeroization is needed. */ - if (ret != 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - return ret; -} - -/* Reconstruct the live wolfSSL key from the serialized public key in keyData. */ -static int wp11_Object_Decode_HssKey(WP11_Object* object) -{ - int ret; - - if (object->objClass != CKO_PUBLIC_KEY) - return NOT_AVAILABLE_E; - - /* Reload from the stored raw public key; wolfSSL re-derives the parameter - * set from the key bytes (no separate parameters are persisted). */ - ret = wp11_HssImportPub(object->data.lmsKey, object->devId, - object->keyData, (word32)object->keyDataLen); - /* The object owns the live key on all paths (the import helper never frees - * it), so encoded stays 0 for WP11_Object_Free to do the single free. */ - object->encoded = 0; - return ret; -} - -static int wp11_Object_Load_HssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret; - void* storage = NULL; - - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_HSSKEY_PUB, tokenId, - objId, &storage); - if (ret == 0) { - ret = wp11_storage_read_alloc_array(storage, &object->keyData, - &object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -static int wp11_Object_Store_HssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret = 0; - void* storage = NULL; - - if (object->keyData == NULL) - ret = wp11_Object_Encode_HssKey(object); - if (ret == 0) - ret = wp11_storage_open(WOLFPKCS11_STORE_HSSKEY_PUB, tokenId, objId, - object->keyDataLen, &storage); - if (ret == 0) { - ret = wp11_storage_write_array(storage, object->keyData, - object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -/* Import an HSS public key from a C_CreateObject template. Following the - * PKCS#11 v3.3 HSS profile the collected attributes are data[0]=CKA_HSS_LEVELS, - * data[1]=CKA_HSS_LMS_TYPE, data[2]=CKA_HSS_LMOTS_TYPE (all optional CK_ULONG) - * and data[3]=CKA_VALUE, the raw RFC 8554 public key. wolfSSL derives the - * parameter set from the key bytes; any supplied CKA_HSS_* attribute is - * validated against the derived parameters and a mismatch is rejected. - * Private-key import is rejected: caller-supplied stateful private material is - * a forgery vector and out of scope for verify-only support. */ -int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, - CK_ULONG* len) -{ - int ret = 0; - LmsKey* newKey = NULL; - - if (object == NULL || data == NULL || len == NULL) - return BAD_FUNC_ARG; - if (object->objClass != CKO_PUBLIC_KEY) - return BAD_FUNC_ARG; - /* A public key must carry its value. There is no keygen path in a - * verify-only build that would populate it later, so an absent - * CKA_VALUE is an incomplete template rather than a deferred import; - * reject it instead of creating an unusable object. */ - if (data[3] == NULL) - return BAD_FUNC_ARG; - - if (object->onToken) - WP11_Lock_LockRW(object->lock); - - /* Import into a temporary key and swap it in only after it fully validates, - * so a failed re-import (C_SetAttributeValue with a new CKA_VALUE) leaves - * the existing key unchanged as PKCS#11 requires. */ - newKey = (LmsKey*)XMALLOC(sizeof(LmsKey), NULL, DYNAMIC_TYPE_KEY); - if (newKey == NULL) - ret = MEMORY_E; - if (ret == 0) { - XMEMSET(newKey, 0, sizeof(LmsKey)); - ret = wp11_HssImportPub(newKey, object->devId, data[3], - (word32)len[3]); - } - if (ret == 0) { - ret = wp11_HssCheckParamAttrs(newKey, data[0], (word32)len[0], - data[1], (word32)len[1], data[2], (word32)len[2]); - } - if (ret == 0) { - /* Replace the live key and drop the cached serialization so a later - * store re-encodes from the new key instead of persisting stale bytes. */ - if (object->data.lmsKey != NULL) { - if (!object->encoded) - wc_LmsKey_Free(object->data.lmsKey); - XFREE(object->data.lmsKey, NULL, DYNAMIC_TYPE_KEY); - } - object->data.lmsKey = newKey; - object->encoded = 0; - newKey = NULL; - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - if (newKey != NULL) { - wc_LmsKey_Free(newKey); - XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); - } - - if (object->onToken) - WP11_Lock_UnlockRW(object->lock); - return ret; -} - -/* Verify an HSS signature over a raw message. Stateless on the verifier. */ -int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, - word32 dataLen, int* stat, WP11_Object* pub) -{ - int ret; - word32 expSigLen = 0; - - if (pub == NULL || pub->data.lmsKey == NULL || stat == NULL) - return BAD_FUNC_ARG; - - /* Verification only reads the key: wc_LmsKey_Verify works from a locally - * allocated LmsState, so a shared RO lock is safe for concurrent verifies - * on the same token object. */ - if (pub->onToken) - WP11_Lock_LockRO(pub->lock); - - /* A signature whose length does not match the key's parameter set cannot - * be valid; report it as an invalid signature (stat = 0 -> - * CKR_SIGNATURE_INVALID) rather than letting wolfSSL's length check - * surface as a function failure. */ - if (wc_LmsKey_GetSigLen(pub->data.lmsKey, &expSigLen) == 0 && - sigLen != expSigLen) { - *stat = 0; - ret = 0; - } - else { - /* Unlike XMSS, whose wolfSSL verify rejects a zero-length message with - * BAD_FUNC_ARG, wc_LmsKey_Verify accepts an empty message, so HSS needs - * no dataLen == 0 special-case here (see WP11_Xmss_Verify). */ - ret = wc_LmsKey_Verify(pub->data.lmsKey, sig, sigLen, data, - (int)dataLen); - /* Only a genuine verification mismatch (SIG_VERIFY_E) maps to stat=0. - * Remaining argument/state errors surface as a function failure so - * they are not silently reported as an invalid signature. */ - if (ret == 0) { - *stat = 1; - } - else if (ret == SIG_VERIFY_E) { - *stat = 0; - ret = 0; - } - else { - *stat = 0; - } - } - - if (pub->onToken) - WP11_Lock_UnlockRO(pub->lock); - - return ret; -} - -/* Serve HSS-specific attributes from a public-key object. */ -static int HssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, - byte* data, CK_ULONG* len) -{ - int ret = 0; - int levels = 0, height = 0, winternitz = 0; - - if (object == NULL || object->data.lmsKey == NULL || len == NULL) - return BAD_FUNC_ARG; - - switch (type) { - case CKA_HSS_LEVELS: { - CK_ULONG v; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - v = (CK_ULONG)levels; - if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - } - break; - } - case CKA_HSS_LMS_TYPE: { - CK_LMS_TYPE v; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - v = wp11_HssHeightToLmsType(height); - if (v == 0) - ret = NOT_AVAILABLE_E; - else if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - } - break; - } - case CKA_HSS_LMOTS_TYPE: { - CK_LMOTS_TYPE v; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - v = wp11_HssWToLmotsType(winternitz); - if (v == 0) - ret = NOT_AVAILABLE_E; - else if (data == NULL) - *len = sizeof(v); - else if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - } - else { - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - } - break; + 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; } - case CKA_HSS_LMS_TYPES: - case CKA_HSS_LMOTS_TYPES: { - /* wolfSSL uses uniform LMS/LMOTS parameters across all HSS levels, so - * every array entry is the top-level type. Per-level types (allowed - * by the PKCS#11 v3.3 profile) are not expressible by wolfSSL and are - * not supported here. */ - CK_ULONG total; - CK_ULONG fill; - int i; - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, - &height, &winternitz); - if (ret == 0) { - total = (CK_ULONG)(sizeof(CK_ULONG) * levels); - fill = (type == CKA_HSS_LMS_TYPES) - ? wp11_HssHeightToLmsType(height) - : wp11_HssWToLmotsType(winternitz); - if (fill == 0) - ret = NOT_AVAILABLE_E; - else if (data == NULL) - *len = total; - else if (*len < total) { - *len = total; - ret = BUFFER_E; - } - else { - for (i = 0; i < levels; i++) - XMEMCPY(data + i * sizeof(CK_ULONG), &fill, - sizeof(CK_ULONG)); - *len = total; - } - } - break; +#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; } - case CKA_HSS_KEYS_REMAINING: - /* Remaining-signature count is a private-key/state property; not - * available in a verify-only build. Return NOT_AVAILABLE_E like the - * default arm; C_GetAttributeValue then sets ulValueLen. */ - ret = NOT_AVAILABLE_E; - break; - case CKA_VALUE: - if (object->objClass == CKO_PUBLIC_KEY) { - word32 pubLen = 0; - ret = wc_LmsKey_GetPubLen(object->data.lmsKey, &pubLen); - if (ret == 0) { - if (data == NULL) - *len = pubLen; - else if (*len < pubLen) { - *len = pubLen; - ret = BUFFER_E; - } - else { - ret = wc_LmsKey_ExportPubRaw(object->data.lmsKey, - data, &pubLen); - if (ret == 0) - *len = pubLen; - } - } - } - else { - *len = CK_UNAVAILABLE_INFORMATION; - ret = NOT_AVAILABLE_E; - } - break; - default: - ret = NOT_AVAILABLE_E; - break; - } - return ret; -} - -/* Validate a supplied CKA_HSS_LMS_TYPES / CKA_HSS_LMOTS_TYPES array against the - * imported public key. wolfSSL uses uniform LMS/LMOTS parameters across all HSS - * levels, so a conforming array holds one CK_ULONG per level, each equal to the - * single top-level type served by HssObject_GetAttr. A wrong length or any - * differing entry is rejected so a malformed or mismatched template is not - * silently accepted; a matching array (as read back from this key) is accepted. - * Returns 0 when the array matches, BAD_FUNC_ARG otherwise. The caller holds the - * object lock. */ -static int wp11_HssCheckTypesAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, - const byte* data, CK_ULONG len) -{ - int ret; - int levels = 0; - int height = 0; - int winternitz = 0; - int i; - CK_ULONG expected; - CK_ULONG entry; - - if (object->data.lmsKey == NULL || data == NULL) - return BAD_FUNC_ARG; - - ret = wc_LmsKey_GetParameters(object->data.lmsKey, &levels, &height, - &winternitz); - if (ret != 0) - return ret; - - expected = (type == CKA_HSS_LMS_TYPES) - ? (CK_ULONG)wp11_HssHeightToLmsType(height) - : (CK_ULONG)wp11_HssWToLmotsType(winternitz); - if (expected == 0) - return BAD_FUNC_ARG; - if (len != (CK_ULONG)(sizeof(CK_ULONG) * levels)) - return BAD_FUNC_ARG; - - for (i = 0; i < levels; i++) { - XMEMCPY(&entry, data + i * sizeof(CK_ULONG), sizeof(entry)); - if (entry != expected) - return BAD_FUNC_ARG; +#endif } - - return 0; -} -#endif /* WOLFPKCS11_LMS */ - -#ifdef WOLFPKCS11_XMSS -/* =========================================================================== - * XMSS/XMSS^MT (RFC 8391): verification and public-key import. - * - * Verify-only. One wolfSSL XmssKey type serves both XMSS and XMSS^MT; the - * PKCS#11 key type (CKK_XMSS vs CKK_XMSSMT) selects which via the is_xmssmt - * flag. The raw public key carries the parameter-set OID in its leading - * bytes, so wc_XmssKey_ImportPubRaw_ex derives the parameter set on import. - * A CKA_PARAMETER_SET attribute is optional and, when supplied, is validated - * against the OID in the key. - * ========================================================================= */ - -/* Length of the parameter-set OID prefix in a raw RFC 8391 public key. wolfSSL - * only exposes XMSS_OID_LEN when built with WOLFSSL_HAVE_XMSS and it is absent - * from some released headers, so define our own to keep the build independent - * of the wolfSSL version. */ -#define WP11_XMSS_OID_LEN 4 - -/* Big-endian 32-bit read from a byte buffer (used to read the XMSS OID). */ -static word32 wp11_HbsReadU32(const byte* p) -{ - return ((word32)p[0] << 24) | ((word32)p[1] << 16) - | ((word32)p[2] << 8) | (word32)p[3]; -} - -/* Import a raw XMSS/XMSS^MT public key into the given wolfSSL XmssKey, deriving - * the parameter set from the embedded OID. is_xmssmt disambiguates the XMSS and - * XMSS^MT OID namespaces (which overlap) and comes from the PKCS#11 key type. - * When the caller supplies a CKA_PARAMETER_SET (params != NULL, the OID as a - * CK_ULONG) it must match the OID in the public key. On failure the key is NOT - * freed here; the caller owns it. */ -static int wp11_XmssImportPub(XmssKey* key, int is_xmssmt, int devId, - const byte* pub, word32 pubLen, const byte* params, word32 paramsLen) -{ - int ret; - - ret = wc_XmssKey_Init(key, NULL, devId); - if (ret == 0 && params != NULL) { - CK_ULONG userOid; - if (paramsLen != sizeof(CK_ULONG) || pubLen < WP11_XMSS_OID_LEN) { - ret = BAD_FUNC_ARG; - } - else { - XMEMCPY(&userOid, params, sizeof(userOid)); - if (userOid != (CK_ULONG)wp11_HbsReadU32(pub)) - ret = BAD_FUNC_ARG; - } +#ifdef WOLFPKCS11_DEFAULT_TOKEN_PATH + if (str == NULL) + str = WC_STRINGIFY(WOLFPKCS11_DEFAULT_TOKEN_PATH); +#endif + if (str == NULL) + return -1; + if (nameLen <= WP11_STATEFUL_STATE_NAME_FIXED_LEN) + return -1; + if (XSTRLEN(str) + XSTRLEN(schemeFilePrefix) > + (size_t)(nameLen - WP11_STATEFUL_STATE_NAME_FIXED_LEN)) { + return -1; } - if (ret == 0) - ret = wc_XmssKey_ImportPubRaw_ex(key, pub, pubLen, is_xmssmt); - return ret; + return XSNPRINTF(name, nameLen, + "%s/wp11_%s_priv_state_%016lx_%016llx", + str, schemeFilePrefix, (unsigned long)tokenId, + (unsigned long long)stateId); } -/* Serialize the public key: keyData holds the raw RFC 8391 public key. */ -static int wp11_Object_Encode_XmssKey(WP11_Object* object) +/* Open the state file for read or atomic-rename write. Mirrors + * wolfPKCS11_Store_OpenSz's file-mode body but uses the word64-safe path. */ +int wp11_Stateful_StateFile_Open(const char* schemeFilePrefix, + int tokenId, word64 stateId, int read, void** store) { int ret; - word32 pubLen = 0; + char name[WP11_STORE_MAX_PATH] = ""; + WP11_FileStoreCtx* ctx = NULL; - if (object->objClass != CKO_PUBLIC_KEY) +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) return NOT_AVAILABLE_E; - - ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + ret = 0; + else + ret = -1; if (ret == 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = (unsigned char*)XMALLOC(pubLen, NULL, + ctx = (WP11_FileStoreCtx*)XMALLOC(sizeof(*ctx), NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (object->keyData == NULL) + if (ctx == NULL) ret = MEMORY_E; } if (ret == 0) { - ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, object->keyData, - &pubLen); - if (ret == 0) - object->keyDataLen = (int)pubLen; - } - /* On failure release the buffer so a later store does not skip re-encoding - * (keyData != NULL) and persist an uninitialised buffer with a stale - * length. This is public key material only, so no zeroization is needed. */ - if (ret != 0) { - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; - } - return ret; -} - -static int wp11_Object_Decode_XmssKey(WP11_Object* object) -{ - int ret; - - if (object->objClass != CKO_PUBLIC_KEY) - return NOT_AVAILABLE_E; - - /* Reload from the stored raw public key; wolfSSL re-derives the parameter - * set from the embedded OID (no separate parameter set is persisted). */ - ret = wp11_XmssImportPub(object->data.xmssKey, - (object->type == CKK_XMSSMT) ? 1 : 0, object->devId, object->keyData, - (word32)object->keyDataLen, NULL, 0); - /* Object owns the key on all paths; encoded stays 0 for the single free in - * WP11_Object_Free. See wp11_Object_Decode_HssKey. */ - object->encoded = 0; - return ret; -} - -static int wp11_Object_Load_XmssKey(WP11_Object* object, int tokenId, int objId) -{ - int ret; - void* storage = NULL; - - ret = wp11_storage_open_readonly(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, - objId, &storage); - if (ret == 0) { - ret = wp11_storage_read_alloc_array(storage, &object->keyData, - &object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -static int wp11_Object_Store_XmssKey(WP11_Object* object, int tokenId, - int objId) -{ - int ret = 0; - void* storage = NULL; - - if (object->keyData == NULL) - ret = wp11_Object_Encode_XmssKey(object); - if (ret == 0) - ret = wp11_storage_open(WOLFPKCS11_STORE_XMSSKEY_PUB, tokenId, objId, - object->keyDataLen, &storage); - if (ret == 0) { - ret = wp11_storage_write_array(storage, object->keyData, - object->keyDataLen); - wp11_storage_close(storage); - } - return ret; -} - -/* Import an XMSS/XMSS^MT public key from a C_CreateObject template. data[1] is - * the raw RFC 8391 public key; data[0] (CKA_PARAMETER_SET, the OID as a - * CK_ULONG) is optional and, when present, is validated against the key. - * Private-key import is rejected. */ -int WP11_Object_SetXmssKey(WP11_Object* object, unsigned char** data, - CK_ULONG* len) -{ - int ret = 0; - XmssKey* newKey = NULL; - - if (object == NULL || data == NULL || len == NULL) - return BAD_FUNC_ARG; - if (object->objClass != CKO_PUBLIC_KEY) - return BAD_FUNC_ARG; - /* A public key must carry its value; an absent CKA_VALUE is an - * incomplete template (no keygen path fills it later). Reject it. */ - if (data[1] == NULL) - return BAD_FUNC_ARG; + char dirPath[WP11_STORE_MAX_PATH]; + size_t nameSz = XSTRLEN(name); - if (object->onToken) - WP11_Lock_LockRW(object->lock); + XMEMSET(ctx, 0, sizeof(*ctx)); + ctx->file = XBADFILE; + ctx->is_write = (read == 0); + if (nameSz >= sizeof(ctx->final_name)) + ret = READ_ONLY_E; + else + XMEMCPY(ctx->final_name, name, nameSz + 1); - /* Import into a temporary key and swap it in only after it validates, so a - * failed re-import leaves the existing key unchanged (PKCS#11 requires a - * failed C_SetAttributeValue to be a no-op). */ - newKey = (XmssKey*)XMALLOC(sizeof(XmssKey), NULL, DYNAMIC_TYPE_KEY); - if (newKey == NULL) - ret = MEMORY_E; - if (ret == 0) { - XMEMSET(newKey, 0, sizeof(XmssKey)); - ret = wp11_XmssImportPub(newKey, (object->type == CKK_XMSSMT) ? 1 : 0, - object->devId, data[1], (word32)len[1], data[0], (word32)len[0]); + if (ret == 0 && read) { + ctx->file = XFOPEN(name, "rb"); + if (ctx->file == NULL) + ret = NOT_AVAILABLE_E; + } + else if (ret == 0) { + ret = wolfPKCS11_StoreDirName(name, dirPath, (int)sizeof(dirPath)); + if (ret == 0) + ret = wolfPKCS11_StoreEnsureDir(dirPath); + if (ret == 0) + ret = wolfPKCS11_StoreCreateTempFile(ctx, dirPath); + } } + if (ret == 0 && (ctx->file == NULL || ctx->file == XBADFILE)) + ret = READ_ONLY_E; if (ret == 0) { - /* Replace the live key and drop the cached serialization so a later - * store re-encodes from the new key. */ - if (object->data.xmssKey != NULL) { - if (!object->encoded) - wc_XmssKey_Free(object->data.xmssKey); - XFREE(object->data.xmssKey, NULL, DYNAMIC_TYPE_KEY); - } - object->data.xmssKey = newKey; - object->encoded = 0; - newKey = NULL; - XFREE(object->keyData, NULL, DYNAMIC_TYPE_TMP_BUFFER); - object->keyData = NULL; - object->keyDataLen = 0; + *store = ctx; } - if (newKey != NULL) { - wc_XmssKey_Free(newKey); - XFREE(newKey, NULL, DYNAMIC_TYPE_KEY); + else if (ctx != NULL) { + if (ctx->file != NULL && ctx->file != XBADFILE) + XFCLOSE(ctx->file); + if (ctx->has_temp) + wolfPKCS11_StoreAbortTemp(ctx); + XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); } - - if (object->onToken) - WP11_Lock_UnlockRW(object->lock); return ret; } -/* Verify an XMSS/XMSS^MT signature over a raw message. Stateless. */ -int WP11_Xmss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, - word32 dataLen, int* stat, WP11_Object* pub) +int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, + int tokenId, word64 stateId) { int ret; - word32 expSigLen = 0; + char name[WP11_STORE_MAX_PATH]; - if (pub == NULL || pub->data.xmssKey == NULL || stat == NULL) - return BAD_FUNC_ARG; - - /* Verification only reads the key: wc_XmssKey_Verify works from a locally - * allocated XmssState, so a shared RO lock is safe for concurrent verifies - * on the same token object. */ - if (pub->onToken) - WP11_Lock_LockRO(pub->lock); - - /* A wrong-length signature cannot be valid for this key; report it as an - * invalid signature rather than a function failure. wolfSSL's XMSS verify - * also rejects a zero-length message with BAD_FUNC_ARG; treat that the same - * way (an invalid verification, not an internal failure). */ - if (wc_XmssKey_GetSigLen(pub->data.xmssKey, &expSigLen) == 0 && - sigLen != expSigLen) { - *stat = 0; - ret = 0; - } - else if (dataLen == 0) { - *stat = 0; - ret = 0; - } - else { - ret = wc_XmssKey_Verify(pub->data.xmssKey, sig, sigLen, data, - (int)dataLen); - /* Only SIG_VERIFY_E maps to stat=0; other argument/state errors - * surface as a function failure so they are not silently reported as - * an invalid signature. */ - if (ret == 0) { - *stat = 1; - } - else if (ret == SIG_VERIFY_E) { - *stat = 0; - ret = 0; - } - else { - *stat = 0; - } - } - - if (pub->onToken) - WP11_Lock_UnlockRO(pub->lock); - - return ret; +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_NO_STORE") != NULL) + return NOT_AVAILABLE_E; +#endif + ret = wp11_Stateful_StateFile_Path(schemeFilePrefix, tokenId, stateId, + name, sizeof(name)); + if (ret > 0 && ret < (int)sizeof(name)) + return remove(name); + return -1; } +#endif /* WOLFPKCS11_STATEFUL_SIG_PRIVATE */ -/* Serve XMSS-specific attributes from a public-key object. */ -static int XmssObject_GetAttr(WP11_Object* object, CK_ATTRIBUTE_TYPE type, - byte* data, CK_ULONG* len) -{ - int ret = 0; - - if (object == NULL || object->data.xmssKey == NULL || len == NULL) - return BAD_FUNC_ARG; - - switch (type) { - case CKA_VALUE: - if (object->objClass == CKO_PUBLIC_KEY) { - word32 pubLen = 0; - ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); - if (ret == 0) { - if (data == NULL) - *len = pubLen; - else if (*len < pubLen) { - *len = pubLen; - ret = BUFFER_E; - } - else { - ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, - data, &pubLen); - if (ret == 0) - *len = pubLen; - } - } - } - else { - *len = CK_UNAVAILABLE_INFORMATION; - ret = NOT_AVAILABLE_E; - } - break; - case CKA_PARAMETER_SET: { - /* The XMSS parameter set is identified by the OID in the leading - * bytes of the raw public key; return it as a CK_ULONG. */ - CK_ULONG v; - word32 pubLen = 0; - byte* raw; - - if (object->objClass != CKO_PUBLIC_KEY) { - *len = CK_UNAVAILABLE_INFORMATION; - ret = NOT_AVAILABLE_E; - break; - } - if (data == NULL) { - *len = sizeof(v); - break; - } - if (*len < sizeof(v)) { - *len = sizeof(v); - ret = BUFFER_E; - break; - } - ret = wc_XmssKey_GetPubLen(object->data.xmssKey, &pubLen); - if (ret == 0 && pubLen < WP11_XMSS_OID_LEN) - ret = NOT_AVAILABLE_E; - if (ret == 0) { - raw = (byte*)XMALLOC(pubLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (raw == NULL) { - ret = MEMORY_E; - } - else { - ret = wc_XmssKey_ExportPubRaw(object->data.xmssKey, raw, - &pubLen); - if (ret == 0) { - v = (CK_ULONG)wp11_HbsReadU32(raw); - XMEMCPY(data, &v, sizeof(v)); - *len = sizeof(v); - } - XFREE(raw, NULL, DYNAMIC_TYPE_TMP_BUFFER); - } - } - break; - } - default: - ret = NOT_AVAILABLE_E; - break; - } - return ret; -} -#endif /* WOLFPKCS11_XMSS */ /** * Decode the symmetric key - requires decryption. @@ -6695,6 +5631,19 @@ static int wp11_Object_Decode(WP11_Object* object) #ifdef WOLFPKCS11_LMS case CKK_HSS: ret = wp11_Object_Decode_HssKey(object); + #ifdef WOLFPKCS11_LMS_PRIVATE + /* A stateful private key that cannot be reloaded must not take + * the whole token down with it. wc_LmsKey_Reload fails for an + * exhausted key as well as a damaged one, and exhaustion is + * the normal end of life for every LMS key, so this is an + * expected outcome rather than an error. Decode_HssKey has + * already left the object poisoned, which makes C_Sign on it + * return CKR_DEVICE_ERROR while every other object on the + * token stays usable - including for the C_DestroyObject that + * clears the spent key. */ + if (ret != 0 && object->objClass == CKO_PRIVATE_KEY) + ret = 0; + #endif break; #endif #ifdef WOLFPKCS11_XMSS @@ -6793,6 +5742,20 @@ static int wp11_Object_Encode(WP11_Object* object, int protect) #ifdef WOLFPKCS11_LMS case CKK_HSS: ret = wp11_Object_Encode_HssKey(object); + #ifdef WOLFPKCS11_LMS_PRIVATE + if (protect && ret == 0 && object->objClass == CKO_PRIVATE_KEY) { + /* Release the live signing key like the other private-key + * types do. wc_LmsKey_Free is what frees the heap-allocated + * priv_data and zeroizes the raw private key; a later + * wc_LmsKey_Init would only memset the handle and leak it. + * The durable state file is untouched, so the next login + * reloads the key from it. */ + wc_LmsKey_Free(object->data.lmsKey); + object->encoded = 1; + /* The state is no longer live in memory. */ + object->opFlag &= ~WP11_FLAG_STATEFUL_STATE_VALID; + } + #endif break; #endif #ifdef WOLFPKCS11_XMSS @@ -6824,11 +5787,21 @@ static int wp11_Object_Encode(WP11_Object* object, int protect) * * Empties the contents of the object. * - * @param [in] object Key object. - * @param [in] tokenId Id of token this key belongs to. - * @param [in] objId Id of object for token. + * Called for two different reasons, which the destroying flag separates. When + * an object is really being removed from the token it is 1, and state held + * outside the object store goes with it. When a surviving object is only being + * renumbered after some other object was removed, it is 0: the caller unstores + * it at its old id and re-stores it at the new one, so anything that is not + * keyed by the object id must be left alone. + * + * @param [in] object Key object. + * @param [in] tokenId Id of token this key belongs to. + * @param [in] objId Id of object for token. + * @param [in] destroying 1 when the object is being removed, 0 when it is + * only being renumbered. */ -static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) +static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId, + int destroying) { int ret; int storeObjType = -1; @@ -6901,6 +5874,18 @@ static int wp11_Object_Unstore(WP11_Object* object, int tokenId, int objId) #ifdef WOLFPKCS11_LMS case CKK_HSS: storeObjType = WOLFPKCS11_STORE_HSSKEY_PUB; + #ifdef WOLFPKCS11_LMS_PRIVATE + if (object->objClass == CKO_PRIVATE_KEY) { + storeObjType = WOLFPKCS11_STORE_HSSKEY_PRIV; + /* The signature-state file lives outside the object store and + * would orphan on disk after C_DestroyObject. It is keyed by + * the per-key nonce rather than the object id, so re-storing a + * renumbered object would never recreate it - only remove it + * when the key really is going away. */ + if (destroying) + wp11_Hss_RemoveStoredState(object, tokenId, objId); + } + #endif break; #endif #ifdef WOLFPKCS11_XMSS @@ -7555,6 +6540,12 @@ static int wp11_Slot_Init(WP11_Slot* slot, int id) XMEMSET(slot, 0, sizeof(*slot)); slot->id = id; + /* Default to pure software. Must be INVALID_DEVID, not the 0 left by the + * XMEMSET above: 0 is a valid crypto-callback device slot, so a 0 devId + * routes every key through the crypto-callback dispatch even when no device + * is registered. The hardware-offload paths below override this with a real + * device id. */ + slot->devId = INVALID_DEVID; slot->token.state = WP11_TOKEN_STATE_UNKNOWN; slot->token.tokenFlags = 0; @@ -7644,6 +6635,12 @@ int WP11_Library_Init(void) #endif if (libraryInitCount == 0) { +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + /* Resolve the relax-fsync setting once, here, while only one thread + * can be running - the sign path reads it without a lock that would + * cover a lazy initialiser. */ + WP11_Stateful_InitRelaxFsync(); +#endif ret = WP11_Lock_Init(&globalLock); if (ret == 0) { @@ -8885,6 +7882,9 @@ static int wp11_init_get_op_category(int init) case WP11_INIT_AES_CMAC_SIGN: case WP11_INIT_TLS_MAC_SIGN: case WP11_INIT_MLDSA_SIGN: +#ifdef WOLFPKCS11_LMS_PRIVATE + case WP11_INIT_HSS_SIGN: +#endif return WP11_OP_SIGN; case WP11_INIT_HMAC_VERIFY: @@ -9876,7 +8876,8 @@ int WP11_Session_RemoveObjectByHandle(WP11_Session* session, #ifndef WOLFPKCS11_NO_STORE /* remove any id's with higher value */ - ret = wp11_Object_Unstore(*curr, (int)session->slotId, id); + /* Renumbering a surviving object, not destroying it. */ + ret = wp11_Object_Unstore(*curr, (int)session->slotId, id, 0); #ifdef DEBUG_WOLFPKCS11 if (ret != 0) { printf("Failed to unstore slot %d, id: %d, ret: %d, continuing...\n", @@ -9891,7 +8892,7 @@ int WP11_Session_RemoveObjectByHandle(WP11_Session* session, } #ifndef WOLFPKCS11_NO_STORE - ret = wp11_Object_Unstore(object, (int)session->slotId, id); + ret = wp11_Object_Unstore(object, (int)session->slotId, id, 1); #ifdef DEBUG_WOLFPKCS11 if (ret != 0) { printf("Failed to unstore slot %d, id: %d, ret: %d\n", diff --git a/src/slot.c b/src/slot.c index 3789cae7..6c97b9a6 100644 --- a/src/slot.c +++ b/src/slot.c @@ -384,6 +384,9 @@ static CK_MECHANISM_TYPE mechanismList[] = { #endif #ifdef WOLFPKCS11_LMS CKM_HSS, +#ifdef WOLFPKCS11_LMS_PRIVATE + CKM_HSS_KEY_PAIR_GEN, +#endif #endif #ifdef WOLFPKCS11_XMSS CKM_XMSS, @@ -648,13 +651,23 @@ static CK_MECHANISM_INFO mldsaMechInfo = { }; #endif #ifdef WOLFPKCS11_LMS -/* Info on the HSS (LMS) verify mechanism. PKCS#11 does not define a key-size - * semantics for stateful hash-based signatures (bits vs bytes), so report the - * "not applicable" 0..0 envelope. Verify-only: no CKF_SIGN. */ +/* Info on the HSS (LMS) sign/verify mechanism. PKCS#11 does not define a + * key-size semantics for stateful hash-based signatures (bits vs bytes), so + * report the "not applicable" 0..0 envelope. CKF_SIGN is added only when + * keygen+signing is built in. */ static CK_MECHANISM_INFO hssMechInfo = { 0, 0, CKF_VERIFY +#ifdef WOLFPKCS11_LMS_PRIVATE + | CKF_SIGN +#endif +}; +#ifdef WOLFPKCS11_LMS_PRIVATE +/* Info on the HSS key-pair generation mechanism. */ +static CK_MECHANISM_INFO hssKgMechInfo = { + 0, 0, CKF_GENERATE_KEY_PAIR }; #endif +#endif #ifdef WOLFPKCS11_XMSS /* Info on the XMSS / XMSS^MT verify mechanisms. 0..0 envelope, verify-only. */ static CK_MECHANISM_INFO xmssMechInfo = { @@ -1023,6 +1036,11 @@ CK_RV C_GetMechanismInfo(CK_SLOT_ID slotID, CK_MECHANISM_TYPE type, case CKM_HSS: XMEMCPY(pInfo, &hssMechInfo, sizeof(CK_MECHANISM_INFO)); break; +#ifdef WOLFPKCS11_LMS_PRIVATE + case CKM_HSS_KEY_PAIR_GEN: + XMEMCPY(pInfo, &hssKgMechInfo, sizeof(CK_MECHANISM_INFO)); + break; +#endif #endif #ifdef WOLFPKCS11_XMSS case CKM_XMSS: diff --git a/src/wp11_types.h b/src/wp11_types.h new file mode 100644 index 00000000..0f80a10b --- /dev/null +++ b/src/wp11_types.h @@ -0,0 +1,626 @@ +/* wp11_types.h + * + * Copyright (C) 2006-2023 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfPKCS11 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Layout of the core wolfPKCS11 objects, private to src/. + * + * wolfpkcs11/internal.h is the installed header that the rest of the library + * and the test programs code against, and it keeps WP11_Object, WP11_Session, + * WP11_Token and WP11_Slot opaque on purpose. This header carries their actual + * layout, along with the helpers internal.c provides to its sibling source + * files. + * + * Include it only from the sources that genuinely need the layout (internal.c + * and hbs.c). crypto.c, slot.c and wolfpkcs11.c stay on the opaque interface, + * which is what stops them reaching into object internals. Never installed. + */ + +#ifndef WOLFPKCS11_WP11_TYPES_H +#define WOLFPKCS11_WP11_TYPES_H + +#ifdef HAVE_CONFIG_H + #include +#endif +#include "wolfpkcs11/pkcs11.h" + +#ifndef WOLFSSL_USER_SETTINGS +#include +#else +#include "user_settings.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef WOLFPKCS11_MLKEM +#include +#endif + +#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) +/* OS-specific includes for directory creation */ +#if defined(_WIN32) || defined(_MSC_VER) + #include + #include + #define MKDIR(path) _mkdir(path) +#else + #include + #define MKDIR(path) mkdir(path, 0700) +#endif +#endif + +#if !defined(WOLFPKCS11_NO_STORE) && !defined(WOLFPKCS11_CUSTOM_STORE) + #include + #include + #if defined(_WIN32) || defined(_MSC_VER) + #include + #include + #include + #include + #else + #include + #include + #include + #endif +#endif + +#include +#include + +#ifdef WOLFPKCS11_TPM + #include + #include + + #ifndef WOLFPKCS11_TPM_CUST_IO + #include + #ifndef TPM2_IOCB_CTX + #define TPM2_IOCB_CTX NULL + #endif + #endif +#endif + +#ifdef WOLFSSL_MAXQ10XX_CRYPTO +#include +#include +#define MAX_CERT_DATASIZE 2048 +#define MAX_SIG_DATASIZE 64 +#define ECC_KEYCOMPLEN 32 +#endif /* WOLFSSL_MAXQ10XX_CRYPTO */ + +#if defined(WC_RSA_BLINDING) && (!defined(HAVE_FIPS) || \ + (defined(HAVE_FIPS_VERSION) && (HAVE_FIPS_VERSION > 2))) +#define WOLFPKCS11_NEED_RSA_RNG +#endif + +#if defined(WOLFPKCS11_TPM) && defined(WOLFSSL_MAXQ10XX_CRYPTO) + #error "wolfTPM and MAXQ10XX are incompatible with each other." +#endif + +/* Helper to get size of struct field */ +#define FIELD_SIZE(type, field) (sizeof(((type *)0)->field)) + +/* Size of hash calculated from PIN. */ +#define PIN_HASH_SZ 32 +/* Size of seed used when calculating hash from PIN. */ +#define PIN_SEED_SZ 16 +/* Size of token's label. */ +#define LABEL_SZ 32 + +/* Length of seed from global random to seed local random. */ +#define RNG_SEED_SZ 32 + + + +/* Maximum size of storage for generated/derived DH key. */ +#ifdef WOLFPKCS11_NSS +#define WP11_MAX_DH_KEY_SZ (8192/8) +#else +#define WP11_MAX_DH_KEY_SZ (4096/8) +#endif + +/* Maximum size of storage for generated/derived symmetric key. */ +#ifdef WOLFPKCS11_NSS +#define WP11_MAX_SYM_KEY_SZ (2048) +#elif !defined(NO_DH) +#define WP11_MAX_SYM_KEY_SZ (4096/8) +#elif defined(HAVE_ECC) +#define WP11_MAX_SYM_KEY_SZ ((521+7)/8) +#else +#define WP11_MAX_SYM_KEY_SZ 64 +#endif + +#ifndef WP11_MAX_CERT_SZ +#define WP11_MAX_CERT_SZ 4096 +#endif + +#define PKCS11_CHECK_VALUE_SIZE 3 + +/* Sizes for storage. */ +#define WP11_MAX_IV_SZ 16 +#define WP11_MAX_GCM_NONCE_SZ 16 +#define WP11_MAX_GCM_TAG_SZ 16 +#define WP11_MAX_GCM_TAG_BITS 128 + +/* wolfSSL ECC signatures are ASN.1 encoding - need to encode and decode. */ +#define ASN_INTEGER 0x02 +#define ASN_OCTET_STRING 0x04 +#define ASN_OBJECT_ID 0x06 +#define ASN_SEQUENCE 0x10 +#define ASN_CONSTRUCTED 0x20 +#define ASN_LONG_LENGTH 0x80 + +/* Create a session handle from slot id and session id. */ +#define SESS_HANDLE(slot, s) (((slot) << 16) | (s)) +/* Determine slot id from session handle. */ +#define SESS_HANDLE_SLOT_ID(s) ((CK_SLOT_ID)((s) >> 16)) +/* Determine session id from session handle. */ +#define SESS_HANDLE_SESS_ID(s) ((s) & 0xffff) + +/* Create an object handle from a onToken and object id. */ +#define OBJ_HANDLE(on, i) (((on) << 28) | (i)) +/* Determine whether object is onToken from object handle. */ +#define OBJ_HANDLE_ON_TOKEN(h) ((int)((h) >> 28)) +/* Determine object id from object handle. */ +#define OBJ_HANDLE_OBJ_ID(h) ((h) & 0xfffffff) + +#ifdef SINGLE_THREADED +/* Disable locking. */ +typedef int WP11_Lock; + +#define WP11_Lock_Init(l) ({ 0; }) +#define WP11_Lock_Free(l) +#define WP11_Lock_LockRW(l) ({ 0; }) +#define WP11_Lock_UnlockRW(l) ({ 0; }) +#define WP11_Lock_LockRO(l) ({ 0; }) +#define WP11_Lock_UnlockRO(l) ({ 0; }) + +#else +typedef struct WP11_Lock { + wolfSSL_Mutex read; /* Mutex for accessing count */ + wolfSSL_Mutex write; /* Mutex for writing */ + int cnt; /* Count of readers */ +} WP11_Lock; +#endif + +/* Symmetric key data. */ +typedef struct WP11_Data { + byte data[WP11_MAX_SYM_KEY_SZ]; /* Key data */ + word32 len; /* Length of key data in bytes */ +} WP11_Data; + +typedef struct WP11_GenericData { + byte *data; + word32 dataLen; + byte *application; + word32 applicationLen; + byte *objectId; /* CKA_OBJECT_ID, a DER encoded ID */ + word32 objectIdLen; +} WP11_GenericData; + +/* Certificate */ +typedef struct WP11_Cert { + byte *data; /* Certificate data */ + word32 len; /* Length of certificate data in bytes */ + CK_CERTIFICATE_TYPE type; +} WP11_Cert; + +typedef struct WP11_Trust { + byte sha1Hash[WC_SHA_DIGEST_SIZE]; + byte md5Hash[WC_MD5_DIGEST_SIZE]; + CK_ULONG serverAuth; + CK_ULONG clientAuth; + CK_ULONG emailProtection; + CK_ULONG codeSigning; + CK_BBOOL stepUpApproved; +} WP11_Trust; + +#ifndef NO_DH +typedef struct WP11_DhKey { + byte key[WP11_MAX_DH_KEY_SZ]; /* Public or private key */ + word32 len; /* Length of public key */ + DhKey params; /* DH parameters object */ +} WP11_DhKey; +#endif + +/* When adding/modifying new members, add support in WP11_Object_Copy */ +struct WP11_Object { + union { + #ifndef NO_RSA + RsaKey* rsaKey; /* RSA key object */ + #endif + #ifdef HAVE_ECC + ecc_key* ecKey; /* EC key object */ + #endif + #ifdef WOLFPKCS11_MLDSA + wc_MlDsaKey* mldsaKey; /* ML-DSA key object */ + #endif + #ifndef NO_DH + WP11_DhKey* dhKey; /* DH parameters object */ + #endif + #ifdef WOLFPKCS11_MLKEM + MlKemKey* mlKemKey; /* ML-KEM key object */ + #endif + #ifdef WOLFPKCS11_LMS + LmsKey* lmsKey; /* LMS/HSS key object (verify) */ + #endif + #ifdef WOLFPKCS11_XMSS + XmssKey* xmssKey; /* XMSS/XMSS^MT key object (verify) */ + #endif + WP11_Data* symmKey; /* Symmetric key object */ + WP11_GenericData genericData; /* Generic data object */ + WP11_Cert cert; /* Certificate object */ + #ifdef WOLFPKCS11_NSS + WP11_Trust trust; /* Trust object */ + #endif + } data; +#ifdef WOLFPKCS11_TPM + WOLFTPM2_KEYBLOB* tpmKey; +#endif +#ifdef WOLFSSL_STM32U5_DHUK + unsigned char* dhukIv; /* IV used with wrapping and unwrapping AES key. */ + int dhukIvLen; +#endif + CK_KEY_TYPE type; /* Key type of this object */ + word32 size; /* Size of the key in bits or bytes */ +#ifndef WOLFPKCS11_NO_STORE + unsigned char* keyData; /* Encoded key data */ + int keyDataLen; /* Length of encoded key data */ + byte iv[GCM_NONCE_MID_SZ]; /* IV/nonce for encrypt/decrypt */ + byte encoded:1; /* Key isn't in decoded form */ +#endif + + WP11_Session* session; /* Session object belongs to */ + WP11_Slot* slot; /* Slot object belongs to */ + + CK_OBJECT_HANDLE handle; /* Handle of this object */ + CK_OBJECT_CLASS objClass; /* Object class */ + CK_MECHANISM_TYPE keyGenMech; /* Key Gen mechanism created with */ + byte onToken:1; /* Object on token or session */ + byte local:1; /* Locally created object */ + word32 opFlag; /* Flags of operations allowed */ +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE + word64 statefulStateId; /* Non-zero nonce keying the durable */ + /* signature-state file (0 = none) */ + word64 statefulSigCount; /* Signatures produced so far (bound */ + /* into the state-file AAD) */ +#endif + + char startDate[8]; /* Start date of usage */ + char endDate[8]; /* End data of usage */ + + unsigned char* keyId; /* Key identifier */ + int keyIdLen; /* Length of key identifier */ + unsigned char* label; /* Object label */ + int labelLen; /* Length of object label */ + unsigned char* issuer; /* Certificate issuer */ + int issuerLen; /* Length of certificate issuer */ + unsigned char* serial; /* Certificate serial number */ + int serialLen; /* Length of certificate serial number */ + unsigned char* subject; /* Subject of the object */ + int subjectLen; /* Length of subject */ +#ifdef WOLFPKCS11_NSS + unsigned char* email; /* Certificate email (NSS extension) */ + int emailLen; /* Length of certificate email */ +#endif + + word32 category; /* Category of certificate */ + + WP11_Lock* lock; /* Object specific lock */ + + int devId; + WP11_Object* next; /* Next object in linked list */ +}; + +typedef struct WP11_Find { + int state; /* Whether operation is initialized */ + CK_OBJECT_HANDLE found[WP11_FIND_MAX]; + /* List of object handles found */ + int count; /* Count of object handles */ + int curr; /* Index of last object returned */ +} WP11_Find; + +#ifndef NO_RSA +#ifndef WC_NO_RSA_OAEP +typedef struct WP11_OaepParams { + enum wc_HashType hashType; /* Type of hash algorithm */ + int mgf; /* Mask Generation Function */ + byte* label; /* Label or AAD */ + int labelSz; /* Size of label in bytes */ +} WP11_OaepParams; +#endif + +#ifdef WC_RSA_PSS +typedef struct WP11_PssParams { + enum wc_HashType hashType; + int mgf; + int saltLen; +} WP11_PssParams; +#endif +#endif + +#ifdef WOLFPKCS11_MLDSA +typedef struct WP11_MldsaParams { + enum wc_HashType preHashType; + word32 hedgeType; + byte ctx[256]; + byte ctxSz; +} WP11_MldsaParams; +#endif + +#ifndef NO_AES +#ifdef HAVE_AES_CBC +typedef struct WP11_CbcParams { + unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ + Aes aes; /* AES object from wolfCrypt */ + unsigned char partial[AES_BLOCK_SIZE]; + /* Partial block when streaming */ + byte partialSz; /* Size of partial block data */ +} WP11_CbcParams; +#endif + +#ifdef HAVE_AESCTR +typedef struct WP11_CtrParams { + Aes aes; /* AES object from wolfCrypt */ +} WP11_CtrParams; +#endif + +#ifdef HAVE_AESGCM +typedef struct WP11_GcmParams { + unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; + /* IV/nonce data */ + int ivSz; /* IV/nonce size in bytes */ + unsigned char* aad; /* Additional Authentication Data */ + int aadSz; /* AAD size in bytes */ + int tagBits; /* Authentication tag size in bits */ + unsigned char authTag[WP11_MAX_GCM_TAG_SZ]; + /* Authentication tag calculated */ + unsigned char* enc; /* Cached data for multi-part: buffered */ + /* ciphertext (decrypt) or, without */ + /* streaming GCM, plaintext (encrypt) */ + int encSz; /* Size of cached data in bytes */ +#ifdef WOLFSSL_AESGCM_STREAM + Aes aes; /* Streaming context for multi-part */ + /* encrypt */ + int streamInit; /* Streaming context is initialized */ +#endif +} WP11_GcmParams; +#endif + +#ifdef HAVE_AESCCM +typedef struct WP11_CcmParams { + int dataSz; /* Data size in bytes */ + unsigned char iv[WP11_MAX_GCM_NONCE_SZ]; + /* IV/nonce data */ + int ivSz; /* IV/nonce size in bytes */ + unsigned char* aad; /* Additional Authentication Data */ + int aadSz; /* AAD size in bytes */ + int macSz; /* Size of MAC data in bytes */ +} WP11_CcmParams; +#endif + +#ifdef HAVE_AES_KEYWRAP +typedef struct WP11_KeyWrapParams { + Aes aes; /* AES object from wolfCrypt */ + unsigned char iv[8]; /* IV/nonce data */ + word32 ivSz; /* IV/nonce size in bytes */ +} WP11_KeyWrapParams; +#endif + +#ifdef HAVE_AESCTS +typedef struct WP11_CtsParams { + unsigned char iv[WP11_MAX_IV_SZ]; /* IV of CBC operation */ + Aes aes; /* AES object from wolfCrypt */ +} WP11_CtsParams; +#endif +#endif + +#ifndef NO_HMAC +typedef struct WP11_Hmac { + Hmac hmac; + word32 hmacSz; +} WP11_Hmac; +#endif + +typedef struct WP11_Digest { + wc_HashAlg hash; + enum wc_HashType hashType; +} WP11_Digest; + +#ifdef HAVE_AESCMAC +typedef struct WP11_Cmac { + Cmac cmac; + byte sigLen; +} WP11_Cmac; +#endif + +typedef struct WP11_TlsMacParams { + enum wc_MACAlgorithm mac; + word32 macSz; + CK_OBJECT_HANDLE key; + byte server:1; + byte isTlsPrf:1; +} WP11_TlsMacParams; + +struct WP11_Session { + unsigned char inUse; /* Indicates session has been opened */ + CK_SESSION_HANDLE handle; /* CryptoKi API session handle value */ + CK_MECHANISM_TYPE mechanism; /* Op that is being performed */ + CK_SLOT_ID slotId; /* Id of slot that session is on */ + WP11_Slot* slot; /* Slot that session is on */ + WP11_Object* object; /* Linked list of objects on session */ + int objCnt; /* Count of objects in session */ + WP11_Object* curr; /* Current object */ + WP11_Find find; /* Find data */ + int init; /* Which op is initialized */ + union { +#ifndef NO_RSA + #ifndef WC_NO_RSA_OAEP + WP11_OaepParams oaep; /* RSA-OAEP parameters */ + #endif + #ifdef WC_RSA_PSS + WP11_PssParams pss; /* RSA-PSS parameters */ + #endif +#endif +#ifdef WOLFPKCS11_MLDSA + WP11_MldsaParams mldsa; /* ML-DSA parameters */ +#endif +#ifndef NO_AES + #ifdef HAVE_AES_CBC + WP11_CbcParams cbc; /* AES-CBC parameters */ + #endif + #ifdef HAVE_AESCTR + WP11_CtrParams ctr; /* AES-CTR parameters */ + #endif + #ifdef HAVE_AESGCM + WP11_GcmParams gcm; /* AES-GCM parameters */ + #endif + #ifdef HAVE_AESCCM + WP11_CcmParams ccm; /* AES-CCM parameters */ + #endif + #ifdef HAVE_AESCTS + WP11_CtsParams cts; /* AES-CTS parameters */ + #endif + #ifdef HAVE_AES_KEYWRAP + WP11_KeyWrapParams kw; /* AES-KW parameters */ + #endif +#endif +#ifndef NO_HMAC + WP11_Hmac hmac; /* HMAC parameters */ +#endif + WP11_Digest digest; /* Digest parameters */ +#ifdef HAVE_AESCMAC + WP11_Cmac cmac; /* CMAC parameters */ +#endif + WP11_TlsMacParams tlsMac; /* TLS MAC parameters */ + } params; + + byte* data; /* Held data for one-shot mechs */ + word32 dataSz; /* Size of held data */ + + int devId; + WP11_Session* next; /* Next session for slot */ +}; + +typedef struct WP11_Token { + char label[LABEL_SZ]; /* Token label */ + int state; /* Token initialize state */ + byte soPin[PIN_HASH_SZ]; /* SO's PIN hashed with seed */ + int soPinLen; /* Used to indicate PIN set */ + byte soPinSeed[PIN_SEED_SZ]; /* Seed for calculating SO's PIN */ + int soFailedLogin; /* Count of consecutive failed logins */ + time_t soLastFailedLogin; /* Time of last login if it failed */ + time_t soFailLoginTimeout; /* Timeout after max login fails */ + byte userPin[PIN_HASH_SZ]; /* User's PIN hashed with seed */ + int userPinLen; /* Used to indicate PIN set */ + byte userPinSeed[PIN_SEED_SZ]; /* Seed for calculating user's PIN */ + int userFailedLogin; /* Count of consecutive failed logins */ + time_t userLastFailedLogin; /* Time of last login if it failed */ + time_t userFailLoginTimeout; /* Timeout after max login fails */ +#ifndef WOLFPKCS11_NO_STORE + byte seed[PIN_SEED_SZ]; /* Seed used to calculate key */ + byte key[AES_256_KEY_SIZE]; /* Key to en/decrypt private data */ +#endif + WC_RNG rng; /* Random number generator */ + WP11_Lock rngLock; /* Lock for random access */ + WP11_Lock lock; /* Lock for object access */ + int loginState; /* Login state of the token */ + WP11_Object* object; /* Linked list of token objects */ + int objCnt; /* Count of objects on token */ + int tokenFlags; /* Flags for token */ + int nextObjId; + byte userPinEmpty:2; /* Indicates user PIN is empty + * 0 = not set + * 1 = empty + * 2 = not empty */ +} WP11_Token; + +struct WP11_Slot { + CK_SLOT_ID id; /* CryptoKi API slot id value */ + WP11_Token token; /* Token information for slot */ + WP11_Session* session; /* Linked list of sessions */ + WP11_Lock lock; /* Lock for access to slot info */ + + int devId; +#ifdef WOLFPKCS11_TPM + WOLFTPM2_DEV tpmDev; + WOLFTPM2_KEY tpmSrk; + WOLFTPM2_SESSION tpmSession; + TpmCryptoDevCtx tpmCtx; +#endif +}; + +/* Helpers implemented in internal.c and used by its sibling source files. + * Not part of the public or the wolfpkcs11/internal.h interface. */ + +#ifndef SINGLE_THREADED +WP11_LOCAL int WP11_Lock_LockRW(WP11_Lock* lock); +WP11_LOCAL int WP11_Lock_UnlockRW(WP11_Lock* lock); +WP11_LOCAL int WP11_Lock_LockRO(WP11_Lock* lock); +WP11_LOCAL int WP11_Lock_UnlockRO(WP11_Lock* lock); +#endif + +WP11_LOCAL int Rng_New(WC_RNG* baseRng, WP11_Lock* lock, WC_RNG* rng); +WP11_LOCAL void Rng_Free(WC_RNG* rng); + +#ifndef WOLFPKCS11_NO_STORE +WP11_LOCAL int wp11_storage_open_readonly(int type, CK_ULONG id1, CK_ULONG id2, + void** storage); +WP11_LOCAL int wp11_storage_open(int type, CK_ULONG id1, CK_ULONG id2, + int variableSz, void** storage); +WP11_LOCAL void wp11_storage_close(void* storage); +WP11_LOCAL int wp11_storage_read(void* storage, unsigned char* buffer, int len); +WP11_LOCAL int wp11_storage_write(void* storage, unsigned char* buffer, + int len); +WP11_LOCAL int wp11_storage_read_alloc_array(void* storage, + unsigned char** buffer, int* len); +WP11_LOCAL int wp11_storage_write_array(void* storage, unsigned char* buffer, + word32 len); + +#ifndef WOLFPKCS11_CUSTOM_STORE +/* Report the result of the close-and-commit rather than discarding it. */ +WP11_LOCAL int wp11_storage_close_report(void* store); +#ifdef WOLFPKCS11_STATEFUL_SIG_PRIVATE +#ifndef WOLFPKCS11_TPM_STORE +/* Ask the file store to fsync the data and its parent directory before the + * atomic rename commits. */ +WP11_LOCAL void wp11_storage_set_durable(void* store, int durable); +#endif + +/* Per-key state files for stateful hash-based signature schemes. Keyed by a + * 64-bit nonce rather than the CK_ULONG ids the wolfPKCS11_Store_* API takes, + * which would truncate to 32 bits on LLP64 platforms. */ +WP11_LOCAL int wp11_Stateful_StateFile_Open(const char* schemeFilePrefix, + int tokenId, word64 stateId, + int read, void** store); +WP11_LOCAL int wp11_Stateful_StateFile_Remove(const char* schemeFilePrefix, + int tokenId, word64 stateId); +#endif +#endif /* !WOLFPKCS11_CUSTOM_STORE */ +#endif /* !WOLFPKCS11_NO_STORE */ + +#endif /* WOLFPKCS11_WP11_TYPES_H */ diff --git a/tests/hbs_private_test.c b/tests/hbs_private_test.c new file mode 100644 index 00000000..df3de600 --- /dev/null +++ b/tests/hbs_private_test.c @@ -0,0 +1,1866 @@ +/* hbs_private_test.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfPKCS11 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + * + * Functional test for the sign-capable LMS/HSS build + * (--enable-lms-private / WOLFPKCS11_LMS_PRIVATE). Exercises the paths that + * the verify-only tests cannot reach: + * + * - keygen -> sign (two-call length query) -> verify + * - durable state survives a C_Finalize/C_Initialize reload with no + * one-time-signature (OTS) index rollback + * - keygen template validation (CKA_TOKEN, missing/oversized attrs, bad + * mechanism parameter, invalid LMS/LMOTS type) + * - CKA_HSS_KEYS_REMAINING on a private key (size query, value, decrement, + * small-buffer error) + * - rejection of a private-key import that carries CKA_VALUE (forged OTS + * state injection) + * - key exhaustion poisons the key (subsequent C_Sign -> CKR_DEVICE_ERROR) + * - C_DestroyObject removes the encrypted state file + * - a tampered on-disk state file is detected on reload (no silent signing) + * + * The scheme used throughout is HSS L1 / LMS SHA256_M32_H5 / LMOTS + * SHA256_N32_W4, i.e. a single 32-leaf tree, so exhaustion is reachable in 32 + * signatures. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include + +#ifndef WOLFPKCS11_USER_SETTINGS + #include +#endif +#include + +#ifndef HAVE_PKCS11_STATIC +#include +#endif + +#include +#include + +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) + +#ifndef _WIN32 +#include +#include +#include +#include +#endif + +#ifndef WOLFPKCS11_DLL_FILENAME + #ifdef __MACH__ + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.dylib" + #else + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.so" + #endif +#endif + +#define CHECK_CKR(rv, msg) \ + do { \ + if ((rv) != CKR_OK) { \ + fprintf(stderr, "\n%s:%d - %s: %lx - FAIL\n", \ + __FILE__, __LINE__, msg, (unsigned long)(rv)); \ + } \ + } \ + while (0) + +/* Assert that an operation failed with exactly the expected return code. */ +#define CHECK_CKR_FAIL(rv, expected, msg) \ + do { \ + if ((rv) != (expected)) { \ + fprintf(stderr, "\n%s:%d - %s: got %lx, expected %lx - FAIL\n",\ + __FILE__, __LINE__, msg, (unsigned long)(rv), \ + (unsigned long)(expected)); \ + } \ + } \ + while (0) + +#ifndef HAVE_PKCS11_STATIC +static void* dlib; +#endif +static CK_FUNCTION_LIST* funcList; +static CK_SLOT_ID slot = 0; +static const char* tokenName = "wolfpkcs11"; +static byte* soPin = (byte*)"password123456"; +static int soPinLen = 14; +static byte* userPin = (byte*)"wolfpkcs11-test"; +static int userPinLen = 15; +static const char* storeDir = "./store/hbs_priv"; + +static CK_BBOOL ckTrue = CK_TRUE; +static CK_BBOOL ckFalse = CK_FALSE; + +/* Scheme under test: HSS L1 / H5 / W4 - a single 32-leaf tree. */ +static CK_ULONG hssLevels = 1; +static CK_ULONG hssLmsType = CKL_LMS_SHA256_M32_H5; +static CK_ULONG hssLmotsType = CKL_LMOTS_SHA256_N32_W4; +#define HSS_H5_SIGS 32 + +static const CK_BYTE testMsg[] = "wolfPKCS11 HSS private-key sign test vector"; +#define TEST_MSG_LEN ((CK_ULONG)(sizeof(testMsg) - 1)) + +static CK_RV pkcs11_init(void) +{ + CK_RV ret; + CK_C_INITIALIZE_ARGS args; + CK_SLOT_ID slotList[16]; + CK_ULONG slotCount = sizeof(slotList) / sizeof(slotList[0]); +#ifndef HAVE_PKCS11_STATIC + CK_C_GetFunctionList func; + + dlib = dlopen(WOLFPKCS11_DLL_FILENAME, RTLD_NOW | RTLD_LOCAL); + if (dlib == NULL) { + fprintf(stderr, "dlopen error: %s\n", dlerror()); + return CKR_GENERAL_ERROR; + } + func = (CK_C_GetFunctionList)dlsym(dlib, "C_GetFunctionList"); + if (func == NULL) { + fprintf(stderr, "Failed to get function list function\n"); + dlclose(dlib); + return CKR_GENERAL_ERROR; + } + ret = func(&funcList); + if (ret != CKR_OK) { + fprintf(stderr, "Failed to get function list: %lx\n", + (unsigned long)ret); + dlclose(dlib); + return ret; + } +#else + ret = C_GetFunctionList(&funcList); + if (ret != CKR_OK) { + fprintf(stderr, "Failed to get function list: %lx\n", + (unsigned long)ret); + return ret; + } +#endif + + XMEMSET(&args, 0, sizeof(args)); + args.flags = CKF_OS_LOCKING_OK; + ret = funcList->C_Initialize(&args); + CHECK_CKR(ret, "Initialize"); + + if (ret == CKR_OK) { + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + CHECK_CKR(ret, "Get Slot List"); + } + if (ret == CKR_OK) { + if (slotCount > 0) + slot = slotList[0]; + else { + fprintf(stderr, "No slots available\n"); + ret = CKR_GENERAL_ERROR; + } + } + return ret; +} + +static void pkcs11_final(void) +{ + funcList->C_Finalize(NULL); +#ifndef HAVE_PKCS11_STATIC + if (dlib != NULL) { + dlclose(dlib); + dlib = NULL; + } +#endif +} + +static CK_RV pkcs11_init_token(void) +{ + CK_RV ret; + unsigned char label[32]; + + XMEMSET(label, ' ', sizeof(label)); + XMEMCPY(label, tokenName, strlen(tokenName)); + ret = funcList->C_InitToken(slot, soPin, soPinLen, label); + CHECK_CKR(ret, "Init Token"); + return ret; +} + +static CK_RV pkcs11_set_user_pin(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, &session); + CHECK_CKR(ret, "Open Session for PIN setup"); + if (ret == CKR_OK) { + ret = funcList->C_Login(session, CKU_SO, soPin, soPinLen); + CHECK_CKR(ret, "Login as SO"); + if (ret == CKR_OK) { + ret = funcList->C_InitPIN(session, userPin, userPinLen); + CHECK_CKR(ret, "Init User PIN"); + } + funcList->C_Logout(session); + funcList->C_CloseSession(session); + } + return ret; +} + +static CK_RV pkcs11_open_session(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); + CHECK_CKR(ret, "Open Session"); + if (ret == CKR_OK) { + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + CHECK_CKR(ret, "Login"); + } + return ret; +} + +static void close_session(CK_SESSION_HANDLE session) +{ + if (session != CK_INVALID_HANDLE) { + funcList->C_Logout(session); + funcList->C_CloseSession(session); + } +} + +/* Remove every wp11_* store artifact so each test starts from a clean token. */ +static void cleanup_store(void) +{ +#ifndef _WIN32 + DIR* d; + struct dirent* e; + char path[512]; + + d = opendir(storeDir); + if (d == NULL) + return; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, "wp11_", 5) == 0) { + snprintf(path, sizeof(path), "%s/%s", storeDir, e->d_name); + (void)remove(path); + } + } + closedir(d); +#endif +} + +/* Fresh library + token + user PIN + logged-in R/W session. */ +static CK_RV fresh_token(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + + cleanup_store(); + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = pkcs11_init_token(); + if (ret == CKR_OK) + ret = pkcs11_set_user_pin(); + if (ret == CKR_OK) + ret = pkcs11_open_session(session); + return ret; +} + +/* Re-open the existing on-disk token (reload path) without wiping the store. */ +static CK_RV reopen_token(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = pkcs11_open_session(session); + return ret; +} + +/* Like reopen_token but does not log a login failure as a FAIL: a tampered + * token is expected to fail the login-time reload, so the caller inspects the + * return code instead of treating it as an error. */ +static CK_RV reopen_token_quiet(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); + if (ret == CKR_OK) + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + return ret; +} + +/* Generate a token-resident HSS keypair with the given scheme parameters. */ +static CK_RV gen_hss_key(CK_SESSION_HANDLE session, CK_ULONG levels, + CK_ULONG lmsType, CK_ULONG lmotsType, CK_BYTE* id, CK_ULONG idLen, + CK_OBJECT_HANDLE* pub, CK_OBJECT_HANDLE* priv) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_ATTRIBUTE pubTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, id, idLen }, + { CKA_HSS_LEVELS, &levels, sizeof(levels) }, + { CKA_HSS_LMS_TYPE, &lmsType, sizeof(lmsType) }, + { CKA_HSS_LMOTS_TYPE, &lmotsType, sizeof(lmotsType) }, + }; + CK_ATTRIBUTE privTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, id, idLen }, + }; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl) / sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl) / sizeof(*privTmpl), pub, priv); + return ret; +} + +/* Find a single token object of the given class by CKA_ID, without reporting + * a miss. Tests that expect the object to be gone use this so a passing run + * never writes "FAIL" into test-suite.log. */ +static CK_RV find_by_id_quiet(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, + CK_BYTE* id, CK_ULONG idLen, CK_OBJECT_HANDLE* obj) +{ + CK_RV ret; + CK_ULONG count = 0; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &cls, sizeof(cls) }, + { CKA_ID, id, idLen }, + }; + + ret = funcList->C_FindObjectsInit(session, tmpl, + sizeof(tmpl) / sizeof(*tmpl)); + if (ret == CKR_OK) { + ret = funcList->C_FindObjects(session, obj, 1, &count); + funcList->C_FindObjectsFinal(session); + } + if (ret == CKR_OK && count != 1) + ret = CKR_GENERAL_ERROR; + return ret; +} + +/* Find a single token object of the given class by CKA_ID. Reports a miss, + * so only use it where the object is expected to be present. */ +static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, + CK_BYTE* id, CK_ULONG idLen, CK_OBJECT_HANDLE* obj) +{ + CK_RV ret = find_by_id_quiet(session, cls, id, idLen, obj); + + if (ret != CKR_OK) + fprintf(stderr, "\nExpected 1 object with the given CKA_ID - FAIL\n"); + return ret; +} + +/* Sign testMsg with the two-call length-query protocol; caller frees *sigOut. */ +static CK_RV hss_sign(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE priv, + CK_BYTE** sigOut, CK_ULONG* sigLenOut) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_ULONG sigLen = 0; + CK_BYTE* sig = NULL; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_SignInit(session, &mech, priv); + if (ret == CKR_OK) { + /* First call: NULL buffer -> length query, operation stays active. */ + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, NULL, + &sigLen); + } + if (ret == CKR_OK && sigLen == 0) + ret = CKR_GENERAL_ERROR; + if (ret == CKR_OK) { + sig = (CK_BYTE*)XMALLOC(sigLen, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (sig == NULL) + ret = CKR_HOST_MEMORY; + } + if (ret == CKR_OK) + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (ret == CKR_OK) { + *sigOut = sig; + *sigLenOut = sigLen; + } + else if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + return ret; +} + +static CK_RV hss_verify(CK_SESSION_HANDLE session, CK_OBJECT_HANDLE pub, + CK_BYTE* sig, CK_ULONG sigLen) +{ + CK_RV ret; + CK_MECHANISM mech; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = funcList->C_VerifyInit(session, &mech, pub); + if (ret == CKR_OK) + ret = funcList->C_Verify(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + sigLen); + return ret; +} + +static CK_RV get_keys_remaining(CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE priv, CK_ULONG* remaining) +{ + CK_ATTRIBUTE a; + + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = remaining; + a.ulValueLen = sizeof(*remaining); + return funcList->C_GetAttributeValue(session, priv, &a, 1); +} + +/* ---- HIGH-1: keygen -> sign (two-call) -> verify ---- */ +static CK_RV test_keygen_sign_verify(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-ksv"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + + printf(" keygen -> sign -> verify...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair(CKM_HSS_KEY_PAIR_GEN)"); + } + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "C_Sign(CKM_HSS)"); + } + if (ret == CKR_OK) { + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "C_Verify(CKM_HSS) of generated signature"); + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- HIGH-2: durable state survives reload with no OTS index rollback ---- */ +static CK_RV test_reload_no_rollback(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-reload"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + CK_ULONG remainingBefore = 0, remainingAfterReload = 0, remainingFinal = 0; + int i; + + printf(" sign N, reload, confirm no rollback...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + /* Sign three times before the reload. */ + for (i = 0; ret == CKR_OK && i < 3; i++) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "pre-reload C_Sign"); + if (ret == CKR_OK) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingBefore); + CHECK_CKR(ret, "keys-remaining before reload"); + } + /* Force a reload from disk. */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) { + printf(" --- reloading token from disk ---\n"); + ret = reopen_token(&session); + } + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingAfterReload); + CHECK_CKR(ret, "keys-remaining after reload"); + } + /* The counter must not have rolled back across the reload. */ + if (ret == CKR_OK && remainingAfterReload != remainingBefore) { + fprintf(stderr, "\nOTS index rolled back on reload: before=%lu " + "after=%lu - FAIL\n", (unsigned long)remainingBefore, + (unsigned long)remainingAfterReload); + ret = CKR_GENERAL_ERROR; + } + /* A post-reload signature must continue decrementing and still verify. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "post-reload C_Sign"); + } + if (ret == CKR_OK) { + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "post-reload C_Verify"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingFinal); + CHECK_CKR(ret, "keys-remaining after post-reload sign"); + } + if (ret == CKR_OK && remainingFinal != remainingAfterReload - 1) { + fprintf(stderr, "\nCounter did not advance after reload: %lu -> %lu " + "- FAIL\n", (unsigned long)remainingAfterReload, + (unsigned long)remainingFinal); + ret = CKR_GENERAL_ERROR; + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- HIGH-3: keygen template validation ---- */ +static CK_RV gen_expect(CK_SESSION_HANDLE session, CK_ATTRIBUTE* pubTmpl, + CK_ULONG pubCount, CK_ATTRIBUTE* privTmpl, CK_ULONG privCount, + CK_VOID_PTR param, CK_ULONG paramLen, CK_RV expected, const char* msg) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = param; + mech.ulParameterLen = paramLen; + + ret = funcList->C_GenerateKeyPair(session, &mech, pubTmpl, pubCount, + privTmpl, privCount, &pub, &priv); + CHECK_CKR_FAIL(ret, expected, msg); + if (ret == CKR_OK) { + /* Should not have succeeded; clean up so the test stays isolated. */ + funcList->C_DestroyObject(session, pub); + funcList->C_DestroyObject(session, priv); + return CKR_GENERAL_ERROR; + } + return (ret == expected) ? CKR_OK : ret; +} + +static CK_RV test_keygen_negative_templates(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_ULONG dummyParam = 0; + CK_ULONG badType = 0x00000099UL; + CK_ULONG shortLevels = 1; + CK_BYTE id[] = "hss-neg"; + + /* Fully valid templates, mutated per-case below. */ + CK_ATTRIBUTE pubOk[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + CK_ATTRIBUTE privOk[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, id, sizeof(id) - 1 }, + }; + /* CKA_TOKEN=FALSE on the private arm. */ + CK_ATTRIBUTE privTokFalse[] = { + { CKA_TOKEN, &ckFalse, sizeof(ckFalse) }, + { CKA_ID, id, sizeof(id) - 1 }, + }; + /* Missing CKA_HSS_LMS_TYPE / LMOTS_TYPE. */ + CK_ATTRIBUTE pubIncomplete[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + }; + /* Wrong-sized CKA_HSS_LEVELS. */ + CK_ATTRIBUTE pubBadSize[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &shortLevels, 1 }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + /* Invalid LMS type value. */ + CK_ATTRIBUTE pubBadType[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &badType, sizeof(badType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + + printf(" keygen template validation...\n"); + ret = fresh_token(&session); + + /* Non-NULL mechanism parameter -> CKR_MECHANISM_PARAM_INVALID. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubOk, 4, privOk, 2, &dummyParam, + sizeof(dummyParam), CKR_MECHANISM_PARAM_INVALID, + "non-NULL mechanism parameter"); + } + /* CKA_TOKEN=FALSE -> CKR_TEMPLATE_INCONSISTENT. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubOk, 4, privTokFalse, 2, NULL, 0, + CKR_TEMPLATE_INCONSISTENT, "CKA_TOKEN=FALSE on private key"); + } + /* Missing scheme attributes -> CKR_TEMPLATE_INCOMPLETE. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubIncomplete, 2, privOk, 2, NULL, 0, + CKR_TEMPLATE_INCOMPLETE, "missing CKA_HSS_LMS/LMOTS_TYPE"); + } + /* Wrong-sized attribute -> CKR_ATTRIBUTE_VALUE_INVALID. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubBadSize, 4, privOk, 2, NULL, 0, + CKR_ATTRIBUTE_VALUE_INVALID, "wrong-sized CKA_HSS_LEVELS"); + } + /* Invalid LMS type -> CKR_MECHANISM_PARAM_INVALID. */ + if (ret == CKR_OK) { + ret = gen_expect(session, pubBadType, 4, privOk, 2, NULL, 0, + CKR_MECHANISM_PARAM_INVALID, "invalid CKA_HSS_LMS_TYPE"); + } + + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-8: CKA_HSS_KEYS_REMAINING on a private key ---- */ +static CK_RV test_keys_remaining_private(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-remain"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + CK_ULONG remaining = 0, remainingAfter = 0; + CK_ATTRIBUTE a; + CK_BYTE small = 0; + + printf(" CKA_HSS_KEYS_REMAINING on private key...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + /* Size query: NULL buffer must report sizeof(CK_ULONG). */ + if (ret == CKR_OK) { + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = NULL; + a.ulValueLen = 0; + ret = funcList->C_GetAttributeValue(session, priv, &a, 1); + CHECK_CKR(ret, "keys-remaining size query"); + if (ret == CKR_OK && a.ulValueLen != sizeof(CK_ULONG)) { + fprintf(stderr, "\nkeys-remaining size query returned %lu - FAIL\n", + (unsigned long)a.ulValueLen); + ret = CKR_GENERAL_ERROR; + } + } + /* Value must equal the full 2^(levels*height) tree size before signing. */ + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remaining); + CHECK_CKR(ret, "keys-remaining value"); + } + if (ret == CKR_OK && remaining != HSS_H5_SIGS) { + fprintf(stderr, "\nInitial keys-remaining %lu, expected %d - FAIL\n", + (unsigned long)remaining, HSS_H5_SIGS); + ret = CKR_GENERAL_ERROR; + } + /* Too-small buffer must be reported as CKR_BUFFER_TOO_SMALL. */ + if (ret == CKR_OK) { + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = &small; + a.ulValueLen = sizeof(small); + ret = funcList->C_GetAttributeValue(session, priv, &a, 1); + CHECK_CKR_FAIL(ret, CKR_BUFFER_TOO_SMALL, "keys-remaining small buffer"); + ret = (ret == CKR_BUFFER_TOO_SMALL) ? CKR_OK : CKR_GENERAL_ERROR; + } + /* The count decrements by one after a signature. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "C_Sign"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remainingAfter); + CHECK_CKR(ret, "keys-remaining after sign"); + } + if (ret == CKR_OK && remainingAfter != remaining - 1) { + fprintf(stderr, "\nkeys-remaining did not decrement: %lu -> %lu " + "- FAIL\n", (unsigned long)remaining, (unsigned long)remainingAfter); + ret = CKR_GENERAL_ERROR; + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-9: private-key import carrying CKA_VALUE is rejected ---- */ +static CK_RV test_import_value_rejected(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE obj = CK_INVALID_HANDLE; + CK_OBJECT_CLASS privClass = CKO_PRIVATE_KEY; + CK_KEY_TYPE keyType = CKK_HSS; + CK_BYTE fakeValue[64]; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &privClass, sizeof(privClass) }, + { CKA_KEY_TYPE, &keyType, sizeof(keyType) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + { CKA_VALUE, fakeValue, sizeof(fakeValue) }, + }; + + printf(" private-key import with CKA_VALUE is rejected...\n"); + XMEMSET(fakeValue, 0xAB, sizeof(fakeValue)); + ret = fresh_token(&session); + if (ret == CKR_OK) { + CK_RV cret = funcList->C_CreateObject(session, tmpl, + sizeof(tmpl) / sizeof(*tmpl), &obj); + if (cret == CKR_OK) { + fprintf(stderr, "\nValue-carrying HSS private-key import accepted " + "- FAIL\n"); + funcList->C_DestroyObject(session, obj); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" rejected with %lx (expected)\n", (unsigned long)cret); + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-7: exhaustion poisons the key -> CKR_DEVICE_ERROR ---- */ +static CK_RV test_exhaustion_poison(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-exhaust"; + CK_MECHANISM mech; + CK_BYTE sig[4096]; + CK_ULONG sigLen; + CK_ULONG initialRemaining = 0; + int successes = 0; + int i; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + printf(" key exhaustion poisons the key...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &initialRemaining); + CHECK_CKR(ret, "initial keys-remaining"); + } + /* Sign until the tree is exhausted. Each failed C_Sign must have cleared + * the operation (MEDIUM-4), so the next C_SignInit succeeds. */ + for (i = 0; ret == CKR_OK && i < HSS_H5_SIGS + 4; i++) { + CK_RV sret = funcList->C_SignInit(session, &mech, priv); + if (sret != CKR_OK) { + fprintf(stderr, "\nC_SignInit failed at iter %d: %lx - FAIL\n", + i, (unsigned long)sret); + ret = CKR_GENERAL_ERROR; + break; + } + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (sret == CKR_OK) { + successes++; + } + else { + /* First failure: the tree is spent. The very next attempt must be + * reported as a poisoned key (CKR_DEVICE_ERROR). */ + printf(" exhausted after %d signatures (first failure %lx)\n", + successes, (unsigned long)sret); + sret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR(sret, "C_SignInit after exhaustion"); + if (sret == CKR_OK) { + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, + TEST_MSG_LEN, sig, &sigLen); + CHECK_CKR_FAIL(sret, CKR_DEVICE_ERROR, + "poisoned-key C_Sign"); + if (sret != CKR_DEVICE_ERROR) + ret = CKR_GENERAL_ERROR; + } + else { + ret = CKR_GENERAL_ERROR; + } + break; + } + } + if (ret == CKR_OK && successes != (int)initialRemaining) { + fprintf(stderr, "\nSigned %d times, keys-remaining said %lu - FAIL\n", + successes, (unsigned long)initialRemaining); + ret = CKR_GENERAL_ERROR; + } + close_session(session); + pkcs11_final(); + return ret; +} + +#ifndef _WIN32 +/* Locate the single encrypted HSS state file in the store directory. */ +static CK_RV find_state_file(char* path, size_t pathLen) +{ + DIR* d; + struct dirent* e; + CK_RV ret = CKR_OBJECT_HANDLE_INVALID; + const char* prefix = "wp11_hsskey_priv_state_"; + + d = opendir(storeDir); + if (d == NULL) + return CKR_GENERAL_ERROR; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, prefix, strlen(prefix)) == 0) { + snprintf(path, pathLen, "%s/%s", storeDir, e->d_name); + ret = CKR_OK; + break; + } + } + closedir(d); + return ret; +} + +/* ---- MEDIUM-10: C_DestroyObject removes the encrypted state file ---- */ +static CK_RV test_destroy_removes_state(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-destroy"; + char statePath[512]; + FILE* f; + + printf(" C_DestroyObject removes the state file...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + if (ret == CKR_OK) { + ret = find_state_file(statePath, sizeof(statePath)); + CHECK_CKR(ret, "locate state file after keygen"); + } + if (ret == CKR_OK) { + ret = funcList->C_DestroyObject(session, priv); + CHECK_CKR(ret, "C_DestroyObject(private key)"); + } + if (ret == CKR_OK) { + f = fopen(statePath, "rb"); + if (f != NULL) { + fclose(f); + fprintf(stderr, "\nState file still present after destroy: %s " + "- FAIL\n", statePath); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" state file removed\n"); + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- MEDIUM-11: a tampered on-disk state file is detected on reload ---- */ +static CK_RV test_state_tamper_detected(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-tamper"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + char statePath[512]; + FILE* f; + long fsize; + int flipByte; + + printf(" tampered state file is detected on reload...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair"); + } + /* Sign once so a real advanced state is on disk before we tamper it. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "C_Sign before tamper"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = find_state_file(statePath, sizeof(statePath)); + /* Force everything to disk, then flip a ciphertext byte near the end. */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) { + f = fopen(statePath, "rb+"); + if (f == NULL) + ret = CKR_GENERAL_ERROR; + else { + CK_BYTE b = 0; + fseek(f, 0, SEEK_END); + fsize = ftell(f); + flipByte = (int)(fsize > 4 ? fsize - 4 : 0); + fseek(f, flipByte, SEEK_SET); + if (fread(&b, 1, 1, f) == 1) { + b ^= 0xFF; + fseek(f, flipByte, SEEK_SET); + fwrite(&b, 1, 1, f); + } + else { + ret = CKR_GENERAL_ERROR; + } + fclose(f); + } + } + /* Reload and attempt to use the key. The AES-GCM authentication failure + * must surface as an error and never produce a signature from + * unauthenticated state. Detection may happen at C_Login (the reload runs + * during login-time decode) or, if login proceeds, at C_Sign. Either is a + * safe outcome; only a produced signature is a failure. */ + if (ret == CKR_OK) { + CK_RV rret; + + printf(" --- reloading tampered token ---\n"); + rret = reopen_token_quiet(&session); + if (rret != CKR_OK) { + printf(" tamper detected at login: %lx\n", + (unsigned long)rret); + } + else if (find_by_id_quiet(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv) != CKR_OK) { + printf(" tampered key not usable after login\n"); + } + else { + CK_RV sret = hss_sign(session, priv, &sig, &sigLen); + if (sret == CKR_OK) { + fprintf(stderr, "\nSigned with tampered state - FAIL\n"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" signing correctly refused: %lx\n", + (unsigned long)sret); + } + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- Phase 3: state-write failure poisons the key; reload never advances + * past the last durable index. ---- */ +static CK_RV test_write_failure_poison(void) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-wfail"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, before = 0, after = 0; + struct stat dirStat; + int dirModeSaved = 0; + int skipInjection = 0; + + printf(" state-write failure poisons the key...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + /* One good signature establishes a durable baseline index on disk. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "baseline sign"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &before); + /* Make the store directory unwritable so the durable state write fails. + * Root bypasses DAC permission checks, so the write would still succeed + * and the assertion below would fail for a reason that says nothing about + * the code under test. */ + if (ret == CKR_OK && geteuid() == 0) { + printf(" skipping write-failure injection (running as root)\n"); + skipInjection = 1; + } + if (ret == CKR_OK && !skipInjection) { + /* Capture the real mode so it can be put back exactly. */ + if (stat(storeDir, &dirStat) != 0 || chmod(storeDir, 0500) != 0) + ret = CKR_GENERAL_ERROR; + else + dirModeSaved = 1; + } + if (ret == CKR_OK && !skipInjection) { + sret = hss_sign(session, priv, &sig, &sigLen); + if (sret == CKR_OK) { + fprintf(stderr, "\nsigned despite an unwritable store - FAIL\n"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + ret = CKR_GENERAL_ERROR; + } + else { + printf(" write-failed sign refused: %lx\n", (unsigned long)sret); + } + } + if (dirModeSaved) + (void)chmod(storeDir, dirStat.st_mode); /* restore, even on failure */ + if (skipInjection) + return ret; + /* The key is poisoned until reloaded: the next sign is CKR_DEVICE_ERROR. */ + if (ret == CKR_OK) { + sret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR_FAIL(sret, CKR_DEVICE_ERROR, "poisoned-key sign"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + if (sret != CKR_DEVICE_ERROR) + ret = CKR_GENERAL_ERROR; + } + /* Reload must recover to the last durable index - the failed write must not + * have advanced it. */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &after); + if (ret == CKR_OK && after != before) { + fprintf(stderr, "\nreload advanced past a failed write: %lu -> %lu " + "- FAIL\n", (unsigned long)before, (unsigned long)after); + ret = CKR_GENERAL_ERROR; + } + /* And it still signs correctly from the recovered state. */ + if (ret == CKR_OK) + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "post-recovery sign/verify"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- Phase 3: per-sign durability survives an unclean exit. A forked child + * signs and _exit()s with NO C_Finalize; the parent confirms the state advanced + * durably and the consumed index is never reused. ---- */ +static CK_RV test_crash_durability(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-crash"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, before = 0, after = 0; + pid_t pid; + int status = 0; + + printf(" per-sign durability survives an unclean exit (fork)...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &before); + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret != CKR_OK) + return ret; + + pid = fork(); + if (pid == 0) { + /* Child: fresh process. Reload, sign once (durable), then hard-exit + * with no C_Finalize, simulating a crash right after a signature. */ + CK_SESSION_HANDLE cs = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE cp = CK_INVALID_HANDLE; + CK_BYTE* csig = NULL; + CK_ULONG cslen = 0; + CK_RV cret; + + cret = reopen_token(&cs); + if (cret == CKR_OK) + cret = find_by_id(cs, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &cp); + if (cret == CKR_OK) + cret = hss_sign(cs, cp, &csig, &cslen); + _exit(cret == CKR_OK ? 0 : 1); + } + if (pid < 0) + return CKR_GENERAL_ERROR; + if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || + WEXITSTATUS(status) != 0) { + fprintf(stderr, "\nchild sign failed (status %d) - FAIL\n", status); + return CKR_GENERAL_ERROR; + } + + /* Parent: the child's signature must have durably advanced the state even + * though it never finalized, and the recovered index is exactly one past + * the parent's - never reused. */ + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &after); + if (ret == CKR_OK && after != before - 1) { + fprintf(stderr, "\ncrash durability: remaining %lu -> %lu, expected a " + "drop of 1 - FAIL\n", (unsigned long)before, (unsigned long)after); + ret = CKR_GENERAL_ERROR; + } + if (ret == CKR_OK) + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "post-crash sign/verify"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* ---- Phase 3: on-disk state-file corruption matrix. ---- */ +#define CORRUPT_TRUNCATE 0 +#define CORRUPT_MAGIC 1 +#define CORRUPT_PARAM 2 +#define CORRUPT_TAG 3 + +static CK_RV corrupt_state_and_check(const char* label, int type) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-corrupt"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + char statePath[512]; + FILE* f; + long fsize = 0, off; + CK_BYTE b = 0; + + printf(" [%s]...\n", label); + ret = fresh_token(&session); + if (ret == CKR_OK) + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) + ret = find_state_file(statePath, sizeof(statePath)); + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret != CKR_OK) + return ret; + + if (type == CORRUPT_TRUNCATE) { + f = fopen(statePath, "rb"); + if (f == NULL) + return CKR_GENERAL_ERROR; + fseek(f, 0, SEEK_END); + fsize = ftell(f); + fclose(f); + if (truncate(statePath, fsize > 8 ? fsize - 8 : 0) != 0) + return CKR_GENERAL_ERROR; + } + else { + f = fopen(statePath, "rb+"); + if (f == NULL) + return CKR_GENERAL_ERROR; + fseek(f, 0, SEEK_END); + fsize = ftell(f); + if (type == CORRUPT_MAGIC) + off = 0; /* header magic */ + else if (type == CORRUPT_PARAM) + off = 12; /* scheme-param bytes (AAD-bound) */ + else + off = fsize - 1; /* CORRUPT_TAG: last GCM tag byte */ + fseek(f, off, SEEK_SET); + if (fread(&b, 1, 1, f) == 1) { + b ^= 0xFF; + fseek(f, off, SEEK_SET); + fwrite(&b, 1, 1, f); + } + else { + fclose(f); + return CKR_GENERAL_ERROR; + } + fclose(f); + } + + /* Reload and attempt to use the key: it must be refused - detection may + * surface at login-time reload or at C_Sign - never a produced signature. */ + ret = reopen_token_quiet(&session); + if (ret != CKR_OK) { + printf(" detected at login: %lx\n", (unsigned long)ret); + ret = CKR_OK; + } + else if (find_by_id_quiet(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv) + != CKR_OK) { + printf(" key not usable after corruption\n"); + } + else { + sret = hss_sign(session, priv, &sig, &sigLen); + if (sret == CKR_OK) { + fprintf(stderr, "\n[%s] signed with corrupted state - FAIL\n", + label); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + ret = CKR_GENERAL_ERROR; + } + else { + printf(" signing refused: %lx\n", (unsigned long)sret); + } + } + close_session(session); + pkcs11_final(); + return ret; +} + +static CK_RV test_corruption_matrix(void) +{ + CK_RV ret; + + printf(" on-disk state corruption matrix...\n"); + ret = corrupt_state_and_check("truncated", CORRUPT_TRUNCATE); + if (ret == CKR_OK) + ret = corrupt_state_and_check("bad magic", CORRUPT_MAGIC); + if (ret == CKR_OK) + ret = corrupt_state_and_check("bad scheme param", CORRUPT_PARAM); + if (ret == CKR_OK) + ret = corrupt_state_and_check("flipped GCM tag", CORRUPT_TAG); + return ret; +} +#endif /* !_WIN32 */ + +/* ---- Phase 2: parameter matrix ---- */ +typedef struct { + const char* name; + CK_ULONG levels; + CK_ULONG lms; + CK_ULONG lmots; + CK_ULONG capacity; /* 2^(levels*height) */ + int fast; /* 1 = part of the default fast subset */ +} HssParamSet; + +static const HssParamSet paramSets[] = { + { "L1/H5/W1", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W1, 32, 0 }, + { "L1/H5/W2", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W2, 32, 0 }, + { "L1/H5/W4", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W4, 32, 1 }, + { "L1/H5/W8", 1, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W8, 32, 0 }, + { "L1/H10/W4", 1, CKL_LMS_SHA256_M32_H10, CKL_LMOTS_SHA256_N32_W4, 1024, 0 }, + { "L2/H5/W4", 2, CKL_LMS_SHA256_M32_H5, CKL_LMOTS_SHA256_N32_W4, 1024, 1 }, +}; + +/* keygen -> sign x2 -> verify -> reload -> confirm counter preserved -> sign + * -> verify, for one parameter set. Each set gets a fresh token. */ +static CK_RV run_param_set(const HssParamSet* ps) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-param"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, remaining = 0, remaining2 = 0; + int i; + + printf(" [%s] keygen/sign/verify/reload...\n", ps->name); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, ps->levels, ps->lms, ps->lmots, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "param-set keygen"); + } + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remaining); + CHECK_CKR(ret, "param-set keys-remaining"); + } + if (ret == CKR_OK && remaining != ps->capacity) { + fprintf(stderr, "\n[%s] capacity %lu, expected %lu - FAIL\n", ps->name, + (unsigned long)remaining, (unsigned long)ps->capacity); + ret = CKR_GENERAL_ERROR; + } + for (i = 0; ret == CKR_OK && i < 2; i++) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "param-set pre-reload sign/verify"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + /* Reload and confirm the counter survived (capacity - 2 signs). */ + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + if (ret == CKR_OK) + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, &pub); + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &remaining2); + if (ret == CKR_OK && remaining2 != ps->capacity - 2) { + fprintf(stderr, "\n[%s] post-reload remaining %lu, expected %lu " + "- FAIL\n", ps->name, (unsigned long)remaining2, + (unsigned long)(ps->capacity - 2)); + ret = CKR_GENERAL_ERROR; + } + if (ret == CKR_OK) + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "param-set post-reload sign/verify"); + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +static CK_RV test_param_matrix(void) +{ + CK_RV ret = CKR_OK; + int full = 0, i, n; + +#ifndef WOLFPKCS11_NO_ENV + if (XGETENV("WOLFPKCS11_HSS_FULL_MATRIX") != NULL) + full = 1; +#endif + n = (int)(sizeof(paramSets) / sizeof(paramSets[0])); + printf(" parameter matrix (%s)...\n", full ? "full" : "fast subset"); + for (i = 0; ret == CKR_OK && i < n; i++) { + if (!full && !paramSets[i].fast) + continue; + ret = run_param_set(¶mSets[i]); + } + if (ret == CKR_OK && !full) { + printf(" (set WOLFPKCS11_HSS_FULL_MATRIX=1 for W1/W2/W8 and H10)\n"); + } + return ret; +} + +/* Multi-level HSS (L2/H5): the lower subtree holds 2^5 = 32 one-time keys. + * Signing past that boundary exhausts the lower tree and advances the parent to + * a fresh subtree - the most error-prone HSS state transition. Confirm signing + * across the boundary verifies on both sides and that a reload straddling the + * boundary resumes correctly. */ +static CK_RV test_multilevel_rollover(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + static CK_BYTE id[] = "hss-rollover"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0, remaining = 0; + const int crossAt = 32; /* lower-subtree size, 2^5 */ + const int total = 36; /* a few signatures past the boundary */ + int i; + + printf(" multi-level (L2/H5) subtree rollover...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, 2, CKL_LMS_SHA256_M32_H5, + CKL_LMOTS_SHA256_N32_W4, id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "L2 keygen"); + } + for (i = 0; ret == CKR_OK && i < total; i++) { + ret = hss_sign(session, priv, &sig, &sigLen); + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + CHECK_CKR(ret, "rollover sign/verify"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + /* Reload right after crossing into the second subtree. */ + if (ret == CKR_OK && i == crossAt) { + printf(" --- reload straddling the subtree boundary ---\n"); + close_session(session); + session = CK_INVALID_HANDLE; + pkcs11_final(); + ret = reopen_token(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, + &pub); + CHECK_CKR(ret, "reload across boundary"); + } + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &remaining); + if (ret == CKR_OK && remaining != (CK_ULONG)(1024 - total)) { + fprintf(stderr, "\nrollover: remaining %lu, expected %d - FAIL\n", + (unsigned long)remaining, 1024 - total); + ret = CKR_GENERAL_ERROR; + } + if (sig != NULL) + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + close_session(session); + pkcs11_final(); + return ret; +} + +/* Destroying an unrelated token object renumbers the survivors, which unstores + * and re-stores them. The re-store rewrites only the object shell, so if the + * renumbering path also dropped the signature-state file the surviving private + * key would be permanently unusable. Destroy the PUBLIC key, which sits after + * the private key in the token list and so forces the private key through the + * renumbering loop, then reload and sign. */ +static CK_RV test_destroy_sibling_keeps_state(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-sibling"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + + printf(" destroying the public key keeps the private state...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + /* One signature so the on-disk state is past the genesis index. */ + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "baseline sign"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + } + if (ret == CKR_OK) { + ret = funcList->C_DestroyObject(session, pub); + CHECK_CKR(ret, "C_DestroyObject(public)"); + } + close_session(session); + pkcs11_final(); + + /* Reload the token: the private key must still decode and sign. */ + if (ret == CKR_OK) { + ret = reopen_token(&session); + CHECK_CKR(ret, "reopen after destroying the public key"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, &priv); + CHECK_CKR(ret, "private key survived"); + } + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "sign after the sibling was destroyed"); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + if (ret == CKR_OK) + printf(" private key still signs\n"); + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* A spent key cannot be reloaded - wolfSSL rejects a key with no signatures + * left. That must not take the whole token down: C_Login has to succeed so the + * other objects stay reachable, while the spent key itself reports + * CKR_DEVICE_ERROR. */ +static CK_RV test_exhausted_key_login(void) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE aes = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-spent"; + CK_BYTE aesId[] = "aes-bystander"; + CK_MECHANISM mech; + CK_BYTE sig[4096]; + CK_ULONG sigLen; + int i; + CK_OBJECT_CLASS secretClass = CKO_SECRET_KEY; + CK_KEY_TYPE aesType = CKK_AES; + CK_BYTE aesKey[32] = { 0 }; + CK_ATTRIBUTE aesTmpl[] = { + { CKA_CLASS, &secretClass, sizeof(secretClass) }, + { CKA_KEY_TYPE, &aesType, sizeof(aesType) }, + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, aesId, sizeof(aesId) - 1 }, + { CKA_VALUE, aesKey, sizeof(aesKey) }, + }; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + printf(" an exhausted key does not break login for the token...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + /* An unrelated token object that must remain reachable afterwards. */ + if (ret == CKR_OK) { + ret = funcList->C_CreateObject(session, aesTmpl, + sizeof(aesTmpl) / sizeof(*aesTmpl), &aes); + CHECK_CKR(ret, "create bystander AES key"); + } + /* Spend the whole tree. */ + for (i = 0; ret == CKR_OK && i < HSS_H5_SIGS + 4; i++) { + sret = funcList->C_SignInit(session, &mech, priv); + if (sret != CKR_OK) + break; + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (sret != CKR_OK) + break; + } + close_session(session); + pkcs11_final(); + + /* The whole point: login must still work. */ + if (ret == CKR_OK) { + ret = reopen_token(&session); + CHECK_CKR(ret, "C_Login with an exhausted HSS key on the token"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_SECRET_KEY, aesId, sizeof(aesId) - 1, + &aes); + CHECK_CKR(ret, "bystander key still reachable"); + } + if (ret == CKR_OK) { + printf(" login succeeded, other objects reachable\n"); + /* A key whose state cannot be reloaded stays flagged as undecoded, + * which is how this library already treats any object it could not + * decode for the current user: wp11_Session_FindNext skips it. So the + * spent key is not enumerable rather than enumerable-but-erroring. + * Assert that, and that it is the ONLY thing lost - the token and its + * other objects keep working. */ + if (find_by_id_quiet(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv) == CKR_OK) { + /* Should it become enumerable one day, it must still refuse. */ + sret = funcList->C_SignInit(session, &mech, priv); + if (sret == CKR_OK) { + sigLen = sizeof(sig); + sret = funcList->C_Sign(session, (CK_BYTE*)testMsg, + TEST_MSG_LEN, sig, &sigLen); + } + CHECK_CKR_FAIL(sret, CKR_DEVICE_ERROR, "spent-key sign"); + if (sret != CKR_DEVICE_ERROR) + ret = CKR_GENERAL_ERROR; + else + printf(" spent key reports CKR_DEVICE_ERROR\n"); + } + else { + printf(" spent key not enumerable, token otherwise intact\n"); + } + } + /* The bystander key must still be usable and destroyable, proving the + * token is fully operational and not merely loggable-into. */ + if (ret == CKR_OK) { + ret = funcList->C_DestroyObject(session, aes); + CHECK_CKR(ret, "C_DestroyObject on the bystander key"); + } + close_session(session); + pkcs11_final(); + return ret; +} + +/* C_Logout re-encodes the live objects with protect set, which releases the + * signing key; the next C_Login decodes it again from the durable state. Both + * halves happen inside one C_Initialize, a path no other test covers. Run it + * twice so a leak or a stale key surfaces on the second cycle. */ +static CK_RV test_logout_login_cycle(void) +{ + CK_RV ret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-cycle"; + CK_BYTE* sig = NULL; + CK_ULONG sigLen = 0; + CK_ULONG remaining = 0, prevRemaining = 0; + int cycle; + + printf(" logout/login cycle keeps the key usable...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + if (ret == CKR_OK) + ret = get_keys_remaining(session, priv, &prevRemaining); + + for (cycle = 0; ret == CKR_OK && cycle < 2; cycle++) { + if (ret == CKR_OK) { + ret = funcList->C_Logout(session); + CHECK_CKR(ret, "C_Logout"); + } + if (ret == CKR_OK) { + ret = funcList->C_Login(session, CKU_USER, userPin, userPinLen); + CHECK_CKR(ret, "C_Login after logout"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_PRIVATE_KEY, id, sizeof(id) - 1, + &priv); + CHECK_CKR(ret, "private key after re-login"); + } + if (ret == CKR_OK) { + ret = hss_sign(session, priv, &sig, &sigLen); + CHECK_CKR(ret, "sign after re-login"); + } + if (ret == CKR_OK) { + ret = find_by_id(session, CKO_PUBLIC_KEY, id, sizeof(id) - 1, + &pub); + } + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + if (sig != NULL) { + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + sig = NULL; + } + /* The index must move forward across the cycle, never back. */ + if (ret == CKR_OK) { + ret = get_keys_remaining(session, priv, &remaining); + CHECK_CKR(ret, "keys-remaining after cycle"); + } + if (ret == CKR_OK && remaining >= prevRemaining) { + fprintf(stderr, + "\nkeys-remaining did not advance across cycle %d " + "(%lu -> %lu) - FAIL\n", cycle, + (unsigned long)prevRemaining, (unsigned long)remaining); + ret = CKR_GENERAL_ERROR; + } + prevRemaining = remaining; + } + if (ret == CKR_OK) + printf(" two logout/login cycles, state advanced each time\n"); + close_session(session); + pkcs11_final(); + return ret; +} + +/* A second C_SignInit while an HSS signing operation is already active must be + * refused. This only works if WP11_INIT_HSS_SIGN is registered as a sign-class + * operation; if it is not, the second init silently replaces the first. */ +static CK_RV test_sign_init_operation_active(void) +{ + CK_RV ret, sret; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_BYTE id[] = "hss-active"; + CK_MECHANISM mech; + CK_BYTE sig[4096]; + CK_ULONG sigLen; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + printf(" a second C_SignInit is refused while one is active...\n"); + ret = fresh_token(&session); + if (ret == CKR_OK) { + ret = gen_hss_key(session, hssLevels, hssLmsType, hssLmotsType, + id, sizeof(id) - 1, &pub, &priv); + CHECK_CKR(ret, "keygen"); + } + if (ret == CKR_OK) { + ret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR(ret, "first C_SignInit"); + } + if (ret == CKR_OK) { + sret = funcList->C_SignInit(session, &mech, priv); + CHECK_CKR_FAIL(sret, CKR_OPERATION_ACTIVE, "second C_SignInit"); + if (sret != CKR_OPERATION_ACTIVE) + ret = CKR_GENERAL_ERROR; + else + printf(" second C_SignInit returned CKR_OPERATION_ACTIVE\n"); + } + /* The first operation must still be the live one and complete normally. */ + if (ret == CKR_OK) { + sigLen = sizeof(sig); + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + CHECK_CKR(ret, "C_Sign completes the original operation"); + } + if (ret == CKR_OK) + ret = hss_verify(session, pub, sig, sigLen); + close_session(session); + pkcs11_final(); + return ret; +} + +static CK_RV run_all(void) +{ + CK_RV ret; + + ret = test_keygen_sign_verify(); + if (ret == CKR_OK) + ret = test_reload_no_rollback(); + if (ret == CKR_OK) + ret = test_keygen_negative_templates(); + if (ret == CKR_OK) + ret = test_keys_remaining_private(); + if (ret == CKR_OK) + ret = test_import_value_rejected(); + if (ret == CKR_OK) + ret = test_exhaustion_poison(); + if (ret == CKR_OK) + ret = test_sign_init_operation_active(); + if (ret == CKR_OK) + ret = test_exhausted_key_login(); + if (ret == CKR_OK) + ret = test_logout_login_cycle(); + if (ret == CKR_OK) + ret = test_destroy_sibling_keeps_state(); + if (ret == CKR_OK) + ret = test_param_matrix(); + if (ret == CKR_OK) + ret = test_multilevel_rollover(); +#ifndef _WIN32 + if (ret == CKR_OK) + ret = test_destroy_removes_state(); + if (ret == CKR_OK) + ret = test_state_tamper_detected(); + if (ret == CKR_OK) + ret = test_write_failure_poison(); + if (ret == CKR_OK) + ret = test_crash_durability(); + if (ret == CKR_OK) + ret = test_corruption_matrix(); +#endif + return ret; +} +#endif /* WOLFPKCS11_LMS_PRIVATE && !WOLFPKCS11_NO_STORE */ + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) + { + CK_RV ret; + +#ifndef WOLFPKCS11_NO_ENV + if (!XGETENV("WOLFPKCS11_TOKEN_PATH")) + XSETENV("WOLFPKCS11_TOKEN_PATH", storeDir, 1); + else + storeDir = XGETENV("WOLFPKCS11_TOKEN_PATH"); +#endif + + printf("wolfPKCS11 LMS/HSS private-key (sign) test\n"); + printf("=========================================\n\n"); + + ret = run_all(); + + cleanup_store(); + if (ret == CKR_OK) { + printf("\nAll tests passed!\n"); + return 0; + } + printf("\nTest failed with error: %lx\n", (unsigned long)ret); + return 1; + } +#else + printf("LMS/HSS sign support (--enable-lms-private) not compiled in!\n"); + return 77; +#endif +} diff --git a/tests/hss_concurrency_test.c b/tests/hss_concurrency_test.c new file mode 100644 index 00000000..7799ee68 --- /dev/null +++ b/tests/hss_concurrency_test.c @@ -0,0 +1,525 @@ +/* hss_concurrency_test.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfPKCS11. + * + * wolfPKCS11 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfPKCS11 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + * + * Concurrency test for the sign-capable LMS/HSS build + * (--enable-lms-private). The one catastrophic failure mode of a stateful + * hash-based scheme is two signers using the same one-time-signature (OTS) + * leaf index. This test drives many threads signing concurrently with a single + * shared token-resident HSS private key, extracts the leaf index q from every + * RFC 8554 signature, and asserts: + * + * - every one of the N*M signatures carries a distinct leaf index, + * - the indices are exactly {0, 1, ..., N*M-1} (no reuse AND no gaps), + * - every signature verifies against the public key, + * - CKA_HSS_KEYS_REMAINING decreased by exactly N*M. + * + * Best run under ThreadSanitizer. + * + * The scheme is HSS L1 (single level) so the leaf index is the big-endian u32 + * immediately after the 4-byte HSS Nspk field: sig = u32(Nspk=0) || lms_sig, + * and lms_sig = u32(q) || ... (RFC 8554 sec. 3.3, 5.4). + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include + +#ifndef WOLFPKCS11_USER_SETTINGS + #include +#endif +#include + +#ifndef HAVE_PKCS11_STATIC +#include +#endif + +#include +#include + +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) && \ + !defined(SINGLE_THREADED) && !defined(_WIN32) + +#include +#include +#include + +#ifndef WOLFPKCS11_DLL_FILENAME + #ifdef __MACH__ + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.dylib" + #else + #define WOLFPKCS11_DLL_FILENAME "./src/.libs/libwolfpkcs11.so" + #endif +#endif + +/* H10 = 1024-leaf tree; NUM_THREADS*SIGS_PER_THREAD stays well under it so the + * test exercises contention without hitting the exhaustion boundary. */ +#define NUM_THREADS 4 +#define SIGS_PER_THREAD 64 +#define TOTAL_SIGS (NUM_THREADS * SIGS_PER_THREAD) +#define MAX_SIG_LEN 8192 + +#define CHECK_CKR(rv, msg) \ + do { \ + if ((rv) != CKR_OK) { \ + fprintf(stderr, "\n%s:%d - %s: %lx - FAIL\n", \ + __FILE__, __LINE__, msg, (unsigned long)(rv)); \ + } \ + } \ + while (0) + +#ifndef HAVE_PKCS11_STATIC +static void* dlib; +#endif +static CK_FUNCTION_LIST* funcList; +static CK_SLOT_ID slot = 0; +static const char* tokenName = "wolfpkcs11"; +static byte* soPin = (byte*)"password123456"; +static int soPinLen = 14; +static byte* userPin = (byte*)"wolfpkcs11-test"; +static int userPinLen = 15; +static const char* storeDir = "./store/hss_conc"; + +static CK_BBOOL ckTrue = CK_TRUE; +static CK_ULONG hssLevels = 1; +static CK_ULONG hssLmsType = CKL_LMS_SHA256_M32_H10; +static CK_ULONG hssLmotsType = CKL_LMOTS_SHA256_N32_W4; +static CK_BYTE keyId[] = "hss-concurrency"; +static const CK_BYTE testMsg[] = "wolfPKCS11 HSS concurrency test message"; +#define TEST_MSG_LEN ((CK_ULONG)(sizeof(testMsg) - 1)) + +/* Per-thread context: the leaf indices this thread observed. */ +typedef struct { + int threadId; + word32 leaf[SIGS_PER_THREAD]; + int signedCount; + CK_RV err; +} WorkerCtx; + +static CK_RV pkcs11_init(void) +{ + CK_RV ret; + CK_C_INITIALIZE_ARGS args; + CK_SLOT_ID slotList[16]; + CK_ULONG slotCount = sizeof(slotList) / sizeof(slotList[0]); +#ifndef HAVE_PKCS11_STATIC + CK_C_GetFunctionList func; + + dlib = dlopen(WOLFPKCS11_DLL_FILENAME, RTLD_NOW | RTLD_LOCAL); + if (dlib == NULL) { + fprintf(stderr, "dlopen error: %s\n", dlerror()); + return CKR_GENERAL_ERROR; + } + func = (CK_C_GetFunctionList)dlsym(dlib, "C_GetFunctionList"); + if (func == NULL) { + dlclose(dlib); + return CKR_GENERAL_ERROR; + } + ret = func(&funcList); + if (ret != CKR_OK) { + dlclose(dlib); + return ret; + } +#else + ret = C_GetFunctionList(&funcList); + if (ret != CKR_OK) + return ret; +#endif + + XMEMSET(&args, 0, sizeof(args)); + args.flags = CKF_OS_LOCKING_OK; + ret = funcList->C_Initialize(&args); + CHECK_CKR(ret, "Initialize"); + if (ret == CKR_OK) { + ret = funcList->C_GetSlotList(CK_TRUE, slotList, &slotCount); + CHECK_CKR(ret, "Get Slot List"); + } + if (ret == CKR_OK) { + if (slotCount > 0) + slot = slotList[0]; + else + ret = CKR_GENERAL_ERROR; + } + return ret; +} + +static void pkcs11_final(void) +{ + funcList->C_Finalize(NULL); +#ifndef HAVE_PKCS11_STATIC + if (dlib != NULL) { + dlclose(dlib); + dlib = NULL; + } +#endif +} + +static CK_RV pkcs11_init_token(void) +{ + CK_RV ret; + unsigned char label[32]; + CK_SESSION_HANDLE session; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + XMEMSET(label, ' ', sizeof(label)); + XMEMCPY(label, tokenName, strlen(tokenName)); + ret = funcList->C_InitToken(slot, soPin, soPinLen, label); + CHECK_CKR(ret, "Init Token"); + if (ret == CKR_OK) { + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, &session); + CHECK_CKR(ret, "Open Session for PIN setup"); + } + if (ret == CKR_OK) { + ret = funcList->C_Login(session, CKU_SO, soPin, soPinLen); + CHECK_CKR(ret, "Login SO"); + if (ret == CKR_OK) { + ret = funcList->C_InitPIN(session, userPin, userPinLen); + CHECK_CKR(ret, "Init User PIN"); + } + funcList->C_Logout(session); + funcList->C_CloseSession(session); + } + return ret; +} + +/* Open a session and ensure the token is logged in as the user. Login is + * application-wide, so a second concurrent login returns + * CKR_USER_ALREADY_LOGGED_IN, which is success for our purposes. */ +static CK_RV open_logged_in_session(CK_SESSION_HANDLE* session) +{ + CK_RV ret; + int flags = CKF_SERIAL_SESSION | CKF_RW_SESSION; + + ret = funcList->C_OpenSession(slot, flags, NULL, NULL, session); + if (ret != CKR_OK) + return ret; + ret = funcList->C_Login(*session, CKU_USER, userPin, userPinLen); + if (ret == CKR_USER_ALREADY_LOGGED_IN) + ret = CKR_OK; + return ret; +} + +static CK_RV gen_hss_key(CK_SESSION_HANDLE session) +{ + CK_RV ret; + CK_MECHANISM mech; + CK_OBJECT_HANDLE pub = CK_INVALID_HANDLE, priv = CK_INVALID_HANDLE; + CK_ATTRIBUTE pubTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_VERIFY, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, keyId, sizeof(keyId) - 1 }, + { CKA_HSS_LEVELS, &hssLevels, sizeof(hssLevels) }, + { CKA_HSS_LMS_TYPE, &hssLmsType, sizeof(hssLmsType) }, + { CKA_HSS_LMOTS_TYPE, &hssLmotsType, sizeof(hssLmotsType) }, + }; + CK_ATTRIBUTE privTmpl[] = { + { CKA_TOKEN, &ckTrue, sizeof(ckTrue) }, + { CKA_SIGN, &ckTrue, sizeof(ckTrue) }, + { CKA_ID, keyId, sizeof(keyId) - 1 }, + }; + + mech.mechanism = CKM_HSS_KEY_PAIR_GEN; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + ret = funcList->C_GenerateKeyPair(session, &mech, + pubTmpl, sizeof(pubTmpl) / sizeof(*pubTmpl), + privTmpl, sizeof(privTmpl) / sizeof(*privTmpl), &pub, &priv); + CHECK_CKR(ret, "C_GenerateKeyPair(HSS L1/H10)"); + return ret; +} + +static CK_RV find_by_id(CK_SESSION_HANDLE session, CK_OBJECT_CLASS cls, + CK_OBJECT_HANDLE* obj) +{ + CK_RV ret; + CK_ULONG count = 0; + CK_ATTRIBUTE tmpl[] = { + { CKA_CLASS, &cls, sizeof(cls) }, + { CKA_ID, keyId, sizeof(keyId) - 1 }, + }; + + ret = funcList->C_FindObjectsInit(session, tmpl, + sizeof(tmpl) / sizeof(*tmpl)); + if (ret == CKR_OK) { + ret = funcList->C_FindObjects(session, obj, 1, &count); + funcList->C_FindObjectsFinal(session); + } + if (ret == CKR_OK && count != 1) + ret = CKR_GENERAL_ERROR; + return ret; +} + +/* Big-endian u32 read. */ +static word32 read_be32(const CK_BYTE* p) +{ + return ((word32)p[0] << 24) | ((word32)p[1] << 16) | + ((word32)p[2] << 8) | (word32)p[3]; +} + +/* Extract the LMS leaf index q from an HSS L1 signature. Returns 0 on success + * and sets *q; non-zero if the signature is too short or not single-level. */ +static int hss_leaf_index(const CK_BYTE* sig, CK_ULONG sigLen, word32* q) +{ + word32 nspk; + + if (sigLen < 8) + return -1; + nspk = read_be32(sig); /* HSS Nspk = L-1; must be 0 for L1 */ + if (nspk != 0) + return -1; + *q = read_be32(sig + 4); /* first field of the LMS signature */ + return 0; +} + +static void* worker(void* arg) +{ + WorkerCtx* ctx = (WorkerCtx*)arg; + CK_SESSION_HANDLE session = CK_INVALID_HANDLE; + CK_OBJECT_HANDLE priv = CK_INVALID_HANDLE, pub = CK_INVALID_HANDLE; + CK_MECHANISM mech; + CK_BYTE sig[MAX_SIG_LEN]; + CK_ULONG sigLen; + CK_RV ret; + int i; + + mech.mechanism = CKM_HSS; + mech.pParameter = NULL; + mech.ulParameterLen = 0; + + ret = open_logged_in_session(&session); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PRIVATE_KEY, &priv); + if (ret == CKR_OK) + ret = find_by_id(session, CKO_PUBLIC_KEY, &pub); + + for (i = 0; ret == CKR_OK && i < SIGS_PER_THREAD; i++) { + ret = funcList->C_SignInit(session, &mech, priv); + if (ret != CKR_OK) + break; + sigLen = sizeof(sig); + ret = funcList->C_Sign(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, sig, + &sigLen); + if (ret != CKR_OK) + break; + /* Verify and record the leaf index. */ + ret = funcList->C_VerifyInit(session, &mech, pub); + if (ret == CKR_OK) { + ret = funcList->C_Verify(session, (CK_BYTE*)testMsg, TEST_MSG_LEN, + sig, sigLen); + } + if (ret != CKR_OK) + break; + if (hss_leaf_index(sig, sigLen, &ctx->leaf[i]) != 0) { + ret = CKR_GENERAL_ERROR; + break; + } + ctx->signedCount++; + } + + if (session != CK_INVALID_HANDLE) + funcList->C_CloseSession(session); + ctx->err = ret; + return NULL; +} + +static CK_RV get_keys_remaining(CK_SESSION_HANDLE session, CK_ULONG* remaining) +{ + CK_RV ret; + CK_OBJECT_HANDLE priv = CK_INVALID_HANDLE; + CK_ATTRIBUTE a; + + ret = find_by_id(session, CKO_PRIVATE_KEY, &priv); + if (ret == CKR_OK) { + a.type = CKA_HSS_KEYS_REMAINING; + a.pValue = remaining; + a.ulValueLen = sizeof(*remaining); + ret = funcList->C_GetAttributeValue(session, priv, &a, 1); + } + return ret; +} + +static int cmp_word32(const void* a, const void* b) +{ + word32 x = *(const word32*)a; + word32 y = *(const word32*)b; + + if (x < y) + return -1; + if (x > y) + return 1; + return 0; +} + +static void cleanup_store(void) +{ + DIR* d; + struct dirent* e; + char path[512]; + + d = opendir(storeDir); + if (d == NULL) + return; + while ((e = readdir(d)) != NULL) { + if (strncmp(e->d_name, "wp11_", 5) == 0) { + snprintf(path, sizeof(path), "%s/%s", storeDir, e->d_name); + (void)remove(path); + } + } + closedir(d); +} + +static CK_RV run_test(void) +{ + CK_RV ret; + CK_SESSION_HANDLE keeper = CK_INVALID_HANDLE; + pthread_t threads[NUM_THREADS]; + WorkerCtx ctx[NUM_THREADS]; + word32 allLeaves[TOTAL_SIGS]; + CK_ULONG remainBefore = 0, remainAfter = 0; + int i, j, n = 0; + int spawned = 0; + + XMEMSET(ctx, 0, sizeof(ctx)); + + cleanup_store(); + ret = pkcs11_init(); + if (ret == CKR_OK) + ret = pkcs11_init_token(); + /* Keeper session keeps the token logged in for the workers. */ + if (ret == CKR_OK) + ret = open_logged_in_session(&keeper); + if (ret == CKR_OK) + ret = gen_hss_key(keeper); + if (ret == CKR_OK) + ret = get_keys_remaining(keeper, &remainBefore); + + /* Fan out. */ + for (i = 0; ret == CKR_OK && i < NUM_THREADS; i++) { + ctx[i].threadId = i; + ctx[i].err = CKR_OK; + if (pthread_create(&threads[i], NULL, worker, &ctx[i]) != 0) { + ret = CKR_GENERAL_ERROR; + break; + } + spawned++; + } + for (i = 0; i < spawned; i++) + pthread_join(threads[i], NULL); + + /* Collect results. */ + for (i = 0; ret == CKR_OK && i < NUM_THREADS; i++) { + if (ctx[i].err != CKR_OK) { + fprintf(stderr, "\nthread %d failed: %lx - FAIL\n", i, + (unsigned long)ctx[i].err); + ret = ctx[i].err; + } + else if (ctx[i].signedCount != SIGS_PER_THREAD) { + fprintf(stderr, "\nthread %d signed %d/%d - FAIL\n", i, + ctx[i].signedCount, SIGS_PER_THREAD); + ret = CKR_GENERAL_ERROR; + } + else { + for (j = 0; j < SIGS_PER_THREAD; j++) + allLeaves[n++] = ctx[i].leaf[j]; + } + } + + /* The N*M signatures must use leaf indices {0, 1, ..., N*M-1} exactly - + * any duplicate is an OTS reuse, any gap is a lost index. */ + if (ret == CKR_OK) { + qsort(allLeaves, TOTAL_SIGS, sizeof(word32), cmp_word32); + for (i = 0; i < TOTAL_SIGS; i++) { + if (allLeaves[i] != (word32)i) { + if (i > 0 && allLeaves[i] == allLeaves[i - 1]) { + fprintf(stderr, "\nOTS INDEX REUSED: leaf %lu appears " + "twice - FAIL\n", (unsigned long)allLeaves[i]); + } + else { + fprintf(stderr, "\nleaf index sequence broken at %d: got " + "%lu - FAIL\n", i, (unsigned long)allLeaves[i]); + } + ret = CKR_GENERAL_ERROR; + break; + } + } + } + + /* Keys-remaining must have dropped by exactly TOTAL_SIGS. */ + if (ret == CKR_OK) + ret = get_keys_remaining(keeper, &remainAfter); + if (ret == CKR_OK && remainAfter != remainBefore - TOTAL_SIGS) { + fprintf(stderr, "\nkeys-remaining %lu -> %lu, expected drop of %d " + "- FAIL\n", (unsigned long)remainBefore, + (unsigned long)remainAfter, TOTAL_SIGS); + ret = CKR_GENERAL_ERROR; + } + if (ret == CKR_OK) { + printf(" %d threads x %d signatures = %d unique leaf indices, all " + "verified\n", NUM_THREADS, SIGS_PER_THREAD, TOTAL_SIGS); + } + + if (keeper != CK_INVALID_HANDLE) { + funcList->C_Logout(keeper); + funcList->C_CloseSession(keeper); + } + pkcs11_final(); + return ret; +} +#endif /* WOLFPKCS11_LMS_PRIVATE && !NO_STORE && !SINGLE_THREADED && !_WIN32 */ + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_NO_STORE) && \ + !defined(SINGLE_THREADED) && !defined(_WIN32) + { + CK_RV ret; + +#ifndef WOLFPKCS11_NO_ENV + if (!XGETENV("WOLFPKCS11_TOKEN_PATH")) + XSETENV("WOLFPKCS11_TOKEN_PATH", storeDir, 1); + else + storeDir = XGETENV("WOLFPKCS11_TOKEN_PATH"); +#endif + + printf("wolfPKCS11 HSS concurrent-signing (OTS reuse) test\n"); + printf("=================================================\n\n"); + + ret = run_test(); + + cleanup_store(); + if (ret == CKR_OK) { + printf("\nAll tests passed!\n"); + return 0; + } + printf("\nTest failed with error: %lx\n", (unsigned long)ret); + return 1; + } +#else + printf("LMS/HSS sign, store, and threads required - skipping.\n"); + return 77; +#endif +} diff --git a/tests/include.am b/tests/include.am index 5af7cbba..7d9164ac 100644 --- a/tests/include.am +++ b/tests/include.am @@ -250,6 +250,16 @@ noinst_PROGRAMS += tests/hbs_persistence_test tests_hbs_persistence_test_SOURCES = tests/hbs_persistence_test.c tests_hbs_persistence_test_LDADD = +check_PROGRAMS += tests/hbs_private_test +noinst_PROGRAMS += tests/hbs_private_test +tests_hbs_private_test_SOURCES = tests/hbs_private_test.c +tests_hbs_private_test_LDADD = + +check_PROGRAMS += tests/hss_concurrency_test +noinst_PROGRAMS += tests/hss_concurrency_test +tests_hss_concurrency_test_SOURCES = tests/hss_concurrency_test.c +tests_hss_concurrency_test_LDADD = + if BUILD_STATIC tests_pkcs11test_LDADD += src/libwolfpkcs11.la tests_pkcs11mtt_LDADD += src/libwolfpkcs11.la @@ -301,6 +311,8 @@ tests_logout_token_key_zero_test_LDADD += src/libwolfpkcs11.la tests_tpm_decode_bounds_test_LDADD += src/libwolfpkcs11.la tests_concurrent_destroy_object_test_LDADD += src/libwolfpkcs11.la tests_hbs_persistence_test_LDADD += src/libwolfpkcs11.la +tests_hbs_private_test_LDADD += src/libwolfpkcs11.la +tests_hss_concurrency_test_LDADD += src/libwolfpkcs11.la else tests_object_id_uniqueness_test_LDADD += src/libwolfpkcs11.la tests_empty_pin_store_test_LDADD += src/libwolfpkcs11.la @@ -344,6 +356,8 @@ tests_logout_token_key_zero_test_LDADD += src/libwolfpkcs11.la tests_tpm_decode_bounds_test_LDADD += src/libwolfpkcs11.la tests_concurrent_destroy_object_test_LDADD += src/libwolfpkcs11.la tests_hbs_persistence_test_LDADD += src/libwolfpkcs11.la +tests_hbs_private_test_LDADD += src/libwolfpkcs11.la +tests_hss_concurrency_test_LDADD += src/libwolfpkcs11.la endif EXTRA_DIST += tests/unit.h \ diff --git a/tests/pkcs11v3test.c b/tests/pkcs11v3test.c index b88ff5fa..ef4c3f0e 100644 --- a/tests/pkcs11v3test.c +++ b/tests/pkcs11v3test.c @@ -3046,8 +3046,25 @@ static CK_RV hbs_keygen_rejected(CK_SESSION_HANDLE session, mech.ulParameterLen = 0; ret = funcList->C_GenerateKeyPair(session, &mech, pubTmpl, 1, privTmpl, 1, &pub, &priv); - CHECK_CKR_FAIL(ret, CKR_MECHANISM_INVALID, - "HBS keygen rejected in verify-only build"); +#ifdef WOLFPKCS11_LMS_PRIVATE + if (kgMech == CKM_HSS_KEY_PAIR_GEN) { + /* Sign-capable build: HSS keygen is supported. This minimal template + * omits CKA_TOKEN (and the scheme attributes), so keygen rejects it as + * an inconsistent template, not an invalid mechanism. */ + CHECK_CKR_FAIL(ret, CKR_TEMPLATE_INCONSISTENT, + "HSS keygen with incomplete template rejected"); + } + else +#endif + { + CHECK_CKR_FAIL(ret, CKR_MECHANISM_INVALID, + "HBS keygen rejected (no sign support for this scheme)"); + } + /* Should not have produced objects, but clean up if a regression did. */ + if (pub != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, pub); + if (priv != CK_INVALID_HANDLE) + funcList->C_DestroyObject(session, priv); return ret; } @@ -3118,6 +3135,16 @@ static CK_RV hbs_mech_info(CK_MECHANISM_TYPE mechType) ret = CKR_GENERAL_ERROR; CHECK_CKR(ret, "HBS mech info missing CKF_VERIFY"); } +#ifdef WOLFPKCS11_LMS_PRIVATE + if (mechType == CKM_HSS) { + /* Sign-capable build: CKM_HSS advertises signing too. */ + if (ret == CKR_OK && (info.flags & CKF_SIGN) == 0) { + ret = CKR_GENERAL_ERROR; + CHECK_CKR(ret, "HSS mech info missing CKF_SIGN (sign build)"); + } + } + else +#endif if (ret == CKR_OK && (info.flags & CKF_SIGN) != 0) { ret = CKR_GENERAL_ERROR; CHECK_CKR(ret, "HBS mech info unexpectedly claims CKF_SIGN"); @@ -3140,6 +3167,10 @@ static CK_RV hbs_empty_template_rejected(CK_SESSION_HANDLE session, ret = funcList->C_CreateObject(session, tmpl, sizeof(tmpl) / sizeof(*tmpl), &obj); + /* This holds in a sign-capable build too. C_GenerateKeyPair does create + * both objects from templates with no key material, but that is internal + * to keygen; C_CreateObject has no second step that would fill the value + * in, so an empty template is still an incomplete one. */ CHECK_CKR_FAIL(ret, CKR_ATTRIBUTE_VALUE_INVALID, "HBS empty-template public key rejected"); if (obj != CK_INVALID_HANDLE) diff --git a/wolfpkcs11/internal.h b/wolfpkcs11/internal.h index db888d52..0fd5f01e 100644 --- a/wolfpkcs11/internal.h +++ b/wolfpkcs11/internal.h @@ -133,6 +133,30 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #error Compiling with XMSS requires XMSS support in wolfSSL (--enable-xmss). #endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFSSL_LMS_VERIFY_ONLY) +#error LMS/HSS keygen+sign (--enable-lms-private) needs a sign-capable wolfSSL LMS build, not verify-only. +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && !defined(WOLFPKCS11_LMS) +#error WOLFPKCS11_LMS_PRIVATE requires WOLFPKCS11_LMS. +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_NO_STORE) +#error WOLFPKCS11_LMS_PRIVATE requires object storage for durable signature state (not WOLFPKCS11_NO_STORE). +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_TPM_STORE) +#error WOLFPKCS11_LMS_PRIVATE is incompatible with WOLFPKCS11_TPM_STORE (encrypted state blobs exceed TPM NV capacity). +#endif +#if defined(WOLFPKCS11_LMS_PRIVATE) && defined(WOLFPKCS11_CUSTOM_STORE) +#error WOLFPKCS11_LMS_PRIVATE is incompatible with WOLFPKCS11_CUSTOM_STORE (the durable signature-state file needs the built-in file store). +#endif + +/* Stateful hash-based signature framework selector. _PRIVATE gates the + * encrypted durable-state infrastructure (keygen + signing); it is compiled + * only in sign-capable builds. Future XMSS private-key support reuses the same + * framework. */ +#if defined(WOLFPKCS11_LMS_PRIVATE) +#define WOLFPKCS11_STATEFUL_SIG_PRIVATE +#endif + /* We need the next two for NSS, just for storage, even if we have no algos */ #ifndef WC_MD5_DIGEST_SIZE #define WC_MD5_DIGEST_SIZE 16 @@ -246,6 +270,10 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_FLAG_NOT_COPYABLE 0x00200000 #define WP11_FLAG_NOT_DESTROYABLE 0x00400000 #define WP11_FLAG_NOT_MODIFIABLE 0x00800000 +/* Set when a stateful private key's one-time-signature state is live and + * usable; cleared ("poisoned") on any keygen/sign/reload failure so a + * subsequent sign returns CKR_DEVICE_ERROR instead of risking OTS reuse. */ +#define WP11_FLAG_STATEFUL_STATE_VALID 0x01000000 /* Flags for token. */ #define WP11_TOKEN_FLAG_USER_PIN_SET 0x00000001 @@ -293,7 +321,7 @@ C_EXTRA_FLAGS="-DWOLFSSL_PUBLIC_MP -DWC_RSA_DIRECT" #define WP11_INIT_TLS_MAC_VERIFY 0x0071 #define WP11_INIT_MLDSA_SIGN 0x0080 #define WP11_INIT_MLDSA_VERIFY 0x0081 -#define WP11_INIT_HSS_SIGN 0x0090 /* Reserved for future use */ +#define WP11_INIT_HSS_SIGN 0x0090 #define WP11_INIT_HSS_VERIFY 0x0091 #define WP11_INIT_XMSS_SIGN 0x00A0 /* Reserved for future use */ #define WP11_INIT_XMSS_VERIFY 0x00A1 @@ -615,6 +643,18 @@ WP11_LOCAL int WP11_Object_SetHssKey(WP11_Object* object, unsigned char** data, WP11_LOCAL int WP11_Hss_Verify(unsigned char* sig, word32 sigLen, unsigned char* data, word32 dataLen, int* stat, WP11_Object* pub); +#ifdef WOLFPKCS11_LMS_PRIVATE +WP11_LOCAL int WP11_Hss_GenerateKeyPair(WP11_Object* pub, WP11_Object* priv, + CK_ULONG levels, CK_LMS_TYPE lmsType, + CK_LMOTS_TYPE lmotsType, + WP11_Slot* slot); +WP11_LOCAL int WP11_Hss_Sign(unsigned char* data, word32 dataLen, + unsigned char* sig, word32* sigLen, + WP11_Object* priv); +WP11_LOCAL word32 WP11_Hss_SigLen(WP11_Object* key); +WP11_LOCAL int WP11_Hss_SigsLeft(WP11_Object* key, word32* remaining); +WP11_LOCAL void WP11_Hss_RemoveOrphanState(WP11_Object* priv, WP11_Slot* slot); +#endif #endif #ifdef WOLFPKCS11_XMSS