Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/build-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/sanitizer-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
61 changes: 57 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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)"
Expand All @@ -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")
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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)
Expand Down
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Comment thread
Frauschi marked this conversation as resolved.
Resolved
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

Expand Down
2 changes: 2 additions & 0 deletions cmake/options.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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 <wolfssl/options.h>
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssl/wolfcrypt/wc_lms.h>]],
[[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],
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading