diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e38fbc6..9078a61 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -44,7 +44,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-${{ matrix.os }}-${{ matrix.wolfssl-ref }}-v4 + key: wolfssl-${{ matrix.os }}-${{ matrix.wolfssl-ref }}-v5-lms - name: Build wolfSSL (${{ matrix.wolfssl-ref }}) if: matrix.wolfssl-ref == 'master' || steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -69,6 +69,7 @@ jobs: --enable-sha384 --enable-sha512 \ --enable-keygen --enable-hkdf --enable-aeskeywrap \ --enable-chacha --enable-poly1305 \ + --enable-lms \ $PQC_FLAGS --enable-rsapss \ --prefix=$HOME/wolfssl-install make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) @@ -120,6 +121,140 @@ jobs: make tool-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + lms-lean: + name: LMS lean profiles (wolfSSL ${{ matrix.wolfssl-ref }}) + needs: discover + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + wolfssl-ref: ${{ fromJson(needs.discover.outputs.refs) }} + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + - name: Cache wolfSSL (${{ matrix.wolfssl-ref }}) + if: matrix.wolfssl-ref != 'master' + id: cache-wolfssl + uses: actions/cache@v4 + with: + path: ~/wolfssl-install + key: wolfssl-ubuntu-latest-${{ matrix.wolfssl-ref }}-v5-lms + + - name: Build wolfSSL (${{ matrix.wolfssl-ref }}) + if: matrix.wolfssl-ref == 'master' || steps.cache-wolfssl.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch ${{ matrix.wolfssl-ref }} \ + https://github.com/wolfSSL/wolfssl.git + cd wolfssl + ./autogen.sh + REF="${{ matrix.wolfssl-ref }}" + if [ "$REF" = "master" ] || \ + [ "$(printf '%s\n%s\n' v5.9.1-stable "$REF" | sort -V | tail -n1)" != "v5.9.1-stable" ]; then + PQC_FLAGS="--enable-mldsa" + else + PQC_FLAGS="" + fi + ./configure --enable-ecc --enable-ed25519 --enable-ed448 \ + --enable-curve25519 --enable-curve448 \ + --enable-aesgcm --enable-aesccm \ + --enable-sha384 --enable-sha512 \ + --enable-keygen --enable-hkdf --enable-aeskeywrap \ + --enable-chacha --enable-poly1305 \ + --enable-lms \ + $PQC_FLAGS --enable-rsapss \ + --prefix=$HOME/wolfssl-install + make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) + make install + + # LMS-only sign+verify profile (WOLFCOSE_LEAN_LMS), warnings fatal. + - name: LMS lean sign and verify + run: | + export WOLFSSL_DIR=$HOME/wolfssl-install + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make lms-demo CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + # Smallest verify-only profile (WOLFCOSE_LEAN_VERIFY_LMS), warnings fatal. + - name: LMS lean verify-only + run: | + export WOLFSSL_DIR=$HOME/wolfssl-install + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make lms-verify CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + # Strict ISO C99 (-pedantic-errors, -Wvla) over the LMS-gated branches and + # both lean LMS profiles, which the default c99-check cannot reach without + # an LMS wolfSSL. + - name: Strict C99 gate (LMS) + run: | + make c99-check-lms WOLFSSL_INC=$HOME/wolfssl-install/include + + lms-verify-only: + name: LMS verify-only wolfSSL profile + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + # Build wolfSSL with LMS support that cannot sign, mirroring a verifier + # deployment. --enable-lms=verify-only records WOLFSSL_LMS_VERIFY_ONLY in + # options.h, which wolfCOSE reads to compile out its own signing paths. + - name: Build wolfSSL (LMS verify-only) + run: | + cd ~ + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git + cd wolfssl + ./autogen.sh + ./configure --enable-ecc --enable-ed25519 --enable-ed448 \ + --enable-curve25519 --enable-curve448 \ + --enable-aesgcm --enable-aesccm \ + --enable-sha384 --enable-sha512 \ + --enable-keygen --enable-hkdf --enable-aeskeywrap \ + --enable-chacha --enable-poly1305 \ + --enable-lms=verify-only \ + --enable-mldsa --enable-rsapss \ + --prefix=$HOME/wolfssl-lmsvo + make -j$(nproc) + make install + + - name: Assert signing symbol absent from verify-only build + run: | + if nm -D $HOME/wolfssl-lmsvo/lib/libwolfssl.so 2>/dev/null \ + | grep -qw wc_LmsKey_Sign; then + echo "FAIL: wc_LmsKey_Sign present in a verify-only wolfSSL" + exit 1 + fi + echo "OK: wc_LmsKey_Sign absent from verify-only wolfSSL" + + # Fixed-vector verify with no sign-capable code in either library. + - name: LMS fixed-vector verify + run: | + export WOLFSSL_DIR=$HOME/wolfssl-lmsvo + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make lms-verify CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + # Delegated LMS signing against a backend that cannot sign: the callback + # supplies the signature, the public LmsKey only its length. + - name: Delegated LMS with verify-only backend + run: | + export WOLFSSL_DIR=$HOME/wolfssl-lmsvo + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make ext-sign-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + coverage: name: Code Coverage (wolfSSL ${{ matrix.wolfssl-version }}) needs: discover @@ -142,7 +277,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-coverage-${{ matrix.wolfssl-version }}-v4 + key: wolfssl-coverage-${{ matrix.wolfssl-version }}-v5-lms - name: Build wolfSSL (${{ matrix.wolfssl-version }}) if: matrix.wolfssl-version == 'master' || steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -165,6 +300,7 @@ jobs: --enable-sha384 --enable-sha512 \ --enable-keygen --enable-hkdf --enable-aeskeywrap \ --enable-chacha --enable-poly1305 \ + --enable-lms \ $PQC_FLAGS --enable-rsapss \ --prefix=$HOME/wolfssl-install make -j$(nproc) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 484d625..3420659 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,8 +33,9 @@ jobs: path: ~/wolfssl-install # Coverage needs its own cache: other workflows share the generic # ubuntu-latest key but configure wolfSSL without aeskeywrap, and a - # cache hit on such a build silently skips the KEY_WRAP tests. - key: wolfssl-coverage-fullcfg-v1-${{ steps.wolfssl-rev.outputs.sha }} + # cache hit on such a build silently skips the KEY_WRAP tests. The + # -lms suffix distinguishes the LMS-enabled coverage build. + key: wolfssl-coverage-fullcfg-lms-v1-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -49,7 +50,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --enable-hkdf --enable-aeskeywrap \ --enable-aescbc \ --prefix=$HOME/wolfssl-install diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index a17f9aa..7eecd5a 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -33,7 +33,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-coverity-v3-${{ steps.wolfssl-rev.outputs.sha }} + key: wolfssl-coverity-v4-lms-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL (latest stable + master, PQC-gated) if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -47,7 +47,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install diff --git a/.github/workflows/misra-2012.yml b/.github/workflows/misra-2012.yml index a4c0215..79b8914 100644 --- a/.github/workflows/misra-2012.yml +++ b/.github/workflows/misra-2012.yml @@ -43,7 +43,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-24.04-v4-${{ env.WOLFSSL_REF }} + key: wolfssl-ubuntu-24.04-v5-lms-${{ env.WOLFSSL_REF }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -59,7 +59,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -97,6 +97,7 @@ jobs: -DHAVE_ED448 \ -DWC_RSA_PSS \ -DWOLFSSL_HAVE_MLDSA \ + -DWOLFSSL_HAVE_LMS \ -DHAVE_AESGCM \ -DHAVE_AESCCM \ -DHAVE_CHACHA \ diff --git a/.github/workflows/misra-2023.yml b/.github/workflows/misra-2023.yml index ec7e1fb..4d40726 100644 --- a/.github/workflows/misra-2023.yml +++ b/.github/workflows/misra-2023.yml @@ -40,7 +40,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-latest-v3-${{ steps.wolfssl-rev.outputs.sha }} + key: wolfssl-ubuntu-latest-v4-lms-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -53,7 +53,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -94,7 +94,7 @@ jobs: -Wlogical-op -Wjump-misses-init -Wdouble-promotion \ -Wnull-dereference -Wsign-conversion \ -DHAVE_ECC -DHAVE_ED25519 -DHAVE_ED448 \ - -DWC_RSA_PSS -DWOLFSSL_HAVE_MLDSA \ + -DWC_RSA_PSS -DWOLFSSL_HAVE_MLDSA -DWOLFSSL_HAVE_LMS \ -DHAVE_AESGCM -DHAVE_AESCCM \ -DHAVE_CHACHA -DHAVE_POLY1305 \ -DWOLFSSL_SHA384 -DWOLFSSL_SHA512 \ @@ -168,7 +168,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-latest-v3-${{ steps.wolfssl-rev.outputs.sha }} + key: wolfssl-ubuntu-latest-v4-lms-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -181,7 +181,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -212,7 +212,7 @@ jobs: -checks='-*,bugprone-*,cert-*,clang-analyzer-*,misc-*,-misc-include-cleaner,-bugprone-branch-clone,-bugprone-easily-swappable-parameters,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling' \ -- -std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -I./include -isystem $WOLFSSL_DIR/include \ -DHAVE_ECC -DHAVE_ED25519 -DHAVE_ED448 \ - -DWC_RSA_PSS -DWOLFSSL_HAVE_MLDSA \ + -DWC_RSA_PSS -DWOLFSSL_HAVE_MLDSA -DWOLFSSL_HAVE_LMS \ -DHAVE_AESGCM -DHAVE_AESCCM \ -DHAVE_CHACHA -DHAVE_POLY1305 \ -DWOLFSSL_SHA384 -DWOLFSSL_SHA512 \ diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index a12ca88..75cdf39 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -31,7 +31,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-latest-v3-${{ steps.wolfssl-rev.outputs.sha }} + key: wolfssl-ubuntu-latest-v4-lms-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -44,7 +44,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -88,7 +88,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-latest-v3-${{ steps.wolfssl-rev.outputs.sha }} + key: wolfssl-ubuntu-latest-v4-lms-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -101,7 +101,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install @@ -139,7 +139,7 @@ jobs: uses: actions/cache@v4 with: path: ~/wolfssl-install - key: wolfssl-ubuntu-latest-v3-${{ steps.wolfssl-rev.outputs.sha }} + key: wolfssl-ubuntu-latest-v4-lms-${{ steps.wolfssl-rev.outputs.sha }} - name: Build wolfSSL if: steps.cache-wolfssl.outputs.cache-hit != 'true' @@ -152,7 +152,7 @@ jobs: --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --prefix=$HOME/wolfssl-install make -j$(nproc) make install diff --git a/.gitignore b/.gitignore index f766806..f732f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ examples/sign1_verify_lean examples/sign1_mldsa examples/ext_sign_demo examples/sign1_verify_mldsa +examples/sign1_lms +examples/sign1_verify_lms # Editor / OS *.swp diff --git a/Makefile b/Makefile index 45fb023..a3edbf7 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,8 @@ LEANV_DEMO = examples/sign1_verify_lean MLDSA_DEMO = examples/sign1_mldsa EXTSIGN_DEMO = examples/ext_sign_demo MLDSAV_DEMO = examples/sign1_verify_mldsa +LMS_DEMO = examples/sign1_lms +LMSV_DEMO = examples/sign1_verify_lms # Comprehensive tests (CI) COMP_SIGN = examples/comprehensive/sign_all @@ -118,7 +120,7 @@ SCEN_IOTFLEET = examples/scenarios/iot_fleet_config SCEN_SENSOR = examples/scenarios/sensor_attestation SCEN_BROADCAST = examples/scenarios/group_broadcast_mac -.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test zero-alloc-check zeroize-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify comprehensive scenarios interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check experimental-check clean FORCE +.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test zero-alloc-check zeroize-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms experimental-check clean FORCE # --- Core library --- all: $(LIB_A) @@ -151,7 +153,9 @@ ECDSA_POLICY_BASE_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ -UWOLFCOSE_NO_ES256 -UWOLFCOSE_NO_SIGN1 \ -UWOLFCOSE_NO_SIGN1_SIGN -UWOLFCOSE_LEAN_VERIFY \ -UWOLFCOSE_LEAN_VERIFY_MLDSA \ - -UWOLFCOSE_LEAN_MLDSA + -UWOLFCOSE_LEAN_MLDSA \ + -UWOLFCOSE_LEAN_VERIFY_LMS \ + -UWOLFCOSE_LEAN_LMS ECDSA_POLICY_NO_SUPPORT_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ -DWOLFSSL_NO_OPTIONS_H -DHAVE_ECC \ -UWOLFSSL_ECDSA_DETERMINISTIC_K \ @@ -160,7 +164,9 @@ ECDSA_POLICY_NO_SUPPORT_FLAGS = $(CFLAGS) -x c -fsyntax-only -Wno-error \ -UWOLFCOSE_NO_SIGN1_SIGN \ -UWOLFCOSE_LEAN_VERIFY \ -UWOLFCOSE_LEAN_VERIFY_MLDSA \ - -UWOLFCOSE_LEAN_MLDSA + -UWOLFCOSE_LEAN_MLDSA \ + -UWOLFCOSE_LEAN_VERIFY_LMS \ + -UWOLFCOSE_LEAN_LMS ECDSA_POLICY_FLAGS = $(ECDSA_POLICY_BASE_FLAGS) \ -DWOLFSSL_ECDSA_DETERMINISTIC_K \ -DWOLFCOSE_ENABLE_DETERMINISTIC_ECDSA @@ -439,6 +445,21 @@ mldsa-verify: @echo "=== Running lean ML-DSA verify-only example ===" ./$(MLDSAV_DEMO) +# --- Stateful hash-based HSS/LMS lean sign + verify (WOLFCOSE_LEAN_LMS) --- +# Requires wolfSSL built with LMS (./configure --enable-lms). +lms-demo: + $(CC) $(CFLAGS) -DWOLFCOSE_LEAN_LMS -o $(LMS_DEMO) \ + $(LMS_DEMO).c $(SRC) $(LDFLAGS) $(LDLIBS) + @echo "=== Running HSS/LMS sign + verify example ===" + ./$(LMS_DEMO) + +# --- Smallest hash-based verify-only (WOLFCOSE_LEAN_VERIFY_LMS) --- +lms-verify: + $(CC) $(CFLAGS) -DWOLFCOSE_LEAN_VERIFY_LMS -o $(LMSV_DEMO) \ + $(LMSV_DEMO).c $(SRC) $(LDFLAGS) $(LDLIBS) + @echo "=== Running lean HSS/LMS verify-only example ===" + ./$(LMSV_DEMO) + # --- Comprehensive algorithm tests (CI) --- comprehensive: $(LIB_A) @mkdir -p examples/comprehensive @@ -609,6 +630,25 @@ c99-check: -fsyntax-only $(SRC) @echo "PASS: all sources conform to ISO C99 (-pedantic-errors)" +# Strict C99 gate for the LMS-gated branches and profiles. Needs an +# LMS-enabled wolfSSL (WOLFSSL_INC), so it runs in the LMS CI job rather than +# the default gate whose wolfSSL may lack LMS. +C99_LMS_CONFIGS = "-DWOLFCOSE_ENABLE_LMS" \ + "-DWOLFCOSE_ENABLE_LMS -DWOLFCOSE_ENABLE_EXT_SIGN" + +c99-check-lms: + @for cfg in $(C99_LMS_CONFIGS); do \ + for f in $(SRC); do \ + echo " C99-LMS $$cfg $$f"; \ + $(CC) $(C99_FLAGS) $$cfg -fsyntax-only $$f || exit 1; \ + done; \ + done + @echo " C99-LMS -DWOLFCOSE_LEAN_LMS $(LMS_DEMO).c" + @$(CC) $(C99_FLAGS) -DWOLFCOSE_LEAN_LMS -fsyntax-only $(LMS_DEMO).c + @echo " C99-LMS -DWOLFCOSE_LEAN_VERIFY_LMS $(LMSV_DEMO).c" + @$(CC) $(C99_FLAGS) -DWOLFCOSE_LEAN_VERIFY_LMS -fsyntax-only $(LMSV_DEMO).c + @echo "PASS: LMS sources conform to ISO C99 (-pedantic-errors)" + # Experimental-feature acknowledgement gate. Proves WOLFCOSE_EXPERIMENTAL guards # draft (pre-RFC) features: enabling one without it is a hard error, enabling # both compiles, and a normal build pulls in zero experimental code. @@ -641,7 +681,8 @@ experimental-check: # --- Cleanup --- clean: rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(MAC_DEMO) \ - $(EXTSIGN_DEMO) $(SIGN1_DEMO) $(COMP_SIGN) $(COMP_ENCRYPT) $(COMP_MAC) $(COMP_ERRORS) \ + $(EXTSIGN_DEMO) $(SIGN1_DEMO) $(MLDSA_DEMO) $(MLDSAV_DEMO) $(LMS_DEMO) $(LMSV_DEMO) \ + $(COMP_SIGN) $(COMP_ENCRYPT) $(COMP_MAC) $(COMP_ERRORS) \ $(SCEN_FIRMWARE) $(SCEN_MULTIPARTY) $(SCEN_IOTFLEET) $(SCEN_SENSOR) $(SCEN_BROADCAST) \ $(INTEROP_DIR)/*.o $(INTEROP_DIR)/*.su $(INTEROP_BIN) \ $(GO_COSE_BIN) $(GO_COSE_ORACLE) \ diff --git a/README.md b/README.md index deb2776..0e292c7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # wolfCOSE -wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.rfc-editor.org/rfc/rfc8949), [COSE (RFC 9052/9053)](https://www.rfc-editor.org/rfc/rfc9052), and post-quantum [ML-DSA for COSE (RFC 9964)](https://www.rfc-editor.org/rfc/rfc9964) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend. +wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.rfc-editor.org/rfc/rfc8949), [COSE (RFC 9052/9053)](https://www.rfc-editor.org/rfc/rfc9052), and post-quantum [ML-DSA for COSE (RFC 9964)](https://www.rfc-editor.org/rfc/rfc9964) and [HSS/LMS for COSE (RFC 8778)](https://www.rfc-editor.org/rfc/rfc8778) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend. ## Main Features - **Complete RFC 9052 message set**: all six COSE message types, including multi-signer `COSE_Sign` and multi-recipient `COSE_Encrypt` / `COSE_Mac` - **Post-quantum signing**: ML-DSA (FIPS 204) at all three security levels, with RFC 9964 `COSE_Key` (AKP key type, seed-based private keys) -- **40 algorithms** across signing, encryption, MAC, and key distribution +- **Stateful hash-based signing**: HSS/LMS (RFC 8778, NIST SP 800-208) with `COSE_Key` type 5 — the CNSA 2.0 algorithm for firmware and software signing, verify-only friendly for constrained devices +- **41 algorithms** across signing, encryption, MAC, and key distribution - **Zero dynamic allocation**: heap-allocation-free and non-recursive. Every operation runs on caller-provided buffers within a bounded, target-customizable stack ceiling (nothing on the heap, zero `.data`/`.bss`) - **Tiny footprint**: ES256 `COSE_Sign1` wolfCOSE (COSE + CBOR engine) **~5.1 KB** verify-only and **~6.8 KB** sign + verify. @@ -21,7 +22,7 @@ wolfCOSE is a lightweight C library implementing [CBOR (RFC 8949)](https://www.r ## Supported Algorithms -**Signing:** `ES256, ES384, ES512, EdDSA (Ed25519/Ed448), PS256/384/512, ML-DSA-44/65/87` +**Signing:** `ES256, ES384, ES512, EdDSA (Ed25519/Ed448), PS256/384/512, ML-DSA-44/65/87, HSS-LMS` **Encryption:** `AES-GCM (128/192/256), ChaCha20-Poly1305, AES-CCM variants` @@ -48,7 +49,8 @@ wolfCOSE has implemented all RFC 9052 messages both single-actor and multi-actor wolfCOSE requires [wolfSSL](https://www.wolfssl.com/) as its crypto backend. **Minimum supported version: v5.8.0-stable**. Some optional algorithms require newer releases; see [Getting Started](docs/Getting-Started.md#prerequisites) for -feature-specific dependency floors and build instructions. +feature-specific dependency floors and build instructions. HSS/LMS (RFC 8778) +requires v5.9.2-stable or later. Choose a build configuration based on the algorithms you need. @@ -107,7 +109,7 @@ cd wolfssl --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa \ + --enable-mldsa --enable-lms \ --enable-hkdf --enable-aeskeywrap make && sudo make install sudo ldconfig @@ -190,7 +192,7 @@ Full documentation is available in the [Wiki](https://github.com/wolfSSL/wolfCOS - [Getting Started](https://github.com/wolfSSL/wolfCOSE/wiki/Getting-Started): Build instructions and first steps - [Message Types](https://github.com/wolfSSL/wolfCOSE/wiki/Message-Types): All six RFC 9052 messages (Sign1, Sign, Encrypt0, Encrypt, Mac0, Mac) with code samples -- [Algorithms](https://github.com/wolfSSL/wolfCOSE/wiki/Algorithms): Complete list of 40 supported algorithms with COSE IDs +- [Algorithms](https://github.com/wolfSSL/wolfCOSE/wiki/Algorithms): Complete list of 41 supported algorithms with COSE IDs - [API Reference](https://github.com/wolfSSL/wolfCOSE/wiki/API-Reference): Function signatures, data structures, error codes - [Macros](https://github.com/wolfSSL/wolfCOSE/wiki/Macros): Compile-time configuration, size tuning, and ECDSA nonce policy - [Footprint](https://github.com/wolfSSL/wolfCOSE/wiki/Footprint): Size and speed numbers, desktop and on-device diff --git a/docs/API-Reference.md b/docs/API-Reference.md index 8bf4ed5..bbddb15 100644 --- a/docs/API-Reference.md +++ b/docs/API-Reference.md @@ -282,6 +282,33 @@ Associate an RSA key with a COSE key structure. --- +### wc_CoseKey_SetLms + +```c +int wc_CoseKey_SetLms(WOLFCOSE_KEY* key, LmsKey* lmsKey); +``` + +Attach an HSS/LMS stateful hash-based key (RFC 8778) to a COSE key structure. +The caller owns the `LmsKey`: for signing it configures the parameter set and +the wolfCrypt private-key read/write callbacks, then calls `wc_LmsKey_MakeKey()` +or `wc_LmsKey_Reload()` before attaching. Each successful signing call consumes +one one-time signature and persists the advanced state through those callbacks. +If a signing call fails, wolfCOSE marks the `LmsKey` bad and it cannot sign +again through any attachment until the caller reloads it with +`wc_LmsKey_Reload()`. + +**Parameters:** +| Name | Description | +|------|-------------| +| `key` | Pointer to initialized COSE key | +| `lmsKey` | Pointer to initialized wolfCrypt `LmsKey` (caller-owned) | + +**Returns:** `WOLFCOSE_SUCCESS` or error code + +**Requires:** `WOLFSSL_HAVE_LMS` (wolfSSL 5.9.2 or later) + +--- + ### wc_CoseKey_SetSymmetric ```c diff --git a/docs/Algorithms.md b/docs/Algorithms.md index bc24d96..55ca30d 100644 --- a/docs/Algorithms.md +++ b/docs/Algorithms.md @@ -1,6 +1,6 @@ # Supported Algorithms -wolfCOSE supports 40 algorithms across signing, encryption, MAC, and key distribution. This page provides the complete list with COSE algorithm IDs and required wolfSSL compile-time guards. All algorithms are usable in both single-actor messages (Sign1/Encrypt0/Mac0) and multi-actor messages (Sign/Encrypt/Mac) — see [[Message Types]] for details. +wolfCOSE supports 41 algorithms across signing, encryption, MAC, and key distribution. This page provides the complete list with COSE algorithm IDs and required wolfSSL compile-time guards. All algorithms are usable in both single-actor messages (Sign1/Encrypt0/Mac0) and multi-actor messages (Sign/Encrypt/Mac) — see [[Message Types]] for details. ## COSE_Sign1 (Digital Signatures) @@ -17,6 +17,7 @@ wolfCOSE supports 40 algorithms across signing, encryption, MAC, and key distrib | ML-DSA-44 | -48 | `WOLFSSL_HAVE_MLDSA` | Post-quantum (FIPS 204) | | ML-DSA-65 | -49 | `WOLFSSL_HAVE_MLDSA` | Post-quantum (FIPS 204) | | ML-DSA-87 | -50 | `WOLFSSL_HAVE_MLDSA` | Post-quantum (FIPS 204) | +| HSS-LMS | -46 | `WOLFSSL_HAVE_LMS` | Post-quantum stateful hash-based (RFC 8778, SP 800-208); COSE_Key kty 5; signature size follows the key's parameter set; signing state is caller-managed, verify-only builds pair with `WOLFSSL_LMS_VERIFY_ONLY` | ### ML-DSA Signature Sizes @@ -91,6 +92,7 @@ AES Key Wrap-based algorithms also require wolfSSL 5.9.0 or later. | RSA | 3 | `WC_RSA_PSS` | PS256, PS384, PS512 | | Symmetric | 4 | always | AES-GCM, AES-CCM, ChaCha20, HMAC | | AKP | 7 | `WOLFSSL_HAVE_MLDSA` | ML-DSA (RFC 9964) | +| HSS-LMS | 5 | `WOLFSSL_HAVE_LMS` | HSS/LMS (RFC 8778) | ML-DSA keys use the RFC 9964 **AKP** (Algorithm Key Pair) key type: the `alg` parameter is REQUIRED, the public key is in `pub` (-1), and the private key is @@ -133,6 +135,7 @@ wolfCOSE defines these constants in `wolfcose.h`: #define WOLFCOSE_ALG_ML_DSA_44 (-48) #define WOLFCOSE_ALG_ML_DSA_65 (-49) #define WOLFCOSE_ALG_ML_DSA_87 (-50) +#define WOLFCOSE_ALG_HSS_LMS (-46) /* Encryption algorithms */ #define WOLFCOSE_ALG_A128GCM (1) @@ -181,8 +184,8 @@ Future algorithm support planned: | Algorithm | Standard | Description | |-----------|----------|-------------| -| ML-KEM | FIPS 203 (Kyber) | Post-quantum key encapsulation for COSE_Encrypt | -| XMSS / LMS | NIST SP 800-208 | Hash-based stateful signatures | +| ML-KEM | FIPS 203 (Kyber) | Post-quantum key encapsulation for COSE_Encrypt (IETF draft, no codepoints yet) | +| XMSS | NIST SP 800-208 | Hash-based stateful signatures (no COSE codepoints assigned yet) | | SLH-DSA | SPHINCS+ | Stateless hash-based signatures | ## See Also diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 8c5d11e..dfcdeed 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -9,7 +9,9 @@ This guide covers prerequisites, building wolfCOSE, and basic usage examples. wolfCOSE requires wolfSSL 5.8.0 or later with the appropriate algorithms enabled. AES Key Wrap requires wolfSSL 5.9.0 or later because that release uses a constant-time integrity comparison during unwrap. ML-DSA requires a -wolfSSL release newer than 5.9.1. +wolfSSL release newer than 5.9.1. HSS/LMS (RFC 8778) requires wolfSSL 5.9.2 or +later, the first release whose public-key importer derives the parameter set +from the key bytes. Here is a full-featured build using a release that meets those feature floors: @@ -20,7 +22,7 @@ cd wolfssl --enable-curve25519 --enable-aesgcm --enable-aesccm \ --enable-sha384 --enable-sha512 --enable-keygen \ --enable-rsapss --enable-chacha --enable-poly1305 \ - --enable-mldsa --enable-hkdf --enable-aeskeywrap + --enable-mldsa --enable-lms --enable-hkdf --enable-aeskeywrap make && sudo make install sudo ldconfig ``` @@ -60,6 +62,7 @@ You can enable only the algorithms you need: | AES Key Wrap | `--enable-aeskeywrap` (wolfSSL 5.9.0+) | | RSA-PSS signing | `--enable-rsapss --enable-keygen` | | ML-DSA (post-quantum) | `--enable-mldsa` | +| HSS/LMS (stateful hash-based) | `--enable-lms` (wolfSSL 5.9.2+) | | AES-MAC | `--enable-aescbc` | ## Building wolfCOSE diff --git a/docs/Home.md b/docs/Home.md index 4a58bc8..b61bb55 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -19,7 +19,7 @@ It uses [wolfSSL](https://www.wolfssl.com/) as the cryptographic backend and is | Post-quantum signing | ML-DSA (FIPS 204) at all three security levels — first COSE library to ship native PQC | | Zero dynamic allocation | Heap-allocation-free, non-recursive; caller-provided buffers within a bounded, target-customizable stack ceiling — zero `.data`/`.bss` | | Tiny footprint | 3.5–5.1 KB COSE engine, 5.1–6.8 KB with the built-in CBOR engine (verify-only → sign + verify); 26.2 KB → 34.6 KB total with wolfCrypt — ES256 `COSE_Sign1`, dead-code-eliminated | -| 40 algorithms | Signing, encryption, MAC, and key distribution — classical and post-quantum | +| 41 algorithms | Signing, encryption, MAC, and key distribution — classical and post-quantum | | FIPS 140-3 path | Sole crypto dependency is wolfCrypt FIPS Certificate #4718 | | CNSA 2.0 ready | ML-DSA-44/65/87 for quantum-resistant signatures | | MISRA-C:2023 | compliance striving, Single-exit pattern, no recursion, deviation-logged | diff --git a/docs/Macros.md b/docs/Macros.md index 8d1d98d..7803b6f 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -47,7 +47,7 @@ Defining `WOLFCOSE_LEAN` keeps only the core — `COSE_Sign1`/`Encrypt0`/`Mac0` | `WOLFCOSE_LEAN` | Core-only base; all extensions become opt-in | | `WOLFCOSE_ENABLE_` | Opt in a single extension (see list below) | -Extension names for `WOLFCOSE_ENABLE_`: `ES384`, `ES512`, `EDDSA`, `ED448`, `RSAPSS`, `MLDSA`, `HMAC384`, `HMAC512`, `AESCCM`, `CHACHA20`, `AESMAC`, `AESWRAP`, `ECDH_ES`, `SIGN` (multi-signer), `ENCRYPT` (multi-recipient), `MAC` (multi-recipient). +Extension names for `WOLFCOSE_ENABLE_`: `ES384`, `ES512`, `EDDSA`, `ED448`, `RSAPSS`, `MLDSA`, `LMS`, `HMAC384`, `HMAC512`, `AESCCM`, `CHACHA20`, `AESMAC`, `AESWRAP`, `ECDH_ES`, `SIGN` (multi-signer), `ENCRYPT` (multi-recipient), `MAC` (multi-recipient). An extension is compiled in when it is explicitly enabled (`WOLFCOSE_ENABLE_`), or — in a non-lean build — when wolfSSL provides the primitive and it is not opted out with `WOLFCOSE_NO_`. Enabling an extension wolfSSL cannot provide is a compile error. The resolved state is exposed internally as read-only `WOLFCOSE_HAVE_` gates (e.g. `WOLFCOSE_HAVE_MLDSA`); sources, tests, and examples compile against those, so you set `WOLFCOSE_ENABLE_*`/`WOLFCOSE_NO_*`, not `WOLFCOSE_HAVE_*`. @@ -64,6 +64,7 @@ Per-algorithm opt-outs for the default (non-lean) build. Each also has a `WOLFCO | `WOLFCOSE_NO_ED448` | Ed448 | `HAVE_ED448` | | `WOLFCOSE_NO_RSAPSS` | RSA-PSS (PS256/384/512) | `WC_RSA_PSS` | | `WOLFCOSE_NO_MLDSA` | ML-DSA (FIPS 204) | `WOLFSSL_HAVE_MLDSA` | +| `WOLFCOSE_NO_LMS` | HSS/LMS (RFC 8778) | `WOLFSSL_HAVE_LMS` (wolfSSL 5.9.2+) | | `WOLFCOSE_NO_AESGCM` | AES-GCM | `HAVE_AESGCM` | | `WOLFCOSE_NO_AESCCM` | AES-CCM | `HAVE_AESCCM` | | `WOLFCOSE_NO_CHACHA20` | ChaCha20-Poly1305 | `HAVE_CHACHA` + `HAVE_POLY1305` | @@ -229,11 +230,11 @@ What the callback receives depends on the algorithm, and getting this wrong prod |---|---| | ES256/384/512 | the **digest** of the `Sig_structure` — sign with a sign-hash primitive (`psa_sign_hash`, `CKM_ECDSA`) and return fixed-width `r \|\| s` (RFC 9053 sec. 2.1), **not** a DER `SEQUENCE` | | PS256/384/512 | the **digest** — sign with RSASSA-PSS, MGF1 over the same SHA-2 as the algorithm, salt length equal to the digest length (RFC 8230 sec. 2) | -| EdDSA, Ed448, ML-DSA | the **`Sig_structure` itself** — sign it with a sign-message primitive | +| EdDSA, Ed448, ML-DSA, HSS-LMS | the **`Sig_structure` itself** — sign it with a sign-message primitive | -It returns the raw COSE signature; wolfCOSE checks the returned length against the algorithm but performs no key operation itself. No RNG is needed. +It returns the raw COSE signature; wolfCOSE checks the returned length against the algorithm but performs no key operation itself. No RNG is needed. HSS-LMS is stateful: the callback's own signing operation advances one-time-signature state, and the caller owns its persistence. -A delegated key needs no local *private* key, but it must declare enough for wolfCOSE to know the expected signature length: ES* and ML-DSA need nothing beyond `alg`; EdDSA needs `kty`/`crv`; PS* needs `kty` plus a local `RsaKey` attached via `wc_CoseKey_SetRsa()` for its modulus size. +A delegated key needs no local *private* key, but it must declare enough for wolfCOSE to know the expected signature length: ES* and ML-DSA need nothing beyond `alg`; EdDSA needs `kty`/`crv`; PS* needs `kty` plus a local `RsaKey` attached via `wc_CoseKey_SetRsa()` for its modulus size; HSS-LMS needs an initialized `LmsKey` attached via `wc_CoseKey_SetLms()`, because its length follows the key's parameter set. Pass a NULL callback to detach. Attaching local key material with `wc_CoseKey_SetEcc()` and friends detaches implicitly, so always call `wc_CoseKey_SetExtSigner()` last. `wc_CoseKey_Decode()` is rejected on a key that has a signer attached, rather than silently importing private material and signing locally with it. @@ -241,10 +242,10 @@ For a key that has no local wolfCrypt object at all, set `kty` (and `crv` for Ed Two limits worth knowing before designing around this: -- It does not remove the local algorithm. `WOLFCOSE_ENABLE_EXT_SIGN` still requires a signing operation, which requires at least one signature algorithm compiled in, so a build with no local signature primitive is rejected at compile time and `WOLFCOSE_LEAN_VERIFY`/`WOLFCOSE_LEAN_VERIFY_MLDSA` cannot be combined with it. Delegating ML-DSA likewise needs local ML-DSA compiled in, which raises the `WOLFCOSE_MAX_SCRATCH_SZ` default to 8192 bytes and enforces a 4096-byte minimum. +- It does not remove the local algorithm. `WOLFCOSE_ENABLE_EXT_SIGN` still requires an enabled wolfCOSE signing operation plus enough algorithm metadata to size the signature, so the verify-only profiles (`WOLFCOSE_LEAN_VERIFY`, `WOLFCOSE_LEAN_VERIFY_MLDSA`, `WOLFCOSE_LEAN_VERIFY_LMS`) cannot be combined with it. The wolfCrypt backend itself may lack the signing primitive: delegated HSS-LMS works against a `WOLFSSL_LMS_VERIFY_ONLY` wolfSSL, because the attached public `LmsKey` supplies the length and the callback signs. Delegating ML-DSA likewise needs local ML-DSA compiled in, which raises the `WOLFCOSE_MAX_SCRATCH_SZ` default to 8192 bytes and enforces a 4096-byte minimum. - Scratch must hold the `Sig_structure`, which embeds the payload, and delegated signing needs at least as much again for the signature: - **ES\*, PS\*** pre-hash, so the signature reuses the `Sig_structure` space: `scratchSz >= max(Sig_structure, signature)`. Local ECDSA signs into a stack buffer and needs only the `Sig_structure`, so a small-payload ES256 case can need more scratch delegated than local. - - **EdDSA, Ed448, ML-DSA** sign the structure in place, so the signature goes after it: `scratchSz >= Sig_structure + signature`. Delegated Ed25519 needs 64 bytes more than the local path, Ed448 114. + - **EdDSA, Ed448, ML-DSA, HSS-LMS** sign the structure in place, so the signature goes after it: `scratchSz >= Sig_structure + signature`. Delegated Ed25519 needs 64 bytes more than the local path, Ed448 114; HSS-LMS needs its parameter-set signature length (up to 10204 bytes for the W4 predefined sets). --- @@ -288,8 +289,9 @@ One define that trims the caller working set to the minimum that still fits the | ES256/384/512, EdDSA (Ed25519/Ed448) | 132 | 512 | | RSA-PSS (PS256/384/512) | 512 | 512 | | ML-DSA-44/65/87 | 4627 | 8192 | +| HSS-LMS | 10240 | 11264 | -Because the floor follows the algorithm, `WOLFCOSE_MIN_BUFFERS` stays valid with any algorithm — ML-DSA and RSA-PSS simply use that algorithm's floor rather than the ECC floor (ML-DSA-87's 4627-byte signature is the largest wolfCOSE supports). It stays zero-heap and shrinks buffers, not stack frames. An explicit `-D` override of any individual limit takes precedence. +Because the floor follows the algorithm, `WOLFCOSE_MIN_BUFFERS` stays valid with any algorithm — ML-DSA, HSS-LMS, and RSA-PSS simply use that algorithm's floor rather than the ECC floor. HSS-LMS uses the largest default floors because its signature size follows the key's parameter set rather than a fixed constant: the `WOLFCOSE_MAX_SIG_SZ` default of 10240 and `WOLFCOSE_MAX_SCRATCH_SZ` default of 11264 (scratch also holds the `Sig_structure`) cover every W4 and W8 predefined set, the largest being L4_H10_W4 at 10204 bytes. The low-Winternitz (W1/W2) multi-level sets reach 18012 bytes and need a `-D` override; a key whose signature exceeds these buffers is rejected via `wc_LmsKey_GetSigLen()` before any signing, so no one-time state is consumed. It stays zero-heap and shrinks buffers, not stack frames. An explicit `-D` override of any individual limit takes precedence. --- @@ -309,7 +311,7 @@ Four levers, smallest impact last. See the [[Footprint]] page for the resulting #define WOLFCOSE_CBOR_MAX_DEPTH 4 /* default 8 */ ``` -**Post-quantum sizing.** ML-DSA is the largest signature wolfCOSE supports; the floors auto-scale (ML-DSA-87: `WOLFCOSE_MAX_SIG_SZ` 4627, `WOLFCOSE_MAX_SCRATCH_SZ` 8192). `WOLFCOSE_LEAN_VERIFY_MLDSA` is the smallest secure PQ build at 20.8 KB total, smaller than classical ES256 verify-only. Always build the application with `-ffunction-sections -fdata-sections -Wl,--gc-sections` so only the COSE functions you call are linked. +**Post-quantum sizing.** The post-quantum signatures drive the largest floors; they auto-scale (ML-DSA-87: `WOLFCOSE_MAX_SIG_SZ` 4627, `WOLFCOSE_MAX_SCRATCH_SZ` 8192; HSS-LMS: `WOLFCOSE_MAX_SIG_SZ` 10240, `WOLFCOSE_MAX_SCRATCH_SZ` 11264, since its size follows the key's parameter set). `WOLFCOSE_LEAN_VERIFY_MLDSA` is the smallest secure PQ build at 20.8 KB total, smaller than classical ES256 verify-only. Always build the application with `-ffunction-sections -fdata-sections -Wl,--gc-sections` so only the COSE functions you call are linked. ## Tuning for Speed @@ -367,6 +369,22 @@ make mldsa-verify # builds + runs examples/sign1_verify_mldsa.c with the prof cc -DWOLFCOSE_LEAN_VERIFY_MLDSA ... src/*.c ``` +### `WOLFCOSE_LEAN_LMS` — lean stateful hash-based sign + verify + +A lean HSS/LMS-only (RFC 8778, SP 800-208) COSE_Sign1 **sign and verify** profile with the same implied gate set as `WOLFCOSE_LEAN_MLDSA` (`WOLFCOSE_LEAN`, `WOLFCOSE_ENABLE_LMS`, `WOLFCOSE_NO_ES256`, no Encrypt0/Mac0/key codec). LMS signing is stateful: signing advances one-time-signature state inside wolfCrypt, and the caller installs the wolfCrypt private-key read/write callbacks that persist it. wolfCOSE stores no state itself and defines no persistence policy. Pair with a wolfCrypt backend built with LMS (`--enable-lms`). + +```bash +make lms-demo # builds + runs examples/sign1_lms.c with the profile +``` + +### `WOLFCOSE_LEAN_VERIFY_LMS` — minimal hash-based verify-only + +HSS/LMS COSE_Sign1 **verify only**: implies `WOLFCOSE_LEAN_LMS` plus `WOLFCOSE_NO_SIGN1_SIGN`. Verification is hash operations only (no big-integer or lattice math), so this pairs naturally with a wolfCrypt LMS verify-only build (`WOLFSSL_LMS_VERIFY_ONLY`), the CNSA 2.0 firmware-verification shape. The signature length follows the key's levels/height/Winternitz parameter set, so the `WOLFCOSE_MAX_SIG_SZ` default rises to 10240 and `WOLFCOSE_MAX_SCRATCH_SZ` to 11264, covering every W4/W8 predefined set; the low-Winternitz (W1/W2) multi-level sets need a `-D` override. + +```bash +make lms-verify # builds + runs examples/sign1_verify_lms.c with the profile +``` + ## Example Build Configurations ### Sign-Only Build (Minimal) diff --git a/examples/sign1_lms.c b/examples/sign1_lms.c new file mode 100644 index 0000000..74e4937 --- /dev/null +++ b/examples/sign1_lms.c @@ -0,0 +1,158 @@ +/* sign1_lms.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCOSE. + * + * wolfCOSE 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. + * + * wolfCOSE 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, see . + */ + +/* Post-quantum COSE_Sign1 sign + verify with HSS/LMS (RFC 8778). + * + * Full round trip: generate an LMS key (SP 800-208, L1/H10/W8), sign a + * payload into a COSE_Sign1 with alg HSS-LMS (-46), then verify it. LMS is + * a stateful scheme: the private key state is persisted through the + * wolfCrypt read/write callbacks, memory-backed here for the demo. + * Requires wolfSSL built with LMS (./configure --enable-lms). For the + * verify-only on-device case see sign1_verify_lms.c and the + * WOLFCOSE_LEAN_VERIFY_LMS build profile. + */ + +#include +#include +#include +#include +#include + +static const char PAYLOAD[] = "wolfCOSE HSS-LMS payload"; + +/* This example persists the compact LMS private key. WOLFSSL_WC_LMS_SERIALIZE_STATE + * instead hands the callback the full serialized working state, which is larger + * than HSS_MAX_PRIVATE_KEY_LEN and needs a bigger backing store. */ +#ifdef WOLFSSL_WC_LMS_SERIALIZE_STATE + #error "sign1_lms.c assumes compact LMS persistence; WOLFSSL_WC_LMS_SERIALIZE_STATE needs a larger gPrivStore" +#endif + +/* LMS signatures run ~1.5KB for L1/H10/W8; keep buffers off the stack. */ +static unsigned char gScratch[8192]; +static unsigned char gMsg[4096]; +static unsigned char gPrivStore[HSS_MAX_PRIVATE_KEY_LEN]; + +static int lms_write_cb(const byte* priv, word32 privSz, void* context) +{ + int ret = (int)WC_LMS_RC_WRITE_FAIL; + + if ((priv != NULL) && (context != NULL) && + (privSz <= (word32)sizeof(gPrivStore))) { + (void)memcpy(context, priv, (size_t)privSz); + ret = (int)WC_LMS_RC_SAVED_TO_NV_MEMORY; + } + return ret; +} + +static int lms_read_cb(byte* priv, word32 privSz, void* context) +{ + int ret = (int)WC_LMS_RC_READ_FAIL; + + if ((priv != NULL) && (context != NULL) && + (privSz <= (word32)sizeof(gPrivStore))) { + (void)memcpy(priv, context, (size_t)privSz); + ret = (int)WC_LMS_RC_READ_TO_MEMORY; + } + return ret; +} + +int main(void) +{ + LmsKey lmsKey; + WOLFCOSE_KEY key; + WOLFCOSE_HDR hdr; + WC_RNG rng; + const uint8_t* payload = NULL; + size_t payloadLen = 0; + size_t msgLen = 0; + int ret; + int rc = 1; + + if (wc_InitRng(&rng) != 0) { + (void)printf("wc_InitRng failed\n"); + return 1; + } + + ret = wc_LmsKey_Init(&lmsKey, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_LmsKey_SetLmsParm(&lmsKey, WC_LMS_PARM_L1_H10_W8); + } + if (ret == 0) { + ret = wc_LmsKey_SetWriteCb(&lmsKey, lms_write_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetReadCb(&lmsKey, lms_read_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetContext(&lmsKey, gPrivStore); + } + if (ret == 0) { + ret = wc_LmsKey_MakeKey(&lmsKey, &rng); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&key); + } + if (ret == 0) { + ret = wc_CoseKey_SetLms(&key, &lmsKey); + } + + /* Sign */ + if (ret == 0) { + ret = wc_CoseSign1_Sign(&key, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + (const uint8_t*)PAYLOAD, sizeof(PAYLOAD) - 1, + NULL, 0, NULL, 0, + gScratch, sizeof(gScratch), + gMsg, sizeof(gMsg), &msgLen, &rng); + } + if (ret == WOLFCOSE_SUCCESS) { + (void)printf("HSS-LMS sign: COSE_Sign1 produced, %u bytes " + "(signatures remaining: %s)\n", (unsigned int)msgLen, + (wc_LmsKey_SigsLeft(&lmsKey) != 0) ? "yes" : "exhausted"); + } + else { + (void)printf("HSS-LMS sign failed (%d)\n", ret); + } + + /* Verify the message we just signed */ + if (ret == WOLFCOSE_SUCCESS) { + (void)memset(&hdr, 0, sizeof(hdr)); + ret = wc_CoseSign1_Verify(&key, gMsg, msgLen, NULL, 0, NULL, 0, + gScratch, sizeof(gScratch), &hdr, + &payload, &payloadLen); + } + if ((ret == WOLFCOSE_SUCCESS) && + (payloadLen == (sizeof(PAYLOAD) - 1)) && (payload != NULL) && + (memcmp(payload, PAYLOAD, payloadLen) == 0)) { + (void)printf("HSS-LMS verify: OK, payload = \"%.*s\"\n", + (int)payloadLen, payload); + rc = 0; + } + else if (ret == WOLFCOSE_SUCCESS) { + (void)printf("HSS-LMS verify: payload mismatch\n"); + } + else { + (void)printf("HSS-LMS verify failed (%d)\n", ret); + } + + wc_CoseKey_Free(&key); + wc_LmsKey_Free(&lmsKey); + wc_FreeRng(&rng); + return rc; +} diff --git a/examples/sign1_verify_lms.c b/examples/sign1_verify_lms.c new file mode 100644 index 0000000..2b0d38b --- /dev/null +++ b/examples/sign1_verify_lms.c @@ -0,0 +1,186 @@ +/* sign1_verify_lms.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCOSE. + * + * wolfCOSE 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. + * + * wolfCOSE 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, see . + */ + +/* Post-quantum verify-only lean example (HSS/LMS, RFC 8778). + * + * Build with -DWOLFCOSE_LEAN_VERIFY_LMS: only the COSE_Sign1 verify path, no + * signing or RNG. The device holds only the 60-byte LMS public key; the key + * and a COSE_Sign1 produced off-device (L1/H10/W8) are embedded below as + * fixed test vectors. Verification is hash-only, so this profile pairs with + * a wolfSSL built with LMS verify support (WOLFSSL_LMS_VERIFY_ONLY). + */ + +#include +#include +#include +#include + +static const unsigned char LMS_PUB[60]={0,0,0,1,0,0,0,6,0,0,0,4,51,24,151,252,118,197,226,25,138,142,162,161,175,57,162,239,77,9,161,39,112,180,69,15,126,244,250,77,65,229,236,173,41,242,196,201,23,62,74,20,206,189,52,91,252,164,114,91,}; +static const unsigned char LMS_COSE[1503] = { + 210,132,68,161,1,56,45,160,88,34,119,111,108,102,67,79, + 83,69,32,72,83,83,45,76,77,83,32,98,101,110,99,104, + 109,97,114,107,32,112,97,121,108,111,97,100,89,5,176,0, + 0,0,0,0,0,0,0,0,0,0,4,218,134,33,16,225, + 56,196,162,45,62,3,231,172,45,36,36,88,92,111,193,36, + 180,122,63,218,122,208,186,126,174,136,102,200,255,17,123,13, + 158,193,164,249,108,168,116,187,210,197,28,178,179,213,36,130, + 50,59,239,251,55,3,159,228,199,3,134,134,200,252,156,5, + 98,101,166,51,88,243,88,232,50,239,54,131,232,195,181,231, + 45,132,94,177,205,162,170,111,228,109,186,71,170,70,141,22, + 33,110,78,29,240,72,111,125,2,33,102,169,225,25,204,234, + 145,245,167,109,192,158,253,210,250,30,144,224,125,119,25,9, + 32,76,172,58,171,19,229,170,250,131,197,194,60,165,187,124, + 212,203,85,144,160,30,36,187,17,239,215,124,126,212,53,253, + 151,14,49,85,190,107,39,69,194,224,216,164,33,139,119,20, + 144,195,212,131,20,120,45,30,139,225,204,99,58,226,217,17, + 90,63,6,159,230,252,18,2,118,254,165,190,222,146,174,228, + 5,4,182,241,57,33,183,26,58,219,188,153,168,59,85,110, + 57,13,239,72,106,13,134,12,162,81,169,134,213,129,234,143, + 242,178,249,35,111,66,156,124,254,59,16,197,244,191,113,14, + 178,209,28,204,203,146,171,49,49,192,218,17,78,30,249,236, + 184,254,241,105,202,111,42,177,250,63,75,221,230,28,202,141, + 1,20,157,51,150,62,132,34,189,12,193,229,234,162,167,174, + 91,54,235,30,140,65,216,70,130,185,136,99,56,95,114,166, + 231,228,234,13,181,145,14,118,24,89,6,103,193,201,9,10, + 25,254,36,49,118,49,87,40,217,232,4,46,223,29,54,224, + 69,206,253,38,122,167,63,13,175,184,165,172,145,19,178,24, + 112,144,180,140,75,131,13,190,145,81,181,108,130,198,89,239, + 140,238,203,36,144,136,98,130,243,46,62,135,230,116,221,21, + 221,105,127,25,155,163,72,68,64,127,18,119,27,52,55,110, + 195,80,220,224,134,213,129,158,36,97,95,1,188,191,148,109, + 115,9,29,52,235,100,135,29,189,245,104,134,10,93,42,4, + 11,17,19,94,151,88,211,252,174,64,234,200,121,173,7,103, + 117,230,8,160,14,189,63,203,184,67,226,226,45,190,161,153, + 101,48,221,175,247,122,58,189,86,98,139,19,187,176,103,208, + 94,67,244,42,233,168,189,170,200,229,150,252,98,50,101,200, + 108,137,107,86,2,112,226,235,18,171,161,136,113,197,108,222, + 42,58,17,77,44,59,219,64,55,82,22,74,155,254,13,144, + 150,33,79,44,62,97,143,167,59,245,21,204,233,144,84,36, + 176,36,145,254,19,24,111,157,134,34,26,14,131,112,23,180, + 179,224,193,140,26,105,155,250,244,96,113,194,66,25,201,181, + 29,240,252,210,228,195,13,243,128,53,48,244,89,0,111,253, + 9,142,122,150,177,116,164,133,204,221,128,251,21,227,31,180, + 156,6,55,13,169,248,241,102,209,25,234,103,22,55,63,62, + 75,110,134,4,100,16,240,13,118,126,193,231,118,243,203,108, + 219,151,227,56,160,238,178,237,111,252,137,248,163,181,82,10, + 146,48,24,167,255,185,243,120,182,220,95,170,105,87,42,187, + 37,148,92,203,220,164,52,200,8,251,12,126,21,252,59,19, + 62,106,19,34,240,191,44,120,0,111,200,81,70,0,167,156, + 150,220,0,253,239,246,118,162,153,249,248,116,144,208,31,188, + 199,168,190,32,211,133,246,54,110,14,21,97,161,19,89,233, + 46,238,29,39,60,132,45,73,124,198,149,36,110,203,39,191, + 90,99,185,70,190,135,116,54,147,7,81,31,124,158,188,221, + 217,45,70,74,49,89,104,139,128,32,225,131,112,205,60,124, + 102,208,183,136,21,121,58,227,180,44,120,98,231,105,47,89, + 216,28,140,183,98,242,187,102,199,83,158,154,121,28,213,25, + 86,157,35,110,148,173,163,245,90,117,156,119,2,68,76,123, + 186,191,226,32,117,148,153,235,7,24,132,94,45,57,142,60, + 242,97,253,1,42,165,178,148,67,200,198,174,220,157,239,245, + 93,173,213,31,86,231,161,156,237,226,87,59,104,179,190,85, + 207,248,46,33,155,240,72,131,65,29,96,226,144,61,39,191, + 128,179,76,95,6,153,209,100,252,33,183,204,36,169,238,163, + 72,158,112,147,53,114,239,97,120,70,133,61,136,132,151,45, + 134,139,56,206,230,16,142,93,242,228,18,167,60,209,231,114, + 183,65,122,181,212,93,209,177,121,243,252,180,102,82,90,71, + 97,246,243,220,21,202,194,162,130,229,86,7,251,166,5,195, + 100,58,22,9,182,46,167,255,158,215,157,7,210,51,111,115, + 97,120,230,100,182,31,179,219,73,129,183,98,163,176,161,132, + 149,2,128,165,108,195,213,0,7,208,20,180,12,162,42,145, + 236,21,93,128,252,227,177,34,26,223,21,14,14,23,168,190, + 183,190,233,173,189,51,25,76,194,132,61,100,236,61,177,212, + 220,203,175,59,219,23,223,162,76,224,245,120,58,84,80,70, + 11,113,96,239,211,112,234,51,125,127,193,254,246,205,200,146, + 201,254,113,5,208,182,85,216,124,186,107,0,0,0,6,73, + 98,25,152,87,127,117,219,37,170,75,250,81,9,129,87,61, + 41,228,176,87,226,208,70,56,188,57,214,24,156,59,33,223, + 80,86,86,243,15,148,3,218,3,229,150,129,87,59,140,210, + 57,77,103,106,193,37,58,4,32,44,29,136,86,52,129,103, + 46,161,229,9,144,169,70,201,97,100,242,76,179,196,168,94, + 83,208,172,35,146,205,38,171,16,157,99,139,95,142,171,224, + 157,144,64,220,44,141,144,81,171,1,210,99,181,157,88,205, + 145,211,76,200,76,133,141,236,18,249,23,146,62,219,24,143, + 47,37,60,67,134,114,60,90,25,55,185,8,149,233,180,179, + 178,111,18,53,219,120,194,199,114,28,32,179,91,130,6,163, + 160,236,106,109,54,159,13,235,9,207,34,167,166,10,255,45, + 154,81,248,131,159,160,111,88,190,57,146,81,199,88,76,194, + 112,42,154,69,57,153,177,231,251,98,15,6,150,89,173,15, + 139,112,36,1,179,162,223,90,79,219,206,80,247,228,14,169, + 28,243,148,209,79,98,88,137,173,148,252,41,50,180,156,200, + 215,66,219,100,196,148,148,139,58,87,104,6,133,110,181,111, + 68,10,111,67,192,160,65,63,27,173,143,130,114,224,38,255, + 82,94,177,57,198,123,30,125,15,51,163,189,86,28,55,0, + 166,252,187,96,33,29,84,215,188,120,17,12,160,54,139,136, + 227,176,36,48,142,208,225,48,52,70,57,23,94,157,94 +}; + +static const char EXPECTED_PAYLOAD[] = "wolfCOSE HSS-LMS benchmark payload"; + +int main(void) +{ + LmsKey lmsKey; + WOLFCOSE_KEY key; + WOLFCOSE_HDR hdr; + unsigned char scratch[4096]; + const uint8_t* payload = NULL; + size_t payloadLen = 0; + int ret; + int rc = 1; + + if (wc_LmsKey_Init(&lmsKey, NULL, INVALID_DEVID) != 0) { + (void)printf("wc_LmsKey_Init failed\n"); + return 1; + } + + /* Import the public key only; the RFC 8554 type codes inside it select + * the LMS parameter set. */ + ret = wc_LmsKey_ImportPubRaw(&lmsKey, LMS_PUB, (word32)sizeof(LMS_PUB)); + if (ret == 0) { + ret = wc_CoseKey_Init(&key); + } + if (ret == 0) { + ret = wc_CoseKey_SetLms(&key, &lmsKey); + } + if (ret == 0) { + (void)memset(&hdr, 0, sizeof(hdr)); + ret = wc_CoseSign1_Verify(&key, LMS_COSE, sizeof(LMS_COSE), + NULL, 0, NULL, 0, + scratch, sizeof(scratch), &hdr, + &payload, &payloadLen); + } + + if ((ret == WOLFCOSE_SUCCESS) && + (payloadLen == (sizeof(EXPECTED_PAYLOAD) - 1)) && + (payload != NULL) && + (memcmp(payload, EXPECTED_PAYLOAD, payloadLen) == 0)) { + (void)printf("lean HSS-LMS verify-only: COSE_Sign1 verified, " + "payload = \"%.*s\"\n", (int)payloadLen, payload); + rc = 0; + } + else if (ret == WOLFCOSE_SUCCESS) { + (void)printf("lean HSS-LMS verify-only: payload mismatch\n"); + } + else { + (void)printf("lean HSS-LMS verify-only: verify failed (%d)\n", ret); + } + + wc_CoseKey_Free(&key); + wc_LmsKey_Free(&lmsKey); + return rc; +} diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index 8886e88..9a07818 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -80,6 +80,48 @@ extern "C" { #endif #endif /* WOLFCOSE_LEAN_VERIFY_MLDSA */ +/* Verify-only COSE_Sign1 with HSS/LMS (implies WOLFCOSE_LEAN_LMS, no signing). */ +#ifdef WOLFCOSE_LEAN_VERIFY_LMS + #ifndef WOLFCOSE_LEAN_LMS + #define WOLFCOSE_LEAN_LMS + #endif + #ifndef WOLFCOSE_NO_SIGN1_SIGN + #define WOLFCOSE_NO_SIGN1_SIGN + #endif +#endif /* WOLFCOSE_LEAN_VERIFY_LMS */ + +/* Lean HSS/LMS-only COSE_Sign1 sign+verify (no ES256, Sign1 only). */ +#ifdef WOLFCOSE_LEAN_LMS + #ifndef WOLFCOSE_LEAN + #define WOLFCOSE_LEAN + #endif + #ifndef WOLFCOSE_ENABLE_LMS + #define WOLFCOSE_ENABLE_LMS + #endif + #ifndef WOLFCOSE_NO_ES256 + #define WOLFCOSE_NO_ES256 + #endif + #ifndef WOLFCOSE_NO_ENCRYPT0 + #define WOLFCOSE_NO_ENCRYPT0 + #endif + #ifndef WOLFCOSE_NO_MAC0 + #define WOLFCOSE_NO_MAC0 + #endif + #ifndef WOLFCOSE_NO_KEY_ENCODE + #define WOLFCOSE_NO_KEY_ENCODE + #endif + #ifndef WOLFCOSE_NO_KEY_DECODE + #define WOLFCOSE_NO_KEY_DECODE + #endif +#endif /* WOLFCOSE_LEAN_LMS */ + +/* The sign+verify LMS profile against a wolfSSL that cannot sign LMS would + * compile and then reject every local signing call at runtime. */ +#if defined(WOLFCOSE_LEAN_LMS) && !defined(WOLFCOSE_LEAN_VERIFY_LMS) && \ + defined(WOLFSSL_LMS_VERIFY_ONLY) && !defined(WOLFCOSE_ENABLE_EXT_SIGN) + #error "WOLFCOSE_LEAN_LMS needs a sign-capable wolfSSL LMS build; use WOLFCOSE_LEAN_VERIFY_LMS with WOLFSSL_LMS_VERIFY_ONLY" +#endif + /* Lean ML-DSA-only COSE_Sign1 sign+verify (no ES256, Sign1 only). */ #ifdef WOLFCOSE_LEAN_MLDSA #ifndef WOLFCOSE_LEAN @@ -193,6 +235,22 @@ extern "C" { #define WOLFCOSE_HAVE_MLDSA #endif +/* HSS/LMS (RFC 8778) — extension */ +/* wc_LmsKey_ImportPubRaw() derives the parameter set from the key bytes only + * from wolfSSL 5.9.2; earlier releases dereference unset parameters. */ +#if defined(WOLFCOSE_ENABLE_LMS) + #ifndef WOLFSSL_HAVE_LMS + #error "WOLFCOSE_ENABLE_LMS requires wolfSSL WOLFSSL_HAVE_LMS" + #endif + #if LIBWOLFSSL_VERSION_HEX < 0x05009002 + #error "WOLFCOSE_ENABLE_LMS requires wolfSSL 5.9.2 or later" + #endif + #define WOLFCOSE_HAVE_LMS +#elif !defined(WOLFCOSE_LEAN) && !defined(WOLFCOSE_NO_LMS) && \ + defined(WOLFSSL_HAVE_LMS) && (LIBWOLFSSL_VERSION_HEX >= 0x05009002) + #define WOLFCOSE_HAVE_LMS +#endif + /* RSA-PSS (PS256/384/512) — extension */ #if defined(WOLFCOSE_ENABLE_RSAPSS) #ifndef WC_RSA_PSS @@ -229,7 +287,7 @@ extern "C" { #endif #if defined(WOLFCOSE_HAVE_ECDSA) || defined(WOLFCOSE_HAVE_EDDSA) || \ defined(WOLFCOSE_HAVE_ED448) || defined(WOLFCOSE_HAVE_RSAPSS) || \ - defined(WOLFCOSE_HAVE_MLDSA) + defined(WOLFCOSE_HAVE_MLDSA) || defined(WOLFCOSE_HAVE_LMS) #define WOLFCOSE_HAVE_SIG #endif @@ -525,14 +583,26 @@ extern "C" { /* ----- Configurable limits (precedence: -D > WOLFCOSE_MIN_BUFFERS > default) ----- * Floors track the largest enabled signature algorithm. See docs/Macros.md. */ #ifndef WOLFCOSE_MAX_SCRATCH_SZ - #if defined(WOLFCOSE_HAVE_MLDSA) + #if defined(WOLFCOSE_HAVE_LMS) + /* Holds the Sig_structure and the HSS/LMS signature placed after it; + * the largest W4 predefined set (L4_H10_W4) is 10204 bytes. */ + #define WOLFCOSE_MAX_SCRATCH_SZ 11264u + #elif defined(WOLFCOSE_HAVE_MLDSA) #define WOLFCOSE_MAX_SCRATCH_SZ 8192u #else #define WOLFCOSE_MAX_SCRATCH_SZ 512u #endif #endif #ifndef WOLFCOSE_MAX_SIG_SZ - #if defined(WOLFCOSE_HAVE_MLDSA) + /* HSS/LMS signature length depends on the key's parameter set + * (levels/height/Winternitz). The default covers every W4 and W8 + * predefined set (largest is L4_H10_W4 at 10204 bytes); the low-Winternitz + * (W1/W2) multi-level sets reach 18012 bytes and need a -D override. An + * attached key whose signature exceeds these buffers is rejected before + * any signing so no one-time state is consumed. */ + #if defined(WOLFCOSE_HAVE_LMS) + #define WOLFCOSE_MAX_SIG_SZ 10240u + #elif defined(WOLFCOSE_HAVE_MLDSA) #define WOLFCOSE_MAX_SIG_SZ 4627u #elif defined(WOLFCOSE_HAVE_RSAPSS) #define WOLFCOSE_MAX_SIG_SZ 512u @@ -582,6 +652,10 @@ extern "C" { #error "wolfCOSE: ML-DSA enabled but WOLFCOSE_MAX_SCRATCH_SZ too small" #endif +#if defined(WOLFCOSE_HAVE_LMS) && (WOLFCOSE_MAX_SCRATCH_SZ < 4096u) + #error "wolfCOSE: HSS/LMS enabled but WOLFCOSE_MAX_SCRATCH_SZ too small" +#endif + #if defined(WOLFCOSE_EXT_SIGN) && !defined(WOLFCOSE_SIGN1_SIGN) && \ !defined(WOLFCOSE_SIGN_SIGN) #error "WOLFCOSE_ENABLE_EXT_SIGN needs a signing op, which needs at least one local signature algorithm; the LEAN_VERIFY profiles are incompatible" diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index 20ab12e..53a30c6 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -51,6 +51,9 @@ #ifdef WOLFSSL_HAVE_MLDSA #include #endif +#ifdef WOLFSSL_HAVE_LMS + #include +#endif #include #ifdef __cplusplus @@ -69,6 +72,9 @@ extern "C" { #if defined(WOLFCOSE_LEAN_VERIFY_MLDSA) && !defined(WOLFCOSE_SIGN1_VERIFY) #error "WOLFCOSE_LEAN_VERIFY_MLDSA requires Sign1 verify; do not also disable it" #endif +#if defined(WOLFCOSE_LEAN_VERIFY_LMS) && !defined(WOLFCOSE_SIGN1_VERIFY) + #error "WOLFCOSE_LEAN_VERIFY_LMS requires Sign1 verify; do not also disable it" +#endif /* ----- Error codes (-9000 to -9099) ----- */ #define WOLFCOSE_SUCCESS 0 @@ -226,6 +232,8 @@ extern "C" { #define WOLFCOSE_ALG_ML_DSA_65 (-49) /* ML-DSA Level 3 */ #define WOLFCOSE_ALG_ML_DSA_87 (-50) /* ML-DSA Level 5 */ +#define WOLFCOSE_ALG_HSS_LMS (-46) /* RFC 8778 HSS/LMS */ + /* RFC 9964: an ML-DSA private key is the 32-byte seed (FIPS 204). */ #define WOLFCOSE_MLDSA_SEED_SZ 32u @@ -234,6 +242,7 @@ extern "C" { #define WOLFCOSE_KTY_EC2 2 #define WOLFCOSE_KTY_RSA 3 #define WOLFCOSE_KTY_SYMMETRIC 4 +#define WOLFCOSE_KTY_HSS_LMS 5 /* RFC 8778: HSS/LMS hash-based signature */ #define WOLFCOSE_KTY_AKP 7 /* RFC 9964: Algorithm Key Pair (ML-DSA) */ /* key.* union is untagged: every member aliases one pointer, so a non-NULL @@ -245,6 +254,7 @@ extern "C" { #define WOLFCOSE_ATT_RSA 4u #define WOLFCOSE_ATT_MLDSA 5u #define WOLFCOSE_ATT_SYMMETRIC 6u +#define WOLFCOSE_ATT_LMS 7u /* Curves */ #define WOLFCOSE_CRV_P256 1 @@ -336,7 +346,8 @@ typedef struct WOLFCOSE_HDR { * * \param cbCtx Opaque caller context, passed through untouched. * \param alg WOLFCOSE_ALG_* being signed with. - * \param tbs To-be-signed bytes (digest, or Sig_structure for EdDSA). + * \param tbs To-be-signed bytes: the digest for ES* and PS*, or the + * complete Sig_structure for EdDSA, ML-DSA, and HSS-LMS. * \param tbsSz Length of tbs. * \param sig Output buffer for the signature. * \param sigSz Capacity of sig. @@ -376,6 +387,9 @@ typedef struct WOLFCOSE_KEY { #endif #ifdef WOLFSSL_HAVE_MLDSA wc_MlDsaKey* mldsa; /**< ML-DSA (FIPS 204), caller-owned */ +#endif +#ifdef WOLFSSL_HAVE_LMS + LmsKey* lms; /**< HSS/LMS (RFC 8778), caller-owned */ #endif void* pqc; /**< Generic PQC handle for future algos */ struct { @@ -813,6 +827,34 @@ WOLFCOSE_API int wc_CoseKey_SetMlDsa_ex(WOLFCOSE_KEY* key, int32_t alg, const uint8_t* seed, size_t seedLen); #endif +#ifdef WOLFCOSE_HAVE_LMS +/** + * \brief Attach an HSS/LMS key to a COSE key structure (RFC 8778). + * + * The caller owns the LmsKey lifecycle and, for signing, configures its + * parameters and state read/write callbacks, then calls wc_LmsKey_MakeKey() + * or wc_LmsKey_Reload() before attaching. Signing locally (no external signer) + * invokes wc_LmsKey_Sign(), which consumes one one-time signature and persists + * the advanced state through those callbacks; wolfCOSE holds no state and + * defines no persistence policy. If a local signing call fails, its persisted + * state may be stale, so wolfCOSE marks the key bad and refuses further signing + * with it, reporting WOLFCOSE_E_CRYPTO. The key's public half is unaffected: + * its public key can still be exported and used for verification. To sign + * again, reconstruct the LmsKey from persisted state (a fresh wc_LmsKey_Init(), + * parameters and callbacks, then wc_LmsKey_Reload()) and reattach it. + * + * With an external signer (wc_CoseKey_SetExtSigner()) the callback performs the + * signature and owns its backend's one-time-signature state: wolfCOSE does not + * mark the key bad on a callback failure, so the callback must quarantine or + * reload its own key after any ambiguous failure before it is reused. + * + * \param key COSE key (must be initialized). + * \param lmsKey Caller-owned, initialized LmsKey. + * \return WOLFCOSE_SUCCESS or negative error code. + */ +WOLFCOSE_API int wc_CoseKey_SetLms(WOLFCOSE_KEY* key, LmsKey* lmsKey); +#endif + #ifdef WOLFCOSE_HAVE_RSAPSS WOLFCOSE_API int wc_CoseKey_SetRsa(WOLFCOSE_KEY* key, RsaKey* rsaKey); #endif @@ -1103,7 +1145,9 @@ WOLFCOSE_API int wc_CoseSign1_Sign_ex(WOLFCOSE_KEY* key, int32_t alg, * * \param key Key whose type determines the signature length. May be * NULL when \p alg determines the exact length. Required - * for RSA-PSS and when both Ed25519 and Ed448 are enabled. + * for RSA-PSS, for HSS-LMS (its length comes from the + * attached key's parameter set), and when both Ed25519 and + * Ed448 are enabled. * \param alg Algorithm identifier (WOLFCOSE_ALG_ES256, etc). * \param kidLen Key ID length (0 if none). * \param payloadLen Attached payload length (0 if detached). @@ -1291,6 +1335,10 @@ WOLFCOSE_API int wc_CoseMac0_Verify(const WOLFCOSE_KEY* key, * Sig_structure = ["Signature", body_protected, sign_protected, ext_aad, payload] * * \param signers Array of WOLFCOSE_SIGNATURE with keys and algorithms. + * At most one entry may use HSS-LMS, and it must be the + * last one, because its one-time state advances as it + * signs and a later signer failing could not return it; + * violating this returns WOLFCOSE_E_INVALID_ARG. * \param signerCount Number of signers (must be >= 1). * \param payload Payload to sign (NULL if detached). * \param payloadLen Payload length (0 if detached). diff --git a/scripts/misra-deviations.json b/scripts/misra-deviations.json index 1c74c6a..4b2f66f 100644 --- a/scripts/misra-deviations.json +++ b/scripts/misra-deviations.json @@ -21,7 +21,7 @@ " } key;" ], "expected": 2, - "range_sha256": "5ea3030e1870253ae4805cb405bf516807cac046c242467fe45648727321b261" + "range_sha256": "5e1a847f33318c6a6d785966753c616a955f2fcde24099f40d0c0c370dcc606e" } ] } diff --git a/src/wolfcose_internal.h b/src/wolfcose_internal.h index 69eb106..e5918cb 100644 --- a/src/wolfcose_internal.h +++ b/src/wolfcose_internal.h @@ -465,7 +465,8 @@ WOLFCOSE_LOCAL int wolfCose_DecodeSkippedRecipient(WOLFCOSE_CBOR_CTX* ctx, WOLFCOSE_LOCAL int wolfCose_KeyCanSign(const WOLFCOSE_KEY* key); #endif -#if defined(WOLFCOSE_KEY_ENCODE) || defined(WOLFCOSE_SIGN1_SIGN) +#if defined(WOLFCOSE_KEY_ENCODE) || defined(WOLFCOSE_SIGN1_SIGN) || \ + defined(WOLFCOSE_SIGN_SIGN) /* SizeAdd -- defined in wolfcose_key.c */ WOLFCOSE_LOCAL int wolfCose_SizeAdd(size_t* total, size_t add); @@ -591,11 +592,26 @@ WOLFCOSE_LOCAL int wolfCose_UpdateRecipientMode(int32_t alg, int* commonMode); WOLFCOSE_LOCAL int wolfCose_MlDsaCheckKey(const WOLFCOSE_KEY* key, int32_t alg); #endif +#if defined(WOLFCOSE_HAVE_LMS) && \ + (defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN1_VERIFY) || \ + defined(WOLFCOSE_SIGN_SIGN) || defined(WOLFCOSE_SIGN_VERIFY) || \ + defined(WOLFCOSE_EXT_SIGN)) +/* LmsCheckKey -- defined in wolfcose_sign1.c */ +WOLFCOSE_LOCAL int wolfCose_LmsCheckKey(const WOLFCOSE_KEY* key); +#endif + #if defined(WOLFCOSE_EXT_SIGN) /* ExtSignAlg -- defined in wolfcose_sign1.c */ WOLFCOSE_LOCAL int wolfCose_ExtSignAlg(int32_t alg, int* preHashes); #endif +#if defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN_SIGN) || \ + defined(WOLFCOSE_EXT_SIGN) +/* SignSigLen -- defined in wolfcose_sign1.c */ +WOLFCOSE_LOCAL int wolfCose_SignSigLen(const WOLFCOSE_KEY* key, int32_t alg, + size_t* expSigLen); +#endif + #if (defined(WOLFCOSE_ENCRYPT0) || defined(WOLFCOSE_ENCRYPT)) && \ (defined(WOLFCOSE_HAVE_AESGCM) || defined(WOLFCOSE_HAVE_AESCCM) || \ defined(WOLFCOSE_HAVE_CHACHA20)) diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index add90d3..00c0eda 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -210,6 +210,39 @@ int wc_CoseKey_SetMlDsa(WOLFCOSE_KEY* key, int32_t alg, } #endif /* WOLFCOSE_HAVE_MLDSA */ +#ifdef WOLFCOSE_HAVE_LMS +int wc_CoseKey_SetLms(WOLFCOSE_KEY* key, LmsKey* lmsKey) +{ + int ret; + + if ((key == NULL) || (lmsKey == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + /* RFC 8778: kty HSS-LMS (5), single algorithm HSS-LMS (-46), no crv. + * The parameter set travels inside the RFC 8554 public key bytes. */ + key->kty = WOLFCOSE_KTY_HSS_LMS; + key->alg = WOLFCOSE_ALG_HSS_LMS; + key->crv = 0; + key->key.lms = lmsKey; + key->attachedType = WOLFCOSE_ATT_LMS; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif + /* WC_LMS_STATE_OK (public wc_lms.h enum) means a private key is + * loaded and able to sign. wc_LmsKey_SigsLeft() is not used here: + * it dereferences private state and faults on a public-only or + * not-yet-loaded key, which SetLms accepts for verification. */ + key->hasPrivate = (lmsKey->state == WC_LMS_STATE_OK) ? 1u : 0u; + ret = WOLFCOSE_SUCCESS; + } + return ret; +} +#endif /* WOLFCOSE_HAVE_LMS */ + #ifdef WOLFCOSE_HAVE_RSAPSS int wc_CoseKey_SetRsa(WOLFCOSE_KEY* key, RsaKey* rsaKey) { @@ -306,7 +339,8 @@ int wc_CoseKey_SetExtSigner(WOLFCOSE_KEY* key, WOLFCOSE_SIGN_CB cb, /* ----- Internal: encoded-size arithmetic ----- * Shared by the COSE_Key and COSE_Sign1 size queries. Every add is checked so * a size computation can never wrap into a too-small buffer request. */ -#if defined(WOLFCOSE_KEY_ENCODE) || defined(WOLFCOSE_SIGN1_SIGN) +#if defined(WOLFCOSE_KEY_ENCODE) || defined(WOLFCOSE_SIGN1_SIGN) || \ + defined(WOLFCOSE_SIGN_SIGN) int wolfCose_SizeAdd(size_t* total, size_t add) { int ret = WOLFCOSE_SUCCESS; @@ -353,7 +387,7 @@ int wolfCose_CborStringSize(size_t len, size_t* encodedLen) } return ret; } -#endif /* WOLFCOSE_KEY_ENCODE || WOLFCOSE_SIGN1_SIGN */ +#endif /* WOLFCOSE_KEY_ENCODE || WOLFCOSE_SIGN1_SIGN || WOLFCOSE_SIGN_SIGN */ #if defined(WOLFCOSE_KEY_ENCODE) @@ -991,6 +1025,86 @@ int wc_CoseKey_Encode_ex(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#ifdef WOLFCOSE_HAVE_LMS + if (key->kty == WOLFCOSE_KTY_HSS_LMS) { + /* RFC 8778 COSE_Key: {1: 5, [2: kid], [3: alg], -1: pub}. The + * RFC defines no private-key labels, so the encoding is always + * public-only; the parameter set is embedded in the RFC 8554 + * public key bytes. */ + uint8_t lmsPubBuf[HSS_MAX_PUBLIC_KEY_LEN]; + word32 lmsPubLen = (word32)sizeof(lmsPubBuf); + size_t lmsMapEntries; + + /* kty alone does not prove the union holds an LmsKey: a key left + * at kty 5 by a failed decode may still hold another type. Gate + * on the attach discriminator before reading key.lms. */ + if (key->attachedType != WOLFCOSE_ATT_LMS) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if (key->key.lms == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + /* RFC 8778 registers only HSS-LMS for kty 5. Refuse to emit a key + * whose alg contradicts kty, which this library's own decoder + * would reject. */ + else if ((key->alg != WOLFCOSE_ALG_UNSET) && + (key->alg != WOLFCOSE_ALG_HSS_LMS)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + /* wc_CoseKey_SetLms accepts a parameter-only key for later decode, + * but its public bytes are still zero. Only a state that holds a + * real public key may be serialised; a key that went bad after a + * signing failure still has its public half, so it is allowed. */ + else if ((key->key.lms->state != WC_LMS_STATE_OK) && + (key->key.lms->state != WC_LMS_STATE_VERIFYONLY) && + (key->key.lms->state != WC_LMS_STATE_NOSIGS) && + (key->key.lms->state != WC_LMS_STATE_BAD)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + /* No action required */ + } + if (ret == WOLFCOSE_SUCCESS) { + INJECT_FAILURE(WOLF_FAIL_LMS_EXPORT_PUB, -1, + ret = wc_LmsKey_ExportPubRaw(key->key.lms, + lmsPubBuf, &lmsPubLen)); + if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + lmsMapEntries = 2u + wolfCose_KeyOptionalEntries(key); + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeMapStart(&ctx, lmsMapEntries); + } + + /* 1: kty = HSS-LMS (5) */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeUint(&ctx, + (uint64_t)WOLFCOSE_KEY_LABEL_KTY); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeUint(&ctx, (uint64_t)key->kty); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_EncodeKeyOptionalFields(&ctx, key); + } + /* -1: pub (RFC 8554 HSS public key bstr) */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeInt(&ctx, + (int64_t)WOLFCOSE_KEY_LABEL_PUB); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeBstr(&ctx, lmsPubBuf, (size_t)lmsPubLen); + } + + if (ret == WOLFCOSE_SUCCESS) { + *outLen = ctx.idx; + } + (void)wolfCose_ForceZero(lmsPubBuf, sizeof(lmsPubBuf)); + } + else +#endif /* WOLFCOSE_HAVE_LMS */ #if defined(WOLFCOSE_HAVE_EDDSA) || defined(WOLFCOSE_HAVE_ED448) if (key->kty == WOLFCOSE_KTY_OKP) { uint8_t pubBuf[57]; /* Ed448 pub = 57 bytes, Ed25519 = 32 */ @@ -1525,6 +1639,51 @@ int wc_CoseKey_EncodeSize_ex(const WOLFCOSE_KEY* key, size_t* outLen, } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#ifdef WOLFCOSE_HAVE_LMS + if (key->kty == WOLFCOSE_KTY_HSS_LMS) { + size_t lmsPubSz = 0u; + + /* Match wc_CoseKey_Encode_ex: reject a kty 5 key whose union does + * not actually hold an LmsKey before reading key.lms. */ + if (key->attachedType != WOLFCOSE_ATT_LMS) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if (key->key.lms == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if ((key->alg != WOLFCOSE_ALG_UNSET) && + (key->alg != WOLFCOSE_ALG_HSS_LMS)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else if ((key->key.lms->state != WC_LMS_STATE_OK) && + (key->key.lms->state != WC_LMS_STATE_VERIFYONLY) && + (key->key.lms->state != WC_LMS_STATE_NOSIGS) && + (key->key.lms->state != WC_LMS_STATE_BAD)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + /* No action required */ + } + if (ret == WOLFCOSE_SUCCESS) { + word32 lmsPubLen = 0; + if (wc_LmsKey_GetPubLen(key->key.lms, &lmsPubLen) != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + else { + lmsPubSz = (size_t)lmsPubLen; + } + } + /* {1: kty, [2: kid], [3: alg], -1: pub}: RFC 8778 defines no + * private-key labels, so the size never includes one. */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_KeyCommonSize(key, (size_t)2, &total); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_KeyBstrEntrySize(lmsPubSz, &total); + } + } + else +#endif /* WOLFCOSE_HAVE_LMS */ #if defined(WOLFCOSE_HAVE_EDDSA) || defined(WOLFCOSE_HAVE_ED448) if (key->kty == WOLFCOSE_KTY_OKP) { size_t okpSz = 0u; @@ -1651,6 +1810,11 @@ static int wolfCose_KeyAttachedTypeCheck(const WOLFCOSE_KEY* key) ret = WOLFCOSE_E_COSE_KEY_TYPE; } break; + case WOLFCOSE_ATT_LMS: + if (key->kty != WOLFCOSE_KTY_HSS_LMS) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + break; default: ret = WOLFCOSE_E_COSE_KEY_TYPE; break; @@ -2071,6 +2235,22 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) } #endif + /* RFC 8778 registers only the HSS-LMS algorithm for kty HSS-LMS. */ + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_HSS_LMS) && + (key->alg != WOLFCOSE_ALG_UNSET) && + (key->alg != WOLFCOSE_ALG_HSS_LMS)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + /* RFC 8778 defines exactly one key member: the public key at -1. + * It arrives as a bstr, so the label loop stashed it in nData. */ + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_HSS_LMS) && + ((nData == NULL) || (xData != NULL) || (yData != NULL) || + (dData != NULL))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + /* Import key data into wolfCrypt key structs */ if (ret == WOLFCOSE_SUCCESS) { #ifdef HAVE_ECC @@ -2323,6 +2503,27 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#ifdef WOLFCOSE_HAVE_LMS + if ((key->kty == WOLFCOSE_KTY_HSS_LMS) && + (key->attachedType == WOLFCOSE_ATT_LMS)) { + /* RFC 8778: pub(-1) bstr was stashed in nData. The RFC 8554 + * levels/type codes inside it select the parameter set, so + * the import derives or cross-checks the key's parameters. + * There is no private-key wire form: the import is always + * public/verify-only. Reaching here means nData holds the pub + * bstr, so label -1 was not an int and crv is unset. */ + INJECT_FAILURE(WOLF_FAIL_LMS_IMPORT_PUB, -1, + ret = wc_LmsKey_ImportPubRaw(key->key.lms, nData, + (word32)nLen)); + if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + else { + key->hasPrivate = 0u; + } + } + else +#endif /* WOLFCOSE_HAVE_LMS */ #if defined(WOLFCOSE_HAVE_EDDSA) || defined(WOLFCOSE_HAVE_ED448) if (key->kty == WOLFCOSE_KTY_OKP) { /* RFC 9052: x is recommended, not required, for a private OKP diff --git a/src/wolfcose_sign.c b/src/wolfcose_sign.c index d07e16c..3911efe 100644 --- a/src/wolfcose_sign.c +++ b/src/wolfcose_sign.c @@ -44,6 +44,7 @@ #include #endif #include +#include /* ----- @@ -56,10 +57,96 @@ #if defined(WOLFCOSE_SIGN) #if defined(WOLFCOSE_SIGN_SIGN) + +#if defined(WOLFCOSE_HAVE_LMS) && \ + (defined(WOLFCOSE_EXT_SIGN) || !defined(WOLFSSL_LMS_VERIFY_ONLY)) +/* Exact encoded size of the tagged COSE_Sign that wc_CoseSign_Sign() emits for + * this signer set. Used only when a stateful HSS/LMS signer is present, so it + * is never advanced for an output buffer that cannot hold the result, whether + * signed locally or through a delegated callback. Mirrors the field order of + * the encoder below. */ +static int wolfCose_SignEncodedSize(const WOLFCOSE_SIGNATURE* signers, + size_t signerCount, size_t payloadLen, + uint8_t isDetached, size_t* outLen) +{ + uint8_t protectedBuf[WOLFCOSE_PROTECTED_HDR_MAX]; + size_t protectedLen = 0; + size_t total = 0; + size_t itemLen = 0; + size_t sigLen = 0; + size_t i; + int ret; + + /* tag 98 (2 bytes) + array(4) + empty body-protected bstr + map(0). */ + ret = wolfCose_SizeAdd(&total, 5u); + if (ret == WOLFCOSE_SUCCESS) { + if (isDetached != 0u) { + ret = wolfCose_SizeAdd(&total, 1u); /* nil payload */ + } + else { + ret = wolfCose_CborStringSize(payloadLen, &itemLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, itemLen); + } + } + } + /* Signatures array header. CBOR argument width is identical across major + * types, so the bstr sizer minus the length yields the header bytes. */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CborStringSize(signerCount, &itemLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, itemLen - signerCount); + } + } + for (i = 0; (ret == WOLFCOSE_SUCCESS) && (i < signerCount); i++) { + ret = wolfCose_EncodeProtectedHdr(signers[i].algId, protectedBuf, + sizeof(protectedBuf), &protectedLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SignSigLen(signers[i].key, signers[i].algId, + &sigLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, 1u); /* array(3) */ + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CborStringSize(protectedLen, &itemLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, itemLen); + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, 1u); /* map(0) or map(1) */ + } + if ((ret == WOLFCOSE_SUCCESS) && (signers[i].kid != NULL) && + (signers[i].kidLen > 0u)) { + ret = wolfCose_SizeAdd(&total, 1u); /* kid label 4 */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CborStringSize(signers[i].kidLen, &itemLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, itemLen); + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_CborStringSize(sigLen, &itemLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SizeAdd(&total, itemLen); + } + } + } + if (ret == WOLFCOSE_SUCCESS) { + *outLen = total; + } + return ret; +} +#endif /* WOLFCOSE_HAVE_LMS && (EXT_SIGN || !WOLFSSL_LMS_VERIFY_ONLY) */ + /** * Create a multi-signer COSE_Sign message. * - * \param signers Array of signer configurations + * \param signers Array of signer configurations. At most one may use + * HSS-LMS, and it must be the last entry: a later + * signer failing could not give back its spent leaf. * \param signerCount Number of signers (must be >= 1) * \param payload Payload to sign * \param payloadLen Payload length @@ -237,6 +324,119 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, } } +#if defined(WOLFCOSE_HAVE_LMS) && \ + (defined(WOLFCOSE_EXT_SIGN) || !defined(WOLFSSL_LMS_VERIFY_ONLY)) + /* A stateful HSS/LMS signer is advanced inside the loop below, before the + * later signers and the output encoding run, whether signed locally or + * through a delegated callback. Verify every signer resolves and the whole + * COSE_Sign fits in out before signing any of them, so an LMS one-time + * signature is never spent on an operation that then fails. */ + if (ret == WOLFCOSE_SUCCESS) { + int haveLms = 0; + size_t neededOut = 0; + for (i = 0; i < signerCount; i++) { + if (signers[i].algId == WOLFCOSE_ALG_HSS_LMS) { + haveLms++; + } + } + /* Signing is sequential and a spent LMS leaf cannot be recovered, so + * any signer failing after it would waste that leaf. Allow one HSS-LMS + * signer, and only in the last slot, so nothing runs after it. */ + if ((haveLms > 1) || + ((haveLms == 1) && + (signers[signerCount - 1u].algId != WOLFCOSE_ALG_HSS_LMS))) { + ret = WOLFCOSE_E_INVALID_ARG; + } + if ((ret == WOLFCOSE_SUCCESS) && (haveLms != 0)) { + ret = wolfCose_SignEncodedSize(signers, signerCount, payloadLen, + isDetached, &neededOut); + if ((ret == WOLFCOSE_SUCCESS) && (outSz < neededOut)) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + /* Every signer's scratch layout too: a later signer's scratch failure + * would otherwise surface after an earlier LMS signer spent its state. + * In-place signers need the Sig_structure plus the signature; the rest + * reuse the structure's space. */ + for (i = 0; (ret == WOLFCOSE_SUCCESS) && (haveLms != 0) && + (i < signerCount); i++) { + size_t sigLen = 0; + size_t need = 0; + int inPlace = 0; +#if defined(WOLFCOSE_EXT_SIGN) + int extPreHash = 0; +#endif + + /* ML-DSA and HSS-LMS sign the Sig_structure in place; a delegated + * signer's placement is refined once its pre-hash is known. */ + if ((signers[i].algId == WOLFCOSE_ALG_ML_DSA_44) || + (signers[i].algId == WOLFCOSE_ALG_ML_DSA_65) || + (signers[i].algId == WOLFCOSE_ALG_ML_DSA_87) || + (signers[i].algId == WOLFCOSE_ALG_HSS_LMS)) { + inPlace = 1; + } + + ret = wolfCose_EncodeProtectedHdr(signers[i].algId, + signerProtectedBuf, + sizeof(signerProtectedBuf), + &signerProtectedLen); + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_BuildToBeSignedMaced( + WOLFCOSE_CTX_SIGNATURE, sizeof(WOLFCOSE_CTX_SIGNATURE), + bodyProtectedBuf, bodyProtectedLen, + signerProtectedBuf, signerProtectedLen, + extAad, extAadLen, + sigPayload, sigPayloadLen, + scratch, scratchSz, &sigStructLen); + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_SignSigLen(signers[i].key, signers[i].algId, + &sigLen); + } +#if !defined(WOLFSSL_LMS_VERIFY_ONLY) + /* A local LMS signer must be able to sign right now; GetSigLen + * above only proves the parameter set. */ + if ((ret == WOLFCOSE_SUCCESS) && +#if defined(WOLFCOSE_EXT_SIGN) + (signers[i].key->signCb == NULL) && +#endif + (signers[i].algId == WOLFCOSE_ALG_HSS_LMS)) { + LmsKey* lmsCheck = signers[i].key->key.lms; + if ((lmsCheck->state != WC_LMS_STATE_OK) || + (wc_LmsKey_SigsLeft(lmsCheck) != 1)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + } +#endif /* !WOLFSSL_LMS_VERIFY_ONLY */ +#if defined(WOLFCOSE_EXT_SIGN) + if ((ret == WOLFCOSE_SUCCESS) && + (signers[i].key->signCb != NULL)) { + ret = wolfCose_ExtSignAlg(signers[i].algId, &extPreHash); + inPlace = 0; + if (extPreHash == 0) { + inPlace = 1; + } + } +#endif + if (ret == WOLFCOSE_SUCCESS) { + need = sigStructLen; + if (inPlace != 0) { + ret = wolfCose_SizeAdd(&need, sigLen); + } + else if (sigLen > need) { + need = sigLen; + } + else { + /* No action required */ + } + } + if ((ret == WOLFCOSE_SUCCESS) && (need > scratchSz)) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + } +#endif /* WOLFCOSE_HAVE_LMS && (EXT_SIGN || !WOLFSSL_LMS_VERIFY_ONLY) */ + /* Body protected headers: zero-length bstr for multi-signer (RFC 9052 §3.1) */ if (ret == WOLFCOSE_SUCCESS) { bodyProtectedLen = 0; @@ -293,15 +493,17 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, * inside each algorithm branch so this dispatch tolerates * algorithms whose signature size is computed dynamically * (RSA-PSS) or whose entry is gated by a different feature - * macro (ML-DSA). ML-DSA signs the Sig_structure directly - * without a pre-hash so the hash type lookup is skipped. */ + * macro (ML-DSA). ML-DSA and HSS-LMS sign the Sig_structure + * directly without a pre-hash so the hash type lookup is + * skipped. */ if ((ret == WOLFCOSE_SUCCESS) && #if defined(WOLFCOSE_EXT_SIGN) (signerKey->signCb == NULL) && #endif (signer->algId != WOLFCOSE_ALG_ML_DSA_44) && (signer->algId != WOLFCOSE_ALG_ML_DSA_65) && - (signer->algId != WOLFCOSE_ALG_ML_DSA_87)) { + (signer->algId != WOLFCOSE_ALG_ML_DSA_87) && + (signer->algId != WOLFCOSE_ALG_HSS_LMS)) { ret = wolfCose_AlgToHashType(signer->algId, &hashType); } @@ -323,9 +525,9 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, scratch, scratchSz, &sigStructLen); } - /* Hash the Sig_structure for algorithms that pre-hash. EdDSA - * and ML-DSA sign the structure directly, and a delegated signer - * does its own hashing inside wolfCose_ExtSign. */ + /* Hash the Sig_structure for algorithms that pre-hash. EdDSA, + * ML-DSA and HSS-LMS sign the structure directly, and a delegated + * signer does its own hashing inside wolfCose_ExtSign. */ if ((ret == WOLFCOSE_SUCCESS) && #if defined(WOLFCOSE_EXT_SIGN) (signerKey->signCb == NULL) && @@ -333,7 +535,8 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, (signer->algId != WOLFCOSE_ALG_EDDSA) && (signer->algId != WOLFCOSE_ALG_ML_DSA_44) && (signer->algId != WOLFCOSE_ALG_ML_DSA_65) && - (signer->algId != WOLFCOSE_ALG_ML_DSA_87)) { + (signer->algId != WOLFCOSE_ALG_ML_DSA_87) && + (signer->algId != WOLFCOSE_ALG_HSS_LMS)) { int digestSz = wc_HashGetDigestSize(hashType); if (digestSz <= 0) { ret = WOLFCOSE_E_CRYPTO; @@ -507,6 +710,61 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#if defined(WOLFCOSE_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) + if ((ret == WOLFCOSE_SUCCESS) && + (signer->algId == WOLFCOSE_ALG_HSS_LMS)) { + size_t expectedSigSz = 0; + + /* RFC 8778: HSS-LMS key attached via wc_CoseKey_SetLms(); + * signature length comes from the key's parameter set. */ + ret = wolfCose_LmsCheckKey(signerKey); + if (ret == WOLFCOSE_SUCCESS) { + word32 lmsSigLen = 0; + if (wc_LmsKey_GetSigLen(signerKey->key.lms, + &lmsSigLen) != 0) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + expectedSigSz = (size_t)lmsSigLen; + } + } + /* wolfSSL takes the message length as int. */ + if ((ret == WOLFCOSE_SUCCESS) && + (sigStructLen > (size_t)INT_MAX)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + /* Sig output goes after Sig_structure in scratch; subtraction + * form so the capacity check cannot wrap. */ + if ((ret == WOLFCOSE_SUCCESS) && + ((expectedSigSz > scratchSz) || + (sigStructLen > (scratchSz - expectedSigSz)))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + if (ret == WOLFCOSE_SUCCESS) { + word32 lmsSigLen = (word32)expectedSigSz; + INJECT_FAILURE(WOLF_FAIL_LMS_SIGN, -1, + ret = wc_LmsKey_Sign(signerKey->key.lms, + &scratch[sigStructLen], &lmsSigLen, + scratch, (int)sigStructLen)); + if (ret != 0) { + /* wolfSSL 5.9.2 can leave the key WC_LMS_STATE_OK after a + * persistence-write failure, so mark it bad here; the + * preflight refuses a bad key before any signer runs. Keep + * an exhausted key as WC_LMS_STATE_NOSIGS so its public key + * can still be exported. */ + if (signerKey->key.lms->state != WC_LMS_STATE_NOSIGS) { + signerKey->key.lms->state = WC_LMS_STATE_BAD; + } + ret = WOLFCOSE_E_CRYPTO; + } + else { + sigPtr = &scratch[sigStructLen]; + sigSz = (size_t)lmsSigLen; + } + } + } + else +#endif /* WOLFCOSE_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ if (ret == WOLFCOSE_SUCCESS) { ret = WOLFCOSE_E_COSE_BAD_ALG; } @@ -791,23 +1049,25 @@ int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey, scratch, scratchSz, &sigStructLen); } - /* Get hash type for algorithms that pre-hash. EdDSA and ML-DSA - * verify against the raw Sig_structure so the hash type lookup is - * skipped (also avoids WOLFCOSE_E_COSE_BAD_ALG for ML-DSA since - * the algorithm has no external hash). */ + /* Get hash type for algorithms that pre-hash. EdDSA, ML-DSA and + * HSS-LMS verify against the raw Sig_structure so the hash type + * lookup is skipped (also avoids WOLFCOSE_E_COSE_BAD_ALG since + * these algorithms have no external hash). */ if ((ret == WOLFCOSE_SUCCESS) && (alg != WOLFCOSE_ALG_EDDSA) && (alg != WOLFCOSE_ALG_ML_DSA_44) && (alg != WOLFCOSE_ALG_ML_DSA_65) && - (alg != WOLFCOSE_ALG_ML_DSA_87)) { + (alg != WOLFCOSE_ALG_ML_DSA_87) && + (alg != WOLFCOSE_ALG_HSS_LMS)) { ret = wolfCose_AlgToHashType(alg, &hashType); } - /* Hash the Sig_structure for algorithms that pre-hash. EdDSA and - * ML-DSA verify the structure directly. */ + /* Hash the Sig_structure for algorithms that pre-hash. EdDSA, + * ML-DSA and HSS-LMS verify the structure directly. */ if ((ret == WOLFCOSE_SUCCESS) && (alg != WOLFCOSE_ALG_EDDSA) && (alg != WOLFCOSE_ALG_ML_DSA_44) && (alg != WOLFCOSE_ALG_ML_DSA_65) && - (alg != WOLFCOSE_ALG_ML_DSA_87)) { + (alg != WOLFCOSE_ALG_ML_DSA_87) && + (alg != WOLFCOSE_ALG_HSS_LMS)) { int digestSz = wc_HashGetDigestSize(hashType); if (digestSz <= 0) { ret = WOLFCOSE_E_CRYPTO; @@ -983,6 +1243,33 @@ int wc_CoseSign_Verify(const WOLFCOSE_KEY* verifyKey, } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#ifdef WOLFCOSE_HAVE_LMS + if ((ret == WOLFCOSE_SUCCESS) && (alg == WOLFCOSE_ALG_HSS_LMS)) { + /* RFC 8778: HSS-LMS verifies the raw Sig_structure directly. Only a + * genuine mismatch (SIG_VERIFY_E) is an auth failure. */ + ret = wolfCose_LmsCheckKey(verifyKey); + if ((ret == WOLFCOSE_SUCCESS) && (sigStructLen > (size_t)INT_MAX)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + if (ret == WOLFCOSE_SUCCESS) { + LmsKey* lmsKey = verifyKey->key.lms; + INJECT_FAILURE(WOLF_FAIL_LMS_VERIFY, SIG_VERIFY_E, + ret = wc_LmsKey_Verify(lmsKey, + signature, (word32)signatureLen, + scratch, (int)sigStructLen)); + if (ret == (int)SIG_VERIFY_E) { + ret = WOLFCOSE_E_COSE_SIG_FAIL; + } + else if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + else { + /* No action required */ + } + } + } + else +#endif /* WOLFCOSE_HAVE_LMS */ if (ret == WOLFCOSE_SUCCESS) { ret = WOLFCOSE_E_COSE_BAD_ALG; } diff --git a/src/wolfcose_sign1.c b/src/wolfcose_sign1.c index f2aeef7..bd66b04 100644 --- a/src/wolfcose_sign1.c +++ b/src/wolfcose_sign1.c @@ -44,6 +44,7 @@ #include #endif #include +#include /* ----- COSE_Sign1 API ----- */ @@ -99,12 +100,31 @@ int wolfCose_MlDsaCheckKey(const WOLFCOSE_KEY* key, int32_t alg) } #endif /* WOLFCOSE_HAVE_MLDSA */ -#if defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_EXT_SIGN) +#if defined(WOLFCOSE_HAVE_LMS) && \ + (defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN1_VERIFY) || \ + defined(WOLFCOSE_SIGN_SIGN) || defined(WOLFCOSE_SIGN_VERIFY) || \ + defined(WOLFCOSE_EXT_SIGN)) +/* RFC 8778: validate that the key is HSS-LMS-typed and was attached through + * wc_CoseKey_SetLms() so the union member is known to be an LmsKey. */ +int wolfCose_LmsCheckKey(const WOLFCOSE_KEY* key) +{ + int ret = WOLFCOSE_SUCCESS; + + if ((key == NULL) || (key->kty != WOLFCOSE_KTY_HSS_LMS) || + (key->attachedType != WOLFCOSE_ATT_LMS) || (key->key.lms == NULL)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + return ret; +} +#endif /* WOLFCOSE_HAVE_LMS */ + +#if defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN_SIGN) || \ + defined(WOLFCOSE_EXT_SIGN) /* Exact signature length for this key and algorithm. wolfCose_SigSize() alone * reports EdDSA's worst case rather than the key's curve, and has no RSA case. * Fails closed when the exact length cannot be determined. */ -static int wolfCose_SignSigLen(const WOLFCOSE_KEY* key, int32_t alg, - size_t* expSigLen) +int wolfCose_SignSigLen(const WOLFCOSE_KEY* key, int32_t alg, + size_t* expSigLen) { int ret; @@ -158,6 +178,27 @@ static int wolfCose_SignSigLen(const WOLFCOSE_KEY* key, int32_t alg, ret = wolfCose_RsaPssCheckKey(key, expSigLen); } break; +#endif +#if defined(WOLFCOSE_HAVE_LMS) && \ + (defined(WOLFCOSE_EXT_SIGN) || !defined(WOLFSSL_LMS_VERIFY_ONLY)) + case WOLFCOSE_ALG_HSS_LMS: + { + /* The exact length lives in the attached key's parameter set, + * so an LMS key must be attached even for a delegated signer. + * wc_LmsKey_GetSigLen is available in verify-only builds, so a + * delegated signer can size an LMS signature there too. */ + ret = wolfCose_LmsCheckKey(key); + if (ret == WOLFCOSE_SUCCESS) { + word32 lmsSigLen = 0; + if (wc_LmsKey_GetSigLen(key->key.lms, &lmsSigLen) != 0) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + *expSigLen = (size_t)lmsSigLen; + } + } + } + break; #endif default: ret = wolfCose_SigSize(alg, expSigLen); @@ -251,6 +292,13 @@ int wolfCose_ExtSignAlg(int32_t alg, int* preHashes) case WOLFCOSE_ALG_ML_DSA_87: *preHashes = 0; break; +#endif +#if defined(WOLFCOSE_HAVE_LMS) + /* Delegated signing only needs the length and dispatch, not the local + * wc_LmsKey_Sign, so this stays available in verify-only builds. */ + case WOLFCOSE_ALG_HSS_LMS: + *preHashes = 0; + break; #endif default: ret = WOLFCOSE_E_COSE_BAD_ALG; @@ -590,6 +638,21 @@ int wc_CoseSign1_Sign_ex(WOLFCOSE_KEY* key, int32_t alg, scratch, scratchSz, &sigStructLen); } +#if defined(WOLFCOSE_HAVE_LMS) + /* HSS-LMS is one-time state whether signed locally or through a delegated + * callback. A too-small out is otherwise caught only at the encode step + * below, after the signature is spent and a retry burns another. Reject it + * before any signing runs, ahead of the local-vs-delegated dispatch. */ + if ((ret == WOLFCOSE_SUCCESS) && (alg == WOLFCOSE_ALG_HSS_LMS)) { + size_t neededOut = 0; + ret = wc_CoseSign1_SignSize_ex(key, alg, kidLen, payloadLen, + detachedLen, flags, &neededOut); + if ((ret == WOLFCOSE_SUCCESS) && (outSz < neededOut)) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } +#endif /* WOLFCOSE_HAVE_LMS */ + /* Sign based on algorithm */ #if defined(WOLFCOSE_EXT_SIGN) if ((ret == WOLFCOSE_SUCCESS) && (key->signCb != NULL)) { @@ -836,6 +899,77 @@ int wc_CoseSign1_Sign_ex(WOLFCOSE_KEY* key, int32_t alg, } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#if defined(WOLFCOSE_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) + if ((ret == WOLFCOSE_SUCCESS) && (alg == WOLFCOSE_ALG_HSS_LMS)) { + size_t expectedSigSz = 0; + + /* RFC 8778: HSS-LMS key attached via wc_CoseKey_SetLms(). The + * signature length comes from the key's parameter set. Signing + * consumes one-time-signature state; the caller-installed wolfCrypt + * write callback persists it. The out-buffer capacity was already + * checked above, before the local-vs-delegated dispatch. */ + ret = wolfCose_LmsCheckKey(key); + + /* Must be able to sign now: reject a non-OK (for example a key wolfSSL + * marked bad after a persistence failure) or exhausted key before the + * backend runs, matching wc_CoseSign_Sign and leaving wolfSSL's own + * state for the caller to inspect. SigsLeft returns 1 while any remain, + * 0 when exhausted. */ + if ((ret == WOLFCOSE_SUCCESS) && + ((key->key.lms->state != WC_LMS_STATE_OK) || + (wc_LmsKey_SigsLeft(key->key.lms) != 1))) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + + if (ret == WOLFCOSE_SUCCESS) { + word32 lmsSigLen = 0; + if (wc_LmsKey_GetSigLen(key->key.lms, &lmsSigLen) != 0) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + expectedSigSz = (size_t)lmsSigLen; + } + } + + /* wolfSSL takes the message length as int. */ + if ((ret == WOLFCOSE_SUCCESS) && (sigStructLen > (size_t)INT_MAX)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + /* Sig output goes after Sig_structure in scratch. LMS signs the + * raw Sig_structure directly (no pre-hash). Subtraction form so the + * capacity check cannot wrap. */ + if ((ret == WOLFCOSE_SUCCESS) && + ((expectedSigSz > scratchSz) || + (sigStructLen > (scratchSz - expectedSigSz)))) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + + if (ret == WOLFCOSE_SUCCESS) { + word32 lmsSigLen = (word32)expectedSigSz; + INJECT_FAILURE(WOLF_FAIL_LMS_SIGN, -1, + ret = wc_LmsKey_Sign(key->key.lms, + &scratch[sigStructLen], &lmsSigLen, + scratch, (int)sigStructLen)); + if (ret != 0) { + /* wolfSSL 5.9.2 can leave the key WC_LMS_STATE_OK after a + * persistence-write failure, so mark it bad here to stop later + * signing from state that may not have been persisted; the + * pre-sign check above then refuses it. Keep an exhausted key + * as WC_LMS_STATE_NOSIGS so exhaustion stays distinguishable + * and its public key can still be exported. */ + if (key->key.lms->state != WC_LMS_STATE_NOSIGS) { + key->key.lms->state = WC_LMS_STATE_BAD; + } + ret = WOLFCOSE_E_CRYPTO; + } + else { + sigPtr = &scratch[sigStructLen]; + sigSz = (size_t)lmsSigLen; + } + } + } + else +#endif /* WOLFCOSE_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ if (ret == WOLFCOSE_SUCCESS) { ret = WOLFCOSE_E_COSE_BAD_ALG; } @@ -1276,6 +1410,34 @@ int wc_CoseSign1_Verify(const WOLFCOSE_KEY* key, } else #endif /* WOLFCOSE_HAVE_MLDSA */ +#ifdef WOLFCOSE_HAVE_LMS + if ((ret == WOLFCOSE_SUCCESS) && (alg == WOLFCOSE_ALG_HSS_LMS)) { + /* RFC 8778: HSS-LMS verifies the raw Sig_structure (no pre-hash). + * Only a genuine signature mismatch (SIG_VERIFY_E) is an auth + * failure; a bad-arg/state/length error is an operational fault. */ + ret = wolfCose_LmsCheckKey(key); + if ((ret == WOLFCOSE_SUCCESS) && (sigStructLen > (size_t)INT_MAX)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + if (ret == WOLFCOSE_SUCCESS) { + LmsKey* lmsKey = key->key.lms; + INJECT_FAILURE(WOLF_FAIL_LMS_VERIFY, SIG_VERIFY_E, + ret = wc_LmsKey_Verify(lmsKey, + sigData, (word32)sigDataLen, + scratch, (int)sigStructLen)); + if (ret == (int)SIG_VERIFY_E) { + ret = WOLFCOSE_E_COSE_SIG_FAIL; + } + else if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + else { + /* No action required */ + } + } + } + else +#endif /* WOLFCOSE_HAVE_LMS */ if (ret == WOLFCOSE_SUCCESS) { ret = WOLFCOSE_E_COSE_BAD_ALG; } diff --git a/tests/force_failure.h b/tests/force_failure.h index 8ee4602..88c8a1c 100644 --- a/tests/force_failure.h +++ b/tests/force_failure.h @@ -82,6 +82,12 @@ typedef enum { WOLF_FAIL_MLDSA_IMPORT_PUB, /* wc_MlDsaKey_ImportPubRaw */ WOLF_FAIL_MLDSA_IMPORT_PRIV, /* wc_MlDsaKey_MakeKeyFromSeed */ + /* HSS/LMS failures */ + WOLF_FAIL_LMS_SIGN, /* wc_LmsKey_Sign */ + WOLF_FAIL_LMS_VERIFY, /* wc_LmsKey_Verify */ + WOLF_FAIL_LMS_EXPORT_PUB, /* wc_LmsKey_ExportPubRaw */ + WOLF_FAIL_LMS_IMPORT_PUB, /* wc_LmsKey_ImportPubRaw */ + /* HMAC failures */ WOLF_FAIL_HMAC_SET_KEY, /* wc_HmacSetKey */ WOLF_FAIL_HMAC_UPDATE, /* wc_HmacUpdate */ diff --git a/tests/misra_consumer.c b/tests/misra_consumer.c index ca25de1..6c8087b 100644 --- a/tests/misra_consumer.c +++ b/tests/misra_consumer.c @@ -55,6 +55,9 @@ static size_t wolfCose_MisraUsePublicApis(void) WOLFCOSE_MISRA_USE_API(wc_CoseKey_SetMlDsa); WOLFCOSE_MISRA_USE_API(wc_CoseKey_SetMlDsa_ex); #endif +#ifdef WOLFCOSE_HAVE_LMS + WOLFCOSE_MISRA_USE_API(wc_CoseKey_SetLms); +#endif #ifdef WOLFCOSE_HAVE_RSAPSS WOLFCOSE_MISRA_USE_API(wc_CoseKey_SetRsa); #endif diff --git a/tests/test_cose.c b/tests/test_cose.c index 2946196..2a4743d 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -3515,6 +3515,1229 @@ static void test_cose_sign1_ml_dsa_level_mismatch(void) } #endif /* WOLFCOSE_HAVE_MLDSA */ +/* ----- COSE_Sign1 with HSS/LMS (RFC 8778) ----- */ +#if defined(WOLFCOSE_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) +/* These tests persist the compact LMS private key. WOLFSSL_WC_LMS_SERIALIZE_STATE + * hands the callback the larger serialized working state instead, which needs a + * bigger backing store than HSS_MAX_PRIVATE_KEY_LEN. */ +#ifdef WOLFSSL_WC_LMS_SERIALIZE_STATE + #error "LMS tests assume compact persistence; WOLFSSL_WC_LMS_SERIALIZE_STATE needs a larger store" +#endif +/* Memory-backed LMS private-key state callbacks. The context is the + * per-key storage buffer, sized for any parameter set. */ +/* Set to make the next persistence write fail: the callbacks are fixed at + * keygen, so a lost-storage fault is modelled by a flag. */ +static int g_lmsWriteFail = 0; + +static int test_lms_write_cb(const byte* priv, word32 privSz, void* context) +{ + int ret = (int)WC_LMS_RC_WRITE_FAIL; + + if ((g_lmsWriteFail == 0) && (priv != NULL) && (context != NULL) && + (privSz <= (word32)HSS_MAX_PRIVATE_KEY_LEN)) { + memcpy(context, priv, (size_t)privSz); + ret = (int)WC_LMS_RC_SAVED_TO_NV_MEMORY; + } + return ret; +} + +/* cppcheck-suppress constParameterCallback ; signature fixed by + * wc_lms_read_private_key_cb typedef */ +static int test_lms_read_cb(byte* priv, word32 privSz, void* context) +{ + int ret = (int)WC_LMS_RC_READ_FAIL; + + if ((priv != NULL) && (context != NULL) && + (privSz <= (word32)HSS_MAX_PRIVATE_KEY_LEN)) { + memcpy(priv, context, (size_t)privSz); + ret = (int)WC_LMS_RC_READ_TO_MEMORY; + } + return ret; +} + +/* Generate a signing-capable LMS key with the given parameter set. */ +static int test_lms_make_key_ex(LmsKey* key, byte* privStore, WC_RNG* rng, + enum wc_LmsParm parm) +{ + int ret; + + ret = wc_LmsKey_Init(key, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_LmsKey_SetLmsParm(key, parm); + } + if (ret == 0) { + ret = wc_LmsKey_SetWriteCb(key, test_lms_write_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetReadCb(key, test_lms_read_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetContext(key, privStore); + } + if (ret == 0) { + ret = wc_LmsKey_MakeKey(key, rng); + } + return ret; +} + +/* L1/H5/W8 keeps keygen fast for the common case. */ +static int test_lms_make_key(LmsKey* key, byte* privStore, WC_RNG* rng) +{ + return test_lms_make_key_ex(key, privStore, rng, WC_LMS_PARM_L1_H5_W8); +} + +static byte g_lmsPrivA[HSS_MAX_PRIVATE_KEY_LEN]; +static byte g_lmsPrivB[HSS_MAX_PRIVATE_KEY_LEN]; +/* Snapshot of a persisted private state: wc_LmsKey_SigsLeft() only reports + * whether any signatures remain, so state preservation is proven by comparing + * the bytes the write callback stored. */ +static byte g_lmsPrivSnap[HSS_MAX_PRIVATE_KEY_LEN]; + +static void test_cose_sign1_lms(void) +{ + WOLFCOSE_KEY signKey; + LmsKey lmsKey; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int lmsInited = 0; + uint8_t payload[] = "HSS-LMS payload"; + uint8_t scratch[4096]; + uint8_t out[4096]; + size_t outLen = 0; + size_t sizedLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign1 HSS-LMS]\n"); + + ret = wc_InitRng(&rng); + if (ret != 0) { TEST_ASSERT(0, "rng init"); } + if (ret == 0) { rngInited = 1; } + + if (ret == 0) { + ret = test_lms_make_key(&lmsKey, g_lmsPrivA, &rng); + if (ret != 0) { TEST_ASSERT(0, "lms keygen"); } + if (ret == 0) { lmsInited = 1; } + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&signKey); + ret = wc_CoseKey_SetLms(&signKey, &lmsKey); + TEST_ASSERT(ret == 0 && signKey.kty == WOLFCOSE_KTY_HSS_LMS && + signKey.alg == WOLFCOSE_ALG_HSS_LMS && + signKey.hasPrivate == 1u, "lms set key"); + } + + if (ret == 0) { + /* Length depends on the key's parameter set, so key is required. */ + ret = wc_CoseSign1_SignSize_ex(&signKey, WOLFCOSE_ALG_HSS_LMS, 0u, + sizeof(payload) - 1u, 0u, 0u, &sizedLen); + TEST_ASSERT(ret == 0, "sign1 lms size"); + + if (ret == 0) { + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, + NULL, 0, + payload, sizeof(payload) - 1, + NULL, 0, /* detachedPayload, detachedLen */ + NULL, 0, /* extAad, extAadLen */ + scratch, sizeof(scratch), + out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0 && outLen > 0, "sign1 lms sign"); + TEST_ASSERT(outLen == sizedLen, "sign1 lms exact size"); + } + } + + if (ret == 0) { + ret = wc_CoseSign1_Verify(&signKey, out, outLen, + NULL, 0, /* detachedPayload, detachedLen */ + NULL, 0, /* extAad, extAadLen */ + scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(ret == 0, "sign1 lms verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + memcmp(decPayload, payload, decPayloadLen) == 0, + "sign1 lms payload match"); + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_HSS_LMS, "sign1 lms hdr alg"); + } + + if (ret == 0) { + /* Tampered signature must fail */ + int tamperRet; + out[outLen - 1u] ^= 0x01u; + tamperRet = wc_CoseSign1_Verify(&signKey, out, outLen, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(tamperRet != 0, "sign1 lms tamper fails"); + out[outLen - 1u] ^= 0x01u; + } + + if (ret == 0) { + /* Wrong key should fail */ + LmsKey lmsWrong; + WOLFCOSE_KEY wrongKey; + int wrongRet; + + wrongRet = test_lms_make_key(&lmsWrong, g_lmsPrivB, &rng); + if (wrongRet == 0) { + (void)wc_CoseKey_Init(&wrongKey); + (void)wc_CoseKey_SetLms(&wrongKey, &lmsWrong); + wrongRet = wc_CoseSign1_Verify(&wrongKey, out, outLen, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(wrongRet != 0, "sign1 lms wrong key fails"); + } + else { + TEST_ASSERT(0, "lms wrong keygen"); + } + /* Free on both paths: a failed MakeKey still leaves an inited key. */ + wc_LmsKey_Free(&lmsWrong); + } + + /* An out too small for the encoded COSE_Sign1 must be rejected before + * signing so the one-time state is not spent; a retry would otherwise + * burn another signature. Scratch stays adequate to isolate the out + * check from the scratch check. */ + if (ret == 0) { + uint8_t smallOut[16]; + size_t smallLen = sizeof(smallOut); + int smallRet; + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + smallRet = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, + NULL, 0, payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), smallOut, sizeof(smallOut), + &smallLen, &rng); + TEST_ASSERT(smallRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "sign1 lms small out rejected"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign1 lms small out preserves state"); + } + + /* A failed persistence write leaves in-memory state possibly ahead of + * storage: the underlying key is quarantined, so no attachment of it can + * sign again, including a reattached wrapper or a second one sharing it. */ + if (ret == 0) { + LmsKey lmsFail; + WOLFCOSE_KEY failKey; + WOLFCOSE_KEY sharedKey; + size_t badEncSz = 0; + int fRet = test_lms_make_key(&lmsFail, g_lmsPrivB, &rng); + if (fRet == 0) { + (void)wc_CoseKey_Init(&failKey); + (void)wc_CoseKey_SetLms(&failKey, &lmsFail); + /* A second wrapper attached while the key is still good. */ + (void)wc_CoseKey_Init(&sharedKey); + (void)wc_CoseKey_SetLms(&sharedKey, &lmsFail); + + g_lmsWriteFail = 1; + fRet = wc_CoseSign1_Sign(&failKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + g_lmsWriteFail = 0; + TEST_ASSERT(fRet == WOLFCOSE_E_CRYPTO, + "sign1 lms write failure reported"); + + /* Same wrapper: refused early. */ + fRet = wc_CoseSign1_Sign(&failKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(fRet == WOLFCOSE_E_COSE_KEY_TYPE, + "sign1 lms refuses retry after write failure"); + + /* Reattaching the quarantined key does not revive it. */ + (void)wc_CoseKey_SetLms(&failKey, &lmsFail); + fRet = wc_CoseSign1_Sign(&failKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(fRet == WOLFCOSE_E_COSE_KEY_TYPE, + "sign1 lms refuses reattached quarantined key"); + + /* The second wrapper sharing the key is refused too. */ + fRet = wc_CoseSign1_Sign(&sharedKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(fRet != 0, + "sign1 lms refuses shared key second wrapper"); + + /* Quarantine stops signing but not public-key export. */ + fRet = wc_CoseKey_EncodeSize(&failKey, &badEncSz); + TEST_ASSERT(fRet == 0 && badEncSz > 0, + "sign1 lms bad key still exports pub"); + } + else { + TEST_ASSERT(0, "lms fail keygen"); + } + wc_LmsKey_Free(&lmsFail); + } + + /* An exhausted key is refused before the backend runs, wolfSSL's own + * exhausted state is left intact (not turned into a bad key), and the + * public key still encodes. */ + if (ret == 0) { + LmsKey lmsSpent; + WOLFCOSE_KEY spentKey; + uint8_t sig[2048]; + const uint8_t msg[] = "x"; + size_t encLen = 0; + word32 sl; + int n; + int sRet = test_lms_make_key(&lmsSpent, g_lmsPrivB, &rng); + for (n = 0; (sRet == 0) && (n < 32); n++) { + sl = (word32)sizeof(sig); + sRet = wc_LmsKey_Sign(&lmsSpent, sig, &sl, msg, (int)sizeof(msg)); + } + if (sRet == 0) { + (void)wc_CoseKey_Init(&spentKey); + (void)wc_CoseKey_SetLms(&spentKey, &lmsSpent); + sRet = wc_CoseSign1_Sign(&spentKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(sRet == WOLFCOSE_E_COSE_KEY_TYPE, + "sign1 lms exhausted key refused"); + sRet = wc_CoseKey_EncodeSize(&spentKey, &encLen); + TEST_ASSERT(sRet == 0 && encLen > 0, + "sign1 lms exhausted key still exports pub"); + } + else { + TEST_ASSERT(0, "lms exhaust keygen or sign"); + } + wc_LmsKey_Free(&lmsSpent); + } + + if (lmsInited != 0) { + wc_LmsKey_Free(&lmsKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} + +/* L4_H5_W4 (RFC 8778): a 9564-byte signature, larger than the old 8192-byte + * default, so this exercises the buffer sizing for a standard multi-level W4 + * set end to end. Keygen builds four H5 subtrees and stays quick. */ +static byte g_lmsPrivL4[HSS_MAX_PRIVATE_KEY_LEN]; + +static void test_cose_sign1_lms_l4(void) +{ + WOLFCOSE_KEY signKey; + LmsKey lmsKey; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int lmsInited = 0; + uint8_t payload[] = "HSS-LMS L4 payload"; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t out[WOLFCOSE_MAX_SCRATCH_SZ]; + size_t outLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign1 HSS-LMS L4/W4]\n"); + + ret = wc_InitRng(&rng); + if (ret != 0) { TEST_ASSERT(0, "rng init"); } + if (ret == 0) { rngInited = 1; } + + if (ret == 0) { + ret = wc_LmsKey_Init(&lmsKey, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_LmsKey_SetLmsParm(&lmsKey, WC_LMS_PARM_L4_H5_W4); + } + if (ret == 0) { + ret = wc_LmsKey_SetWriteCb(&lmsKey, test_lms_write_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetReadCb(&lmsKey, test_lms_read_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetContext(&lmsKey, g_lmsPrivL4); + } + if (ret == 0) { + ret = wc_LmsKey_MakeKey(&lmsKey, &rng); + } + if (ret != 0) { TEST_ASSERT(0, "lms l4 keygen"); } + if (ret == 0) { lmsInited = 1; } + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&signKey); + (void)wc_CoseKey_SetLms(&signKey, &lmsKey); + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0 && outLen > 8192u, "sign1 lms l4 sign"); + } + + if (ret == 0) { + ret = wc_CoseSign1_Verify(&signKey, out, outLen, NULL, 0, NULL, 0, + scratch, sizeof(scratch), &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(ret == 0, "sign1 lms l4 verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + memcmp(decPayload, payload, decPayloadLen) == 0, + "sign1 lms l4 payload match"); + } + + if (lmsInited != 0) { wc_LmsKey_Free(&lmsKey); } + if (rngInited != 0) { (void)wc_FreeRng(&rng); } +} + +#if defined(WOLFCOSE_KEY_ENCODE) && defined(WOLFCOSE_KEY_DECODE) +static void test_cose_key_lms(void) +{ + WOLFCOSE_KEY key; + WOLFCOSE_KEY decKey; + LmsKey lmsKey; + LmsKey lmsPub; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int lmsInited = 0; + int lmsPubInited = 0; + uint8_t keyBuf[256]; + size_t keyLen = 0; + size_t sizedLen = 0; + uint8_t pubRaw[HSS_MAX_PUBLIC_KEY_LEN]; + word32 pubRawLen = (word32)sizeof(pubRaw); + uint8_t badBuf[256]; + WOLFCOSE_CBOR_CTX enc; + + TEST_LOG(" [Key HSS-LMS]\n"); + + ret = wc_InitRng(&rng); + if (ret != 0) { TEST_ASSERT(0, "rng init"); } + if (ret == 0) { rngInited = 1; } + + if (ret == 0) { + ret = test_lms_make_key(&lmsKey, g_lmsPrivA, &rng); + if (ret != 0) { TEST_ASSERT(0, "lms keygen"); } + if (ret == 0) { lmsInited = 1; } + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&key); + (void)wc_CoseKey_SetLms(&key, &lmsKey); + + /* RFC 8778: {1: 5, 3: -46, -1: pub}; no private-key form exists. */ + ret = wc_CoseKey_EncodeSize(&key, &sizedLen); + TEST_ASSERT(ret == 0 && sizedLen > 0, "lms key encode size"); + ret = wc_CoseKey_Encode(&key, keyBuf, sizeof(keyBuf), &keyLen); + TEST_ASSERT(ret == 0 && keyLen > 0, "lms key encode"); + TEST_ASSERT(keyLen == sizedLen, "lms key encode exact size"); + } + + if (ret == 0) { + ret = wc_LmsKey_Init(&lmsPub, NULL, INVALID_DEVID); + if (ret == 0) { lmsPubInited = 1; } + TEST_ASSERT(ret == 0, "lms pub init"); + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&decKey); + (void)wc_CoseKey_SetLms(&decKey, &lmsPub); + ret = wc_CoseKey_Decode(&decKey, keyBuf, keyLen); + TEST_ASSERT(ret == 0, "lms key decode"); + TEST_ASSERT(decKey.kty == WOLFCOSE_KTY_HSS_LMS && + decKey.alg == WOLFCOSE_ALG_HSS_LMS && + decKey.hasPrivate == 0u, "lms key decode fields"); + } + + if (ret == 0) { + /* The decoded public key must match the original's export. */ + uint8_t pubRaw2[HSS_MAX_PUBLIC_KEY_LEN] = {0}; + word32 pubRaw2Len = (word32)sizeof(pubRaw2); + ret = wc_LmsKey_ExportPubRaw(&lmsKey, pubRaw, &pubRawLen); + if (ret == 0) { + ret = wc_LmsKey_ExportPubRaw(&lmsPub, pubRaw2, &pubRaw2Len); + } + TEST_ASSERT(ret == 0 && pubRawLen == pubRaw2Len && + memcmp(pubRaw, pubRaw2, (size_t)pubRawLen) == 0, + "lms key decode pub match"); + } + + if (ret == 0) { + /* Decoded verify-only key verifies a message from the original. */ + uint8_t scratch[4096]; + uint8_t out[4096]; + size_t outLen = 0; + const uint8_t payload[] = "lms pub-only verify"; + WOLFCOSE_HDR hdr; + const uint8_t* dec = NULL; + size_t decLen = 0; + + ret = wc_CoseSign1_Sign(&key, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0, "lms pub-only sign"); + if (ret == 0) { + ret = wc_CoseSign1_Verify(&decKey, out, outLen, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), &hdr, &dec, &decLen); + TEST_ASSERT(ret == 0, "lms pub-only verify"); + } + } + + if (ret == 0) { + /* Wrong alg label (ES256) for kty HSS-LMS is rejected. */ + int negRet; + enc.buf = badBuf; enc.cbuf = NULL; + enc.bufSz = sizeof(badBuf); enc.idx = 0; + (void)wc_CBOR_EncodeMapStart(&enc, 3); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY); + (void)wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_HSS_LMS); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_ALG); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_ALG_ES256); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_PUB); + (void)wc_CBOR_EncodeBstr(&enc, pubRaw, (size_t)pubRawLen); + negRet = wc_CoseKey_Decode(&decKey, badBuf, enc.idx); + TEST_ASSERT(negRet == WOLFCOSE_E_COSE_BAD_ALG, + "lms key decode bad alg rejected"); + + /* Missing pub(-1) member is rejected. */ + enc.idx = 0; + (void)wc_CBOR_EncodeMapStart(&enc, 1); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY); + (void)wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_HSS_LMS); + negRet = wc_CoseKey_Decode(&decKey, badBuf, enc.idx); + TEST_ASSERT(negRet == WOLFCOSE_E_COSE_BAD_HDR, + "lms key decode missing pub rejected"); + } + + if (lmsInited != 0) { wc_LmsKey_Free(&lmsKey); } + if (lmsPubInited != 0) { wc_LmsKey_Free(&lmsPub); } + if (rngInited != 0) { (void)wc_FreeRng(&rng); } +} + +/* Error and edge paths: NULL args, unattached keys, wrong parameter set, + * too-small scratch, and the decode attach-type mismatch. */ +static void test_cose_lms_negative(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY badKey; + LmsKey lmsKey; + LmsKey lmsWrongParm; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int lmsInited = 0; + int wrongInited = 0; + uint8_t payload[] = "lms negative"; + uint8_t scratch[4096]; + uint8_t out[4096]; + uint8_t out2[4096]; + uint8_t tiny[64]; + size_t outLen = 0; + size_t encLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [LMS negative]\n"); + + ret = wc_CoseKey_SetLms(NULL, NULL); + TEST_ASSERT(ret == WOLFCOSE_E_INVALID_ARG, "lms set NULL args"); + + ret = wc_InitRng(&rng); + if (ret != 0) { TEST_ASSERT(0, "rng init"); } + if (ret == 0) { rngInited = 1; } + + if (ret == 0) { + ret = test_lms_make_key(&lmsKey, g_lmsPrivA, &rng); + if (ret == 0) { lmsInited = 1; } + else { TEST_ASSERT(0, "lms keygen"); } + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&signKey); + (void)wc_CoseKey_SetLms(&signKey, &lmsKey); + outLen = sizeof(out); + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0, "lms sign"); + } + + /* Too-small scratch cannot hold the Sig_structure plus signature. + * Writes into out2 so the valid message in out is preserved. */ + if (ret == 0) { + size_t tinyOutLen = sizeof(out2); + int tinyRet = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, + NULL, 0, payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + tiny, sizeof(tiny), out2, sizeof(out2), &tinyOutLen, &rng); + TEST_ASSERT(tinyRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "lms sign small scratch"); + } + + /* Verify with an HSS-LMS key that has no attached LmsKey. */ + if (ret == 0) { + int badRet; + (void)wc_CoseKey_Init(&badKey); + badKey.kty = WOLFCOSE_KTY_HSS_LMS; + badKey.alg = WOLFCOSE_ALG_HSS_LMS; + badRet = wc_CoseSign1_Verify(&badKey, out, outLen, NULL, 0, NULL, 0, + scratch, sizeof(scratch), &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(badRet == WOLFCOSE_E_COSE_KEY_TYPE, + "lms verify unattached"); + } + + /* A kty 5 key whose union does not hold an LmsKey (attachedType not LMS) + * is rejected before key.lms is read, preventing type confusion. */ + if (ret == 0) { + int encRet; + badKey.attachedType = WOLFCOSE_ATT_NONE; + encLen = 0; + encRet = wc_CoseKey_EncodeSize(&badKey, &encLen); + TEST_ASSERT(encRet == WOLFCOSE_E_COSE_KEY_TYPE, + "lms encode size wrong attach"); + encLen = sizeof(out); + encRet = wc_CoseKey_Encode(&badKey, out, sizeof(out), &encLen); + TEST_ASSERT(encRet == WOLFCOSE_E_COSE_KEY_TYPE, + "lms encode wrong attach"); + } + + /* Encode and size of an HSS-LMS key with no attached LmsKey. */ + if (ret == 0) { + int encRet; + badKey.attachedType = WOLFCOSE_ATT_LMS; /* key.lms stays NULL */ + encLen = 0; + encRet = wc_CoseKey_EncodeSize(&badKey, &encLen); + TEST_ASSERT(encRet == WOLFCOSE_E_INVALID_ARG, "lms encode size null"); + encLen = sizeof(out); + encRet = wc_CoseKey_Encode(&badKey, out, sizeof(out), &encLen); + TEST_ASSERT(encRet == WOLFCOSE_E_INVALID_ARG, "lms encode null"); + } + + /* A kty 5 key whose alg contradicts HSS-LMS must not be encoded: it would + * emit a {1:5, 3:} the decoder rejects. */ + if (ret == 0) { + int32_t savedAlg = signKey.alg; + int badAlgRet; + signKey.alg = WOLFCOSE_ALG_ES256; + encLen = 0; + badAlgRet = wc_CoseKey_EncodeSize(&signKey, &encLen); + TEST_ASSERT(badAlgRet == WOLFCOSE_E_COSE_BAD_ALG, + "lms encode size bad alg"); + encLen = sizeof(out2); + badAlgRet = wc_CoseKey_Encode(&signKey, out2, sizeof(out2), &encLen); + TEST_ASSERT(badAlgRet == WOLFCOSE_E_COSE_BAD_ALG, "lms encode bad alg"); + signKey.alg = savedAlg; + } + + /* A parameter-only key (no MakeKey or import) holds no public bytes yet: + * size and encode must agree on rejecting it. */ + if (ret == 0) { + LmsKey lmsParm; + WOLFCOSE_KEY parmKey; + int pRet = wc_LmsKey_Init(&lmsParm, NULL, INVALID_DEVID); + if (pRet == 0) { + pRet = wc_LmsKey_SetLmsParm(&lmsParm, WC_LMS_PARM_L1_H5_W8); + } + if (pRet == 0) { + (void)wc_CoseKey_Init(&parmKey); + (void)wc_CoseKey_SetLms(&parmKey, &lmsParm); + encLen = 0; + pRet = wc_CoseKey_EncodeSize(&parmKey, &encLen); + TEST_ASSERT(pRet == WOLFCOSE_E_COSE_KEY_TYPE, + "lms encode size parameter-only key"); + encLen = sizeof(out2); + pRet = wc_CoseKey_Encode(&parmKey, out2, sizeof(out2), &encLen); + TEST_ASSERT(pRet == WOLFCOSE_E_COSE_KEY_TYPE, + "lms encode parameter-only key"); + } + else { + TEST_ASSERT(0, "lms parameter-only key init"); + } + wc_LmsKey_Free(&lmsParm); + } + + /* Decode a non-LMS COSE_Key into an LMS-attached key: the attach-type + * cross-check rejects the kty mismatch. */ + if (ret == 0) { + WOLFCOSE_KEY mkey; + WOLFCOSE_CBOR_CTX enc; + uint8_t symKey[16]; + int mRet; + (void)memset(symKey, 0x5a, sizeof(symKey)); + (void)wc_CoseKey_Init(&mkey); + (void)wc_CoseKey_SetLms(&mkey, &lmsKey); + enc.buf = out; enc.cbuf = NULL; enc.bufSz = sizeof(out); enc.idx = 0; + (void)wc_CBOR_EncodeMapStart(&enc, 2); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_KTY); + (void)wc_CBOR_EncodeUint(&enc, WOLFCOSE_KTY_SYMMETRIC); + (void)wc_CBOR_EncodeInt(&enc, WOLFCOSE_KEY_LABEL_K); + (void)wc_CBOR_EncodeBstr(&enc, symKey, sizeof(symKey)); + mRet = wc_CoseKey_Decode(&mkey, out, enc.idx); + TEST_ASSERT(mRet == WOLFCOSE_E_COSE_KEY_TYPE, "lms decode kty mismatch"); + } + + /* Verify a valid signature with a key of a different parameter set: the + * signature length no longer matches, so it is a crypto fault, not a + * plain signature mismatch. */ + if (ret == 0) { + outLen = sizeof(out); + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + } + if (ret == 0) { + ret = wc_LmsKey_Init(&lmsWrongParm, NULL, INVALID_DEVID); + if (ret == 0) { wrongInited = 1; } + if (ret == 0) { + ret = wc_LmsKey_SetLmsParm(&lmsWrongParm, WC_LMS_PARM_L1_H10_W8); + } + if (ret == 0) { + ret = wc_LmsKey_SetWriteCb(&lmsWrongParm, test_lms_write_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetReadCb(&lmsWrongParm, test_lms_read_cb); + } + if (ret == 0) { + ret = wc_LmsKey_SetContext(&lmsWrongParm, g_lmsPrivB); + } + if (ret == 0) { + ret = wc_LmsKey_MakeKey(&lmsWrongParm, &rng); + } + } + if (ret == 0) { + WOLFCOSE_KEY wrongParmKey; + int wpRet; + (void)wc_CoseKey_Init(&wrongParmKey); + (void)wc_CoseKey_SetLms(&wrongParmKey, &lmsWrongParm); + wpRet = wc_CoseSign1_Verify(&wrongParmKey, out, outLen, NULL, 0, + NULL, 0, scratch, sizeof(scratch), &hdr, &decPayload, + &decPayloadLen); + TEST_ASSERT(wpRet == WOLFCOSE_E_CRYPTO, "lms verify wrong params"); + } + + if (wrongInited != 0) { wc_LmsKey_Free(&lmsWrongParm); } + if (lmsInited != 0) { wc_LmsKey_Free(&lmsKey); } + if (rngInited != 0) { (void)wc_FreeRng(&rng); } +} +#endif /* WOLFCOSE_KEY_ENCODE && WOLFCOSE_KEY_DECODE */ + +#ifdef WOLFCOSE_SIGN +static void test_cose_sign_lms(void) +{ + WOLFCOSE_KEY signKey; + LmsKey lmsKey; + WOLFCOSE_SIGNATURE signers[1]; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int lmsInited = 0; + uint8_t out[4096]; + size_t outLen = 0; + uint8_t scratch[4096]; + const uint8_t payload[] = "lms multi-signer"; + WOLFCOSE_HDR hdr; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + + TEST_LOG(" [Sign multi-signer HSS-LMS]\n"); + + ret = wc_InitRng(&rng); + if (ret != 0) { TEST_ASSERT(0, "rng init"); } + if (ret == 0) { rngInited = 1; } + + if (ret == 0) { + ret = test_lms_make_key(&lmsKey, g_lmsPrivA, &rng); + if (ret != 0) { TEST_ASSERT(0, "lms keygen"); } + if (ret == 0) { lmsInited = 1; } + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&signKey); + (void)wc_CoseKey_SetLms(&signKey, &lmsKey); + + signers[0].algId = WOLFCOSE_ALG_HSS_LMS; + signers[0].key = &signKey; + signers[0].kid = NULL; + signers[0].kidLen = 0; + + ret = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), + out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0 && outLen > 0, "sign lms sign"); + } + + if (ret == 0) { + ret = wc_CoseSign_Verify(&signKey, 0, out, outLen, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(ret == 0, "sign lms verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + memcmp(decPayload, payload, decPayloadLen) == 0, + "sign lms payload match"); + } + + /* Too-small scratch cannot hold the Sig_structure plus signature. */ + if (ret == 0) { + uint8_t tiny[64]; + uint8_t smallOut[4096]; + size_t smallLen = sizeof(smallOut); + int smallRet = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + tiny, sizeof(tiny), smallOut, sizeof(smallOut), &smallLen, &rng); + TEST_ASSERT(smallRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "sign lms small scratch"); + } + + /* The output-capacity preflight must be exact and run before signing. A + * buffer one byte short of the earlier success is rejected with the + * stateful signer intact (an under-count would burn a signature here), and + * a buffer sized exactly to the message still signs (an over-count would + * wrongly reject it). LMS signatures are fixed length per parameter set, + * so the encoded length matches the earlier success. */ + if (ret == 0) { + uint8_t exactOut[4096]; + size_t tmpLen; + int shortRet; + int exactRet; + + tmpLen = sizeof(exactOut); + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + shortRet = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), exactOut, outLen - 1u, &tmpLen, &rng); + TEST_ASSERT(shortRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "sign lms short out rejected"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign lms short out preserves state"); + + tmpLen = 0; + exactRet = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), exactOut, outLen, &tmpLen, &rng); + TEST_ASSERT(exactRet == 0 && tmpLen == outLen, + "sign lms exact out signs"); + } + + /* Two HSS-LMS signers are refused outright: a second stateful signer + * failing could not give back the first one's spent leaf. The first + * key's persisted state must not move. */ + if (ret == 0) { + LmsKey lmsBig; + WOLFCOSE_KEY bigKey; + WOLFCOSE_SIGNATURE two[2]; + uint8_t bigOut[12288]; + size_t tmpLen = 0; + int bigRet = test_lms_make_key_ex(&lmsBig, g_lmsPrivL4, &rng, + WC_LMS_PARM_L4_H5_W4); + if (bigRet == 0) { + (void)wc_CoseKey_Init(&bigKey); + (void)wc_CoseKey_SetLms(&bigKey, &lmsBig); + two[0] = signers[0]; + two[1].algId = WOLFCOSE_ALG_HSS_LMS; + two[1].key = &bigKey; + two[1].kid = NULL; + two[1].kidLen = 0; + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + bigRet = wc_CoseSign_Sign(two, 2, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + bigOut, sizeof(bigOut), &tmpLen, &rng); + TEST_ASSERT(bigRet == WOLFCOSE_E_INVALID_ARG, + "sign lms second lms signer rejected"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign lms second lms signer preserves first state"); + } + else { + TEST_ASSERT(0, "lms big keygen"); + } + wc_LmsKey_Free(&lmsBig); + } + + /* Detached payload with a kid: the size preflight's nil-payload and kid + * arithmetic, exact and one byte short. */ + if (ret == 0) { + WOLFCOSE_SIGNATURE kidSigner[1]; + const uint8_t kid[] = { 0x6b, 0x31 }; + uint8_t dOut[4096]; + size_t tmpLen = 0; + int dRet; + + kidSigner[0] = signers[0]; + kidSigner[0].kid = kid; + kidSigner[0].kidLen = sizeof(kid); + dRet = wc_CoseSign_Sign(kidSigner, 1, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, + scratch, sizeof(scratch), dOut, sizeof(dOut), &tmpLen, &rng); + TEST_ASSERT(dRet == 0 && tmpLen > 0, "sign lms detached kid sign"); + if (dRet == 0) { + dRet = wc_CoseSign_Verify(&signKey, 0, dOut, tmpLen, + payload, sizeof(payload) - 1, NULL, 0, + scratch, sizeof(scratch), &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(dRet == 0, "sign lms detached kid verify"); + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + dRet = wc_CoseSign_Sign(kidSigner, 1, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, + scratch, sizeof(scratch), dOut, tmpLen - 1u, &tmpLen, &rng); + TEST_ASSERT(dRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "sign lms detached kid short out"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign lms detached kid preserves state"); + } + } + +#ifdef WOLFCOSE_HAVE_ES256 + /* An LMS signer that is not last is refused with its state intact; moved + * to the last slot beside a pre-hashing signer it signs, which also + * exercises the scratch preflight's reuse-the-structure sizing rule. */ + if (ret == 0) { + ecc_key eccKey; + WOLFCOSE_KEY esKey; + WOLFCOSE_SIGNATURE mixed[2]; + uint8_t mOut[4096]; + size_t tmpLen = 0; + int eccInited = 0; + int mRet = wc_ecc_init(&eccKey); + if (mRet == 0) { + eccInited = 1; + mRet = wc_ecc_make_key(&rng, 32, &eccKey); + } + if (mRet == 0) { + (void)wc_CoseKey_Init(&esKey); + (void)wc_CoseKey_SetEcc(&esKey, WOLFCOSE_CRV_P256, &eccKey); + mixed[0] = signers[0]; + mixed[1].algId = WOLFCOSE_ALG_ES256; + mixed[1].key = &esKey; + mixed[1].kid = NULL; + mixed[1].kidLen = 0; + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + mRet = wc_CoseSign_Sign(mixed, 2, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + mOut, sizeof(mOut), &tmpLen, &rng); + TEST_ASSERT(mRet == WOLFCOSE_E_INVALID_ARG, + "sign lms not last rejected"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign lms not last preserves state"); + mixed[1] = signers[0]; + mixed[0].algId = WOLFCOSE_ALG_ES256; + mixed[0].key = &esKey; + mixed[0].kid = NULL; + mixed[0].kidLen = 0; + mRet = wc_CoseSign_Sign(mixed, 2, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + mOut, sizeof(mOut), &tmpLen, &rng); + TEST_ASSERT(mRet == 0 && tmpLen > 0, + "sign lms last beside es256 signer"); + } + else { + TEST_ASSERT(0, "es256 keygen"); + } + if (eccInited != 0) { wc_ecc_free(&eccKey); } + } +#endif + + /* The same LmsKey in two slots is refused with state intact, and an + * exhausted key is refused before any signing. */ + if (ret == 0) { + LmsKey lmsEx; + WOLFCOSE_KEY exKey; + WOLFCOSE_SIGNATURE two[2]; + uint8_t exSig[2048]; + uint8_t exOut[4096]; + const uint8_t msg[] = "x"; + size_t tmpLen = 0; + word32 sl; + int n; + int eRet; + + two[0] = signers[0]; + two[1] = signers[0]; + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + eRet = wc_CoseSign_Sign(two, 2, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + exOut, sizeof(exOut), &tmpLen, &rng); + TEST_ASSERT(eRet == WOLFCOSE_E_INVALID_ARG, + "sign lms duplicate key rejected"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign lms duplicate key preserves state"); + + /* L1/H5 has 32 one-time signatures; spend them all. */ + eRet = test_lms_make_key(&lmsEx, g_lmsPrivB, &rng); + for (n = 0; (eRet == 0) && (n < 32); n++) { + sl = (word32)sizeof(exSig); + eRet = wc_LmsKey_Sign(&lmsEx, exSig, &sl, msg, (int)sizeof(msg)); + } + if (eRet == 0) { + (void)wc_CoseKey_Init(&exKey); + (void)wc_CoseKey_SetLms(&exKey, &lmsEx); + two[0].key = &exKey; + eRet = wc_CoseSign_Sign(two, 1, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + exOut, sizeof(exOut), &tmpLen, &rng); + TEST_ASSERT(eRet == WOLFCOSE_E_COSE_KEY_TYPE, + "sign lms exhausted key rejected"); + } + else { + TEST_ASSERT(0, "lms exhaust keygen or sign"); + } + wc_LmsKey_Free(&lmsEx); + } + + /* Verify with a same-parameter wrong key is a signature failure; a + * different-parameter key is a crypto fault (length mismatch). */ + if (ret == 0) { + LmsKey lmsWrong; + WOLFCOSE_KEY wrongKey; + int wrongRet = test_lms_make_key(&lmsWrong, g_lmsPrivB, &rng); + if (wrongRet == 0) { + (void)wc_CoseKey_Init(&wrongKey); + (void)wc_CoseKey_SetLms(&wrongKey, &lmsWrong); + wrongRet = wc_CoseSign_Verify(&wrongKey, 0, out, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(wrongRet == WOLFCOSE_E_COSE_SIG_FAIL, + "sign lms wrong key"); + wc_LmsKey_Free(&lmsWrong); + } + } + + if (lmsInited != 0) { wc_LmsKey_Free(&lmsKey); } + if (rngInited != 0) { (void)wc_FreeRng(&rng); } +} + +#if defined(WOLFCOSE_EXT_SIGN) +/* Delegated HSS-LMS signer that counts its invocations. Proves the output + * preflight rejects a short buffer before the stateful callback ever runs. */ +typedef struct { + LmsKey* key; + int calls; +} test_lms_ext_ctx; + +static int test_lms_ext_cb(void* cbCtx, int32_t alg, const uint8_t* tbs, + size_t tbsSz, uint8_t* sig, size_t sigSz, + size_t* sigLen) +{ + test_lms_ext_ctx* c = (test_lms_ext_ctx*)cbCtx; + word32 wlen = (word32)sigSz; + int ret; + (void)alg; + c->calls++; + ret = wc_LmsKey_Sign(c->key, sig, &wlen, tbs, (int)tbsSz); + if (ret == 0) { + *sigLen = (size_t)wlen; + } + return (ret == 0) ? 0 : -1; +} + +static void test_cose_lms_delegated(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + LmsKey lmsKey; + test_lms_ext_ctx cbCtx; + WOLFCOSE_SIGNATURE signers[1]; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int lmsInited = 0; + int shortRet; + int callsBefore; + uint8_t payload[] = "delegated LMS"; + uint8_t scratch[4096]; + uint8_t out[4096]; + size_t outLen = 0; + size_t sizedLen = 0; + WOLFCOSE_HDR hdr; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + + TEST_LOG(" [HSS-LMS delegated signer]\n"); + + ret = wc_InitRng(&rng); + if (ret != 0) { TEST_ASSERT(0, "rng init"); } + if (ret == 0) { rngInited = 1; } + + if (ret == 0) { + ret = test_lms_make_key(&lmsKey, g_lmsPrivA, &rng); + if (ret != 0) { TEST_ASSERT(0, "lms keygen"); } + if (ret == 0) { lmsInited = 1; } + } + + /* Attach the LmsKey (so the length is known) and a delegated signer. */ + if (ret == 0) { + cbCtx.key = &lmsKey; + cbCtx.calls = 0; + (void)wc_CoseKey_Init(&signKey); + (void)wc_CoseKey_SetLms(&signKey, &lmsKey); + ret = wc_CoseKey_SetExtSigner(&signKey, test_lms_ext_cb, &cbCtx); + TEST_ASSERT(ret == 0 && signKey.signCb != NULL, + "lms delegated set signer"); + } + + if (ret == 0) { + ret = wc_CoseSign1_SignSize_ex(&signKey, WOLFCOSE_ALG_HSS_LMS, 0u, + sizeof(payload) - 1u, 0u, 0u, &sizedLen); + TEST_ASSERT(ret == 0 && sizedLen > 0, "lms delegated size"); + } + + /* A one-byte-short out must be rejected before the callback runs. */ + if (ret == 0) { + callsBefore = cbCtx.calls; + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + outLen = sizeof(out); + shortRet = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizedLen - 1u, &outLen, &rng); + TEST_ASSERT(shortRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "lms delegated short out rejected"); + TEST_ASSERT(cbCtx.calls == callsBefore, + "lms delegated short out no callback"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "lms delegated short out preserves state"); + } + + /* An exact-size out signs once through the callback and verifies. */ + if (ret == 0) { + outLen = sizeof(out); + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizedLen, &outLen, &rng); + TEST_ASSERT(ret == 0 && outLen == sizedLen, + "lms delegated exact out signs"); + TEST_ASSERT(cbCtx.calls == 1, "lms delegated one callback"); + /* Proves the snapshot comparison is live: a real sign moves state. */ + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) != 0, + "lms delegated sign advances state"); + } + + if (ret == 0) { + (void)wc_CoseKey_Init(&verifyKey); + (void)wc_CoseKey_SetLms(&verifyKey, &lmsKey); + ret = wc_CoseSign1_Verify(&verifyKey, out, outLen, NULL, 0, NULL, 0, + scratch, sizeof(scratch), &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(ret == 0, "lms delegated verify"); + } + + /* Same guarantee for the multi-signer path. */ + if (ret == 0) { + cbCtx.calls = 0; + signers[0].algId = WOLFCOSE_ALG_HSS_LMS; + signers[0].key = &signKey; + signers[0].kid = NULL; + signers[0].kidLen = 0; + + outLen = sizeof(out); + ret = wc_CoseSign_Sign(signers, 1, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0 && cbCtx.calls == 1, "sign lms delegated sign"); + } + if (ret == 0) { + callsBefore = cbCtx.calls; + memcpy(g_lmsPrivSnap, g_lmsPrivA, sizeof(g_lmsPrivSnap)); + shortRet = wc_CoseSign_Sign(signers, 1, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + out, outLen - 1u, &outLen, &rng); + TEST_ASSERT(shortRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "sign lms delegated short out rejected"); + TEST_ASSERT(cbCtx.calls == callsBefore, + "sign lms delegated short out no callback"); + TEST_ASSERT(memcmp(g_lmsPrivSnap, g_lmsPrivA, + sizeof(g_lmsPrivSnap)) == 0, + "sign lms delegated short out preserves state"); + } + + if (lmsInited != 0) { wc_LmsKey_Free(&lmsKey); } + if (rngInited != 0) { (void)wc_FreeRng(&rng); } +} +#endif /* WOLFCOSE_EXT_SIGN */ +#endif /* WOLFCOSE_SIGN */ +#endif /* WOLFCOSE_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ + +#if defined(WOLFCOSE_HAVE_LMS) && defined(WOLFCOSE_EXT_SIGN) && \ + defined(WOLFCOSE_SIGN1_SIGN) +/* Delegated LMS with no local signing primitive at all, so this also links + * against a WOLFSSL_LMS_VERIFY_ONLY backend: a parameter-only public LmsKey + * supplies the length and a mock callback supplies fixed bytes. */ +typedef struct { + int calls; +} test_lms_mock_ctx; + +static int test_lms_mock_cb(void* cbCtx, int32_t alg, const uint8_t* tbs, + size_t tbsSz, uint8_t* sig, size_t sigSz, + size_t* sigLen) +{ + test_lms_mock_ctx* c = (test_lms_mock_ctx*)cbCtx; + (void)alg; + (void)tbs; + (void)tbsSz; + c->calls++; + memset(sig, 0xAA, sigSz); + *sigLen = sigSz; + return 0; +} + +static void test_cose_lms_delegated_mock(void) +{ + WOLFCOSE_KEY signKey; + LmsKey lmsKey; + test_lms_mock_ctx cbCtx; + int ret; + int lmsInited = 0; + int shortRet; + uint8_t payload[] = "delegated LMS mock"; + uint8_t scratch[4096]; + uint8_t out[4096]; + size_t outLen = 0; + size_t sizedLen = 0; + + TEST_LOG(" [HSS-LMS delegated mock signer]\n"); + + cbCtx.calls = 0; + ret = wc_LmsKey_Init(&lmsKey, NULL, INVALID_DEVID); + if (ret == 0) { lmsInited = 1; } + if (ret == 0) { + ret = wc_LmsKey_SetLmsParm(&lmsKey, WC_LMS_PARM_L1_H5_W8); + } + if (ret == 0) { + (void)wc_CoseKey_Init(&signKey); + (void)wc_CoseKey_SetLms(&signKey, &lmsKey); + ret = wc_CoseKey_SetExtSigner(&signKey, test_lms_mock_cb, &cbCtx); + } + if (ret != 0) { TEST_ASSERT(0, "lms mock setup"); } + + if (ret == 0) { + ret = wc_CoseSign1_SignSize_ex(&signKey, WOLFCOSE_ALG_HSS_LMS, 0u, + sizeof(payload) - 1u, 0u, 0u, &sizedLen); + TEST_ASSERT(ret == 0 && sizedLen > 0, "lms mock size"); + } + if (ret == 0) { + shortRet = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizedLen - 1u, &outLen, NULL); + TEST_ASSERT(shortRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "lms mock short out rejected"); + TEST_ASSERT(cbCtx.calls == 0, "lms mock short out no callback"); + } + if (ret == 0) { + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_HSS_LMS, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == 0 && outLen == sizedLen, "lms mock exact size"); + TEST_ASSERT(cbCtx.calls == 1, "lms mock one callback"); + } + + if (lmsInited != 0) { wc_LmsKey_Free(&lmsKey); } +} +#endif /* WOLFCOSE_HAVE_LMS && WOLFCOSE_EXT_SIGN && WOLFCOSE_SIGN1_SIGN */ + /* ----- COSE_Sign1 with external AAD ----- */ #ifdef WOLFCOSE_HAVE_ES256 static void test_cose_sign1_with_aad(void) @@ -20055,6 +21278,136 @@ static void test_force_failure_crypto(void) } #endif /* WOLFCOSE_HAVE_MLDSA */ +#if defined(WOLFCOSE_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) + /* empty-brace-scan: allow - test-local temporary scope */ + { + WOLFCOSE_KEY key; + LmsKey lmsKey; + uint8_t keyBuf[256]; + uint8_t lmsScratch[4096]; + uint8_t lmsCoseMsg[4096]; + size_t lmsCoseMsgLen; + size_t keyLen; + int lmsReady = 0; + + (void)wc_CoseKey_Init(&key); + ret = test_lms_make_key(&lmsKey, g_lmsPrivA, &rng); + if (ret == 0) { + lmsReady = 1; + (void)wc_CoseKey_SetLms(&key, &lmsKey); + + /* Test LMS export public failure */ + keyLen = sizeof(keyBuf); + wolfForceFailure_Set(WOLF_FAIL_LMS_EXPORT_PUB); + ret = wc_CoseKey_Encode(&key, keyBuf, sizeof(keyBuf), &keyLen); + TEST_ASSERT(ret == WOLFCOSE_E_CRYPTO, "LMS export pub forced failure"); + + /* Encode a valid COSE_Key for the import failure test */ + keyLen = sizeof(keyBuf); + ret = wc_CoseKey_Encode(&key, keyBuf, sizeof(keyBuf), &keyLen); + if (ret == 0) { + LmsKey lmsPub; + WOLFCOSE_KEY decKey; + if (wc_LmsKey_Init(&lmsPub, NULL, INVALID_DEVID) == 0) { + (void)wc_CoseKey_Init(&decKey); + (void)wc_CoseKey_SetLms(&decKey, &lmsPub); + /* Test LMS import public failure */ + wolfForceFailure_Set(WOLF_FAIL_LMS_IMPORT_PUB); + ret = wc_CoseKey_Decode(&decKey, keyBuf, keyLen); + TEST_ASSERT(ret == WOLFCOSE_E_CRYPTO, + "LMS import pub forced failure"); + wc_LmsKey_Free(&lmsPub); + } + } + + /* Test LMS sign failure */ + lmsCoseMsgLen = sizeof(lmsCoseMsg); + wolfForceFailure_Set(WOLF_FAIL_LMS_SIGN); + ret = wc_CoseSign1_Sign(&key, WOLFCOSE_ALG_HSS_LMS, + NULL, 0, payload, sizeof(payload), NULL, 0, NULL, 0, + lmsScratch, sizeof(lmsScratch), + lmsCoseMsg, sizeof(lmsCoseMsg), &lmsCoseMsgLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_E_CRYPTO, "LMS sign forced failure"); + + /* The injection did not run wc_LmsKey_Sign, so the real state never + * advanced; clear wolfCOSE's synthetic bad mark before signing. The + * sign below reads it through the key alias, which cppcheck cannot + * follow. */ + /* cppcheck-suppress redundantAssignment */ + lmsKey.state = WC_LMS_STATE_OK; + + /* Create a valid signature for the verify test */ + lmsCoseMsgLen = sizeof(lmsCoseMsg); + ret = wc_CoseSign1_Sign(&key, WOLFCOSE_ALG_HSS_LMS, + NULL, 0, payload, sizeof(payload), NULL, 0, NULL, 0, + lmsScratch, sizeof(lmsScratch), + lmsCoseMsg, sizeof(lmsCoseMsg), &lmsCoseMsgLen, &rng); + if (ret == 0) { + const uint8_t* decodedPayload; + size_t decodedPayloadLen; + WOLFCOSE_HDR hdr; + + /* Injected SIG_VERIFY_E maps to a signature failure */ + wolfForceFailure_Set(WOLF_FAIL_LMS_VERIFY); + ret = wc_CoseSign1_Verify(&key, lmsCoseMsg, lmsCoseMsgLen, + NULL, 0, NULL, 0, lmsScratch, sizeof(lmsScratch), + &hdr, &decodedPayload, &decodedPayloadLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_SIG_FAIL, + "LMS verify forced failure"); + } + +#ifdef WOLFCOSE_SIGN + /* Same injections through the multi-signer COSE_Sign paths. */ + /* empty-brace-scan: allow - test-local temporary scope */ + { + WOLFCOSE_SIGNATURE signers[1]; + const uint8_t* mPayload; + size_t mPayloadLen; + WOLFCOSE_HDR mHdr; + + signers[0].algId = WOLFCOSE_ALG_HSS_LMS; + signers[0].key = &key; + signers[0].kid = NULL; + signers[0].kidLen = 0; + + lmsCoseMsgLen = sizeof(lmsCoseMsg); + wolfForceFailure_Set(WOLF_FAIL_LMS_SIGN); + ret = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload), NULL, 0, NULL, 0, + lmsScratch, sizeof(lmsScratch), + lmsCoseMsg, sizeof(lmsCoseMsg), &lmsCoseMsgLen, &rng); + TEST_ASSERT(ret == WOLFCOSE_E_CRYPTO, + "LMS multi-signer sign forced failure"); + + /* Injection did not advance real state; clear the synthetic + * bad mark so the key signs (read below through the key alias, + * which cppcheck cannot follow). */ + /* cppcheck-suppress redundantAssignment */ + lmsKey.state = WC_LMS_STATE_OK; + lmsCoseMsgLen = sizeof(lmsCoseMsg); + ret = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload), NULL, 0, NULL, 0, + lmsScratch, sizeof(lmsScratch), + lmsCoseMsg, sizeof(lmsCoseMsg), &lmsCoseMsgLen, &rng); + if (ret == 0) { + wolfForceFailure_Set(WOLF_FAIL_LMS_VERIFY); + ret = wc_CoseSign_Verify(&key, 0, + lmsCoseMsg, lmsCoseMsgLen, NULL, 0, NULL, 0, + lmsScratch, sizeof(lmsScratch), + &mHdr, &mPayload, &mPayloadLen); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_SIG_FAIL, + "LMS multi-signer verify forced failure"); + } + } +#endif /* WOLFCOSE_SIGN */ + } + if (lmsReady != 0) { + wc_LmsKey_Free(&lmsKey); + } + wc_CoseKey_Free(&key); + } +#endif /* WOLFCOSE_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ + #ifdef WOLFCOSE_HAVE_AESCCM /* empty-brace-scan: allow - test-local temporary scope */ { @@ -24152,6 +25505,16 @@ int test_cose(void) test_cose_sign1_ml_dsa_level_mismatch(); #endif + /* HSS/LMS signature tests (RFC 8778) */ +#if defined(WOLFCOSE_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) + test_cose_sign1_lms(); + test_cose_sign1_lms_l4(); +#if defined(WOLFCOSE_KEY_ENCODE) && defined(WOLFCOSE_KEY_DECODE) + test_cose_key_lms(); + test_cose_lms_negative(); +#endif +#endif + /* Mac0 basic tests */ #if defined(WOLFCOSE_HAVE_HMAC256) test_cose_mac_wrong_tag_lengths(); @@ -24198,6 +25561,20 @@ int test_cose(void) test_rfc_mac0_hmac_01(); #endif +#if defined(WOLFCOSE_HAVE_LMS) && defined(WOLFCOSE_EXT_SIGN) && \ + defined(WOLFCOSE_SIGN1_SIGN) + test_cose_lms_delegated_mock(); +#endif + + /* Multi-signer HSS/LMS tests need no ES256, so they sit outside it. */ +#if defined(WOLFCOSE_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) && \ + defined(WOLFCOSE_SIGN) + test_cose_sign_lms(); +#if defined(WOLFCOSE_EXT_SIGN) + test_cose_lms_delegated(); +#endif +#endif + /* Multi-signer tests */ #if defined(WOLFCOSE_SIGN) && defined(WOLFCOSE_HAVE_ES256) test_cose_sign_multi_signer();