Add LMS Support - #459
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds verification-only LMS (RFC 8554 / NIST SP 800-208) support to wolfProvider for OpenSSL 3.6+, including raw XDR public key import/export, an XDR decoder, and CI/test coverage integration alongside existing PQC work (ML-KEM/ML-DSA).
Changes:
- Register LMS keymgmt, signature (verify-only), and XDR decoder implementations in the provider.
- Add LMS unit tests for import/export, XDR decoding, and unsupported operations.
- Extend build/test scripts and CI workflows to support LMS opt-in builds and LMS KAT vector runs.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit.h | Adds LMS unit test prototypes behind WP_HAVE_LMS. |
| test/unit.c | Registers LMS unit tests behind WP_HAVE_LMS. |
| test/test_lms.c | New LMS-focused unit tests (import/export, decode, unsupported ops). |
| test/include.am | Adds test/test_lms.c to unit test sources. |
| src/wp_wolfprov.c | Registers LMS in provider keymgmt/signature/decoder algorithm tables. |
| src/wp_lms.c | New LMS implementation: keymgmt import/export/match, verify-only signature, XDR decoder. |
| src/include.am | Adds src/wp_lms.c to library sources. |
| scripts/utils-wolfssl.sh | Adds LMS configure flags and mismatch checks for existing wolfSSL installs. |
| scripts/utils-wolfprovider.sh | Adds --enable-lms forwarding and avoids system wolfSSL when PQC/LMS requested. |
| scripts/utils-openssl.sh | Adds LMS mismatch check and enable-lms for OpenSSL builds when requested. |
| scripts/test-pqc-kat.sh | Extends KAT runner to optionally include LMS vectors and enforce expected sub-test counts. |
| scripts/build-wolfprovider.sh | Adds --enable-lms, version gating, and blocks LMS/PQC on FIPS builds and Debian packaging path. |
| README.md | Documents LMS opt-in usage and its verification-only contract/format expectations. |
| include/wolfprovider/settings.h | Adds compile-time gating for LMS availability and explicit error messages. |
| include/wolfprovider/alg_funcs.h | Declares LMS algorithm name and new dispatch tables. |
| docs/INTEGRATION_GUIDE.md | Documents --enable-lms and expands PQC section to include LMS. |
| configure.ac | Adds --enable-lms configure option and defines WOLFPROV_HAVE_LMS. |
| .github/workflows/wolfssl-versions-pqc.yml | Adds an LMS-only matrix row, adjusts eligibility logic, and separates cache variants. |
| .github/workflows/wolfssl-pqc-kat.yml | Adds LMS to the KAT matrix, caching, build flags, and runtime selection. |
| .github/workflows/nginx-pqc.yml | Updates PQC floor logic to v5.9.2-stable and adjusts eligibility checks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 18 total — 15 posted, 3 skipped
Posted findings
- [High] No unit test ever reaches wc_LmsKey_Verify - the entire verification path is untested outside the optional KAT job —
test/test_lms.c:321-378 - [Medium] wp_lms_dec_selection has the wrong first-parameter type for OSSL_FUNC_DECODER_DOES_SELECTION, and wp_lms_dec_decode still carries a dead (void)selection cast —
src/wp_lms.c:398-403,524 - [Medium] src/wp_lms.c has no WOLFPROV_ENTER/LEAVE tracing and no Doxygen comments, unlike every other provider source —
src/wp_lms.c:58-528 - [Medium] C99 compound literal in wp_lms_dec_decode - the only one in the whole src/ tree —
src/wp_lms.c:497-500 - [Medium] LMS keymgmt and signature dispatch tables omit GET_PARAMS/GETTABLE_PARAMS/DUP/DUPCTX, so EVP_PKEY_get_bits/size/security_bits return 0 and EVP_PKEY_dup fails —
src/wp_lms.c:267-281,366-373 - [Medium] wp_lms_verify_message_init silently discards the caller's OSSL_PARAM array —
src/wp_lms.c:308-327 - [Medium] wp_lms_import accepts any public-key length in [48,56] instead of deriving the exact length from the LMS type —
src/wp_lms.c:193-209 - [Medium] wp_lms_has() reports a public-only LMS key as a complete keypair, and the new test locks that in —
src/wp_lms.c:128-141 - [Low] Redundant pubLen checks in wp_lms_match —
src/wp_lms.c:152-163 - [Low] configure.ac summary line for LMS is misaligned —
configure.ac:272 - [Low] test-pqc-kat.sh replaced the named EXPECTED_TESTS constant with inline magic numbers —
scripts/test-pqc-kat.sh:72-81 - [Low] wp_lms_free() returns early on wc_LockMutex failure, leaking the key with no comment explaining the tradeoff —
src/wp_lms.c:92-100 - [Low] check_openssl_lms_mismatch reports a misleading cause when the install directory exists but headers do not —
scripts/utils-openssl.sh:403-415 - [Low] New system-wolfSSL guard omits WOLFPROV_PQC, so PQC/LMS builds can still fall back to the distro libwolfssl —
scripts/utils-wolfssl.sh:158-163 - [Info] No ChangeLog.md entry for the new LMS feature —
ChangeLog.md:10
Skipped findings
- [Medium] settings.h gates WP_HAVE_LMS on WP_WOLFSSL_PQC_CAPABLE, which is documented as the ML-DSA header marker
- [Low] Raw LMS type codes in wp_lms_xdr_pub_len have no named constants or spec reference
- [Low] wp_lms_new frees with OPENSSL_free on error paths while wp_lms_free uses OPENSSL_clear_free
Review generated by Skoll via Claude/Codex
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #459
Scan targets checked: wolfprovider-bugs, wolfprovider-src
Findings: 7
7 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 25 total — 7 posted, 18 skipped
Posted findings
- [High] test_lms.c lmsPub2 is a SHAKE256/192 key, but this commit narrows the build to --enable-lms=sha256-192, so its import assertion cannot pass —
test/test_lms.c:38-45,133-139 - [Medium] test_lms_unsupported_operations reuses one already-modified EVP_PKEY_CTX, pins return codes OpenSSL does not guarantee, and hosts a badSig check that belongs in the verify test —
test/test_lms.c:350-364 - [Medium] src/wp_lms.c still has no WOLFPROV_ENTER/LEAVE tracing and no doxygen comments, unlike every other provider source —
src/wp_lms.c:1-628 - [Medium] OSSL_PKEY_PARAM_PUB_KEY is not gettable, so raw-key support is import-only: EVP_PKEY_new_raw_public_key_ex works but EVP_PKEY_get_raw_public_key fails —
src/wp_lms.c:234-275 - [Medium] wp_lms_import accepts a KEYPAIR selection that the decoder rejects, so the two entry points disagree on what an LMS key can be —
src/wp_lms.c:185-187 - [High] wp_lms_get_params reports both BITS and SECURITY_BITS at values OpenSSL's own LMS keymgmt does not use - SECURITY_BITS comes out at half the real strength —
src/wp_lms.c:255-262 - [Low] LMS externs split the ML-KEM declaration group in alg_funcs.h and place a decoder in the keymgmt block —
include/wolfprovider/alg_funcs.h:435-437
Skipped findings
- [High] wp_lms_match() hard-fails any selection carrying the PRIVATE_KEY bit, diverging from wp_rsa_match/wp_mldsa_match
- [Medium] The dispatch functions added by this commit have no direct test coverage: dup/dupctx, get_params values, dec_export, verify NULL guards, match selection handling, and the SHA-256/256 key path
- [Medium] WP_HAVE_LMS is gated on WP_WOLFSSL_PQC_CAPABLE, a marker defined entirely in terms of ML-DSA
- [Medium] wolfSSL is built with full LMS rather than verify-only, compiling in signing and key-generation code wolfProvider can never reach
- [Medium] run_pqc_kat's new infrastructure-error paths are inverted into a green build on force-fail rows, and WOLFPROV_FORCE_FAIL is expanded bare next to default-safe expansions
- [Medium] Vector handling in test-pqc-kat.sh is inconsistent: EXPECTED_LMS_TESTS is a hardcoded strict-equality count against a dynamically resolved OpenSSL, and only LMS gets an existence check
- [Medium] The FIPS rejection message names only "PQC" while silently broadening the rejection to --enable-fips --enable-pqc
- [Low] --enable-lms help text omits the wolfSSL v5.9.2+/master floor the version gate enforces
- [Low] utils-wolfssl.sh nits: check_wolfssl_feature blames the feature when options.h is missing entirely, and WOLFSSL_PQC_CONFIG_OPTS is a no-op indirection
- [Low] OpenSSL cache separation for LMS rides on the variant string, but the adjacent extra_key reads as though it did the job
- [Low] test_lms.c style nits: braceless single-statement if bodies and two key-import failures with no diagnostic message
- [Low] Two brittle negative-path cases: badPubLen's size is coupled to sizeof(lmsPub3), and the decoder's under-4-byte header path is uncovered
- [Low] XDR decoder readability: WP_LMS_LEVELS_SZ is reused to size the lms_type field, and wp_lms_xdr_pub_len encodes bare LMS registry ranges with no explanatory comment
- [Low] wp_lms_verify_message_init() still silently discards its params array
- [Info] Unrelated trailing-newline deletions add churn to six files
- [Info] wp_lms_free() keeps the key object when the mutex lock fails (scan re-raise: this is the agreed wp_mldsa_free behaviour, not a defect)
- [Info] A failed re-import leaves wp_Lms holding a stale public key that no longer matches the wolfCrypt key
- [Info] The XDR decoder reports 'not my format' when wolfCrypt import genuinely fails, hiding real errors from OpenSSL's decoder chain
Review generated by Skoll via Claude/Codex
| 0x96, 0x61, 0x83, 0x14, 0x50, 0x8b, 0x12, 0xd2 | ||
| }; | ||
|
|
||
| static const unsigned char lmsPub2[] = { |
There was a problem hiding this comment.
🟠 [High] test_lms.c lmsPub2 is a SHAKE256/192 key, but this commit narrows the build to --enable-lms=sha256-192, so its import assertion cannot pass
🚫 BLOCK bug
Sites: test/test_lms.c:38-45,133-139
lmsPub2 declares LMS type 0x00000014 (20) and LM-OTS type 0x00000010 (16). Verified in wolfSSL: wc_lms.h:587 defines LMS_SHAKE_M24_H5 (0x14 | LMS_SHAKE256_192) and wc_lms.h:604 defines LMOTS_SHAKE_N24_W8 (0x10 | LMS_SHAKE256_192) - a SHAKE256/192 parameter set. The only wc_lms_map entry with that combination is WC_LMS_PARM_SHAKE192_L1_H5_W8, which sits inside #ifdef WOLFSSL_LMS_SHAKE256 (wolfcrypt/src/wc_lms.c:449-599 on master; identical line range confirmed on the v5.9.2-stable tag CI pins). This PR builds wolfSSL with --enable-lms=sha256-192 (scripts/utils-wolfssl.sh:66, the only path that sets WOLFPROV_LMS), and wolfSSL configure.ac:2403-2405 maps sha256-192 to -DWOLFSSL_LMS_SHA256_192 only. grep -n 'DWOLFSSL_LMS_SHAKE256' configure.ac returns exactly two sites: line 2407 (requires the explicit shake256 sub-option, never passed) and line 7200 (the FIPS-v7 block, which build-wolfprovider.sh:229 now hard-rejects for LMS). So WOLFSSL_LMS_SHAKE256 is never defined in any wolfProvider build. Consequently wc_LmsKey_ImportPubRaw's auto-derive loop (wc_lms.c:1799-1808) finds no match and returns NOT_COMPILED_IN, wp_lms_import fails at src/wp_lms.c:207, and test_lms_import_export fails with 'LMS alternate public key import failed'. This breaks the two new LMS-only rows added by this PR (master (--enable-lms only) and v5.9.2-stable (--enable-lms only) in wolfssl-versions-pqc.yml) plus every combined row that runs the unit tests. The sibling vectors are fine: lmsPub1 (type 10 / OTS 8) is SHA-256/192 and lmsPub3 (type 5 / OTS 1) is SHA-256/256, which stays compiled because sha256-192 does not imply WOLFSSL_NO_LMS_SHA256_256. Note the corollary: check_wolfssl_feature_mismatch (scripts/utils-wolfssl.sh:147-153) checks WOLFSSL_HAVE_LMS and WOLFSSL_LMS_SHA256_192 but not WOLFSSL_LMS_SHAKE256, so a developer with a stale wolfSSL install built from --enable-all-quantum-crypto or FIPS-v7 flags would see this test pass locally while clean CI fails - which likely explains the PR description's 'full Linux suite passed' claim.
Suggestion:
| static const unsigned char lmsPub2[] = { | |
| /* Replace lmsPub2 with a SHA-256/256 vector (type 5 / OTS 1), which the | |
| * sha256-192 build does compile, and which still differs from lmsPub1 in | |
| * both parameter set and body so the EVP_PKEY_eq / parameters_eq | |
| * assertions keep their meaning. */ |
Recommendation: Replace lmsPub2 with a parameter set the shipped build compiles - either a second SHA-256/192 type-10 key with different I/T1, or the existing type-5 lmsPub3 bytes. If SHAKE support is genuinely intended, change scripts/utils-wolfssl.sh to --enable-lms=sha256-192,shake256 and add WOLFSSL_LMS_SHAKE256 to check_wolfssl_feature_mismatch so the parameter-set requirement is enforced rather than assumed.
| err = lms_from_data(lmsPub1, sizeof(lmsPub1), EVP_PKEY_PUBLIC_KEY, &key); | ||
| if (err == 0) { | ||
| signature = EVP_SIGNATURE_fetch(wpLibCtx, "LMS", NULL); | ||
| ctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, key, NULL); |
There was a problem hiding this comment.
🟡 [Medium] test_lms_unsupported_operations reuses one already-modified EVP_PKEY_CTX, pins return codes OpenSSL does not guarantee, and hosts a badSig check that belongs in the verify test
💡 SUGGEST test
Sites: test/test_lms.c:350-364; test/test_lms.c:360-381; test/test_lms.c:344,356-359
The same ctx is driven through verify_message_init, then EVP_PKEY_verify, then EVP_PKEY_verify_init, then EVP_PKEY_sign_message_init. Each *_init tears down the previous operation state before deciding whether the new operation is supported, so by the time sign_message_init runs the failed verify_init has already reset ctx->operation. A failure from the last probe cannot be attributed to 'LMS has no sign_message_init' rather than 'the context is in an undefined-operation state' - the assertions are not independent, and a regression breaking verify_message_init would still leave every downstream negative check passing.
Also, at test/test_lms.c:360-381 (Low, review scan) — Unsupported-operation probes pin exact return codes (-2 and 0) that OpenSSL does not guarantee:
Six assertions require an exact numeric return: EVP_PKEY_verify_init == -2, EVP_PKEY_sign_message_init == -2, EVP_PKEY_keygen_init == -2, EVP_PKEY_paramgen_init == -2, EVP_DigestVerifyInit_ex == 0 and EVP_DigestSignInit_ex == 0. -2 is OpenSSL's convention for 'operation not supported for this key type', but these entry points can also return 0 or -1 for the same underlying condition depending on where the failure is detected, and EVP_DigestVerifyInit_ex is documented as returning '1 on success, 0 or a negative value on failure' - a negative return would fail the test even though it is a correct rejection.
Also, at test/test_lms.c:344,356-359 (Low, review scan) — The 1-byte badSig EVP_PKEY_verify check tests almost nothing and sits in the wrong test:
badSig is a single zero byte. wp_lms_verify (src/wp_lms.c:431-437) rejects it at the sigLen != expectedRawSigLen - WP_LMS_LEVELS_SZ gate before allocating anything or touching wolfCrypt, so no cryptographic path runs. The assertion is only == 1, so it also passes if the context is broken, if the key never loaded, or on an unrelated error. It also sits in the unsupported-operations test rather than test_lms_verify, where the meaningful negative cases live.
Recommendation: Create a fresh EVP_PKEY_CTX_new_from_pkey for each unsupported-operation probe, as the test already does for genCtx and each EVP_MD_CTX.
[test/test_lms.c:360-381] Assert 'not success' rather than a specific code; if pinning -2 is deliberate, comment that it is an OpenSSL-internal convention.
[test/test_lms.c:344,356-359] Move the wrong-length rejection into test_lms_verify and test sizeof(lmsVerifySig) -/+ 1 so the WP_LMS_LEVELS_SZ boundary is pinned.
| @@ -0,0 +1,628 @@ | |||
| /* wp_lms.c | |||
There was a problem hiding this comment.
🟡 [Medium] src/wp_lms.c still has no WOLFPROV_ENTER/LEAVE tracing and no doxygen comments, unlike every other provider source
💡 SUGGEST convention
Verified by grep: wp_lms.c contains 0 occurrences of WOLFPROV_ENTER/WOLFPROV_LEAVE, while wp_mldsa_kmgmt.c has 42, wp_mldsa_sig.c 54, wp_ecx_kmgmt.c 52, and wp_mlkem_kmgmt.c 25. WP_LOG_COMP_PQC already exists (wp_logging.h:190) and is what the sibling PQC files use. wolfProvider's entire runtime debugging story - WOLFPROV_DEBUG, the component filter, WOLFPROV_LOG_COMPONENTS_FILTER - is built on these macros, so LMS failures will be silent while every neighbouring algorithm is traceable. The file also carries no doxygen function headers, while every sibling documents each function with @param/@return blocks. This is a new 628-line file, so the divergence is entirely introduced here.
Suggestion:
| /* wp_lms.c | |
| /** | |
| * Verify an LMS signature over a message. | |
| * | |
| * @param [in] ctx LMS signature context. | |
| * @param [in] sig Signature (OpenSSL format, no HSS header). | |
| * @param [in] sigLen Length of signature in bytes. | |
| * @param [in] msg Message that was signed. | |
| * @param [in] msgLen Length of message in bytes. | |
| * @return 1 on success, 0 on failure. | |
| */ | |
| static int wp_lms_verify(...) | |
| { | |
| WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_lms_verify"); | |
| ... | |
| WOLFPROV_LEAVE(WP_LOG_COMP_PQC, | |
| __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); | |
| return ok; | |
| } |
Recommendation: Add WOLFPROV_ENTER/LEAVE with WP_LOG_COMP_PQC and doxygen headers to each function, matching wp_mldsa_kmgmt.c.
| return wp_lms_import_types(selection); | ||
| } | ||
|
|
||
| static const OSSL_PARAM* wp_lms_gettable_params(WOLFPROV_CTX* provCtx) |
There was a problem hiding this comment.
🟡 [Medium] OSSL_PKEY_PARAM_PUB_KEY is not gettable, so raw-key support is import-only: EVP_PKEY_new_raw_public_key_ex works but EVP_PKEY_get_raw_public_key fails
💡 SUGGEST api
wp_lms_gettable_params advertises only BITS, SECURITY_BITS and MAX_SIZE, and wp_lms_get_params handles only those three. Neither OSSL_PKEY_PARAM_PUB_KEY nor OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY is served, so EVP_PKEY_get_raw_public_key() and EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, ...) both fail - even though the PR explicitly supports and tests the counterpart EVP_PKEY_new_raw_public_key_ex() (test/test_lms.c:224-230) and README.md states wolfProvider 'imports and exports raw XDR public keys'. Export currently works only via the keymgmt export callback (EVP_PKEY_todata). Every sibling keymgmt exposes the param: wp_mldsa_kmgmt.c:780 and wp_ecx_kmgmt.c:498-500 list both PUB_KEY and ENCODED_PUBLIC_KEY.
Suggestion:
| static const OSSL_PARAM* wp_lms_gettable_params(WOLFPROV_CTX* provCtx) | |
| OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), | |
| OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, NULL, 0), | |
| /* plus matching OSSL_PARAM_set_octet_string(p, lms->pub, lms->pubLen) | |
| * handling in wp_lms_get_params */ |
Recommendation: Serve OSSL_PKEY_PARAM_PUB_KEY (and ENCODED_PUBLIC_KEY) in both gettable_params and get_params, and add an EVP_PKEY_get_raw_public_key round-trip to test_lms_import_export.
| size_t pubLen = 0; | ||
| unsigned char raw[WP_LMS_LEVELS_SZ + WP_LMS_XDR_MAX_SZ]; | ||
|
|
||
| if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) == 0)) { |
There was a problem hiding this comment.
🟡 [Medium] wp_lms_import accepts a KEYPAIR selection that the decoder rejects, so the two entry points disagree on what an LMS key can be
💡 SUGGEST bug
wp_lms_import only rejects a selection that lacks OSSL_KEYMGMT_SELECT_PUBLIC_KEY. A caller passing EVP_PKEY_KEYPAIR (PUBLIC|PRIVATE) therefore succeeds, silently importing only the public half and yielding an EVP_PKEY that wp_lms_has(KEYPAIR) then reports as incomplete. The decoder path is stricter: wp_lms_selection_ok (lines 490-495) rejects any selection containing the private bit, and test_lms_decode covers exactly that (LMS XDR decoder accepted keypair selection). The two entry points into the same key object disagree, and the fromdata side is untested. For a key type that can never hold a private component, import should refuse a request that names one.
Also raised by the bugs scan (src/wp_lms.c:185-187):
wp_lms_import() only requires the OSSL_KEYMGMT_SELECT_PUBLIC_KEY bit to be present; it never rejects a selection that also asks for the private key. EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) with only OSSL_PKEY_PARAM_PUB_KEY in params therefore succeeds and hands back an EVP_PKEY that claims to be a keypair. The very next call, wp_lms_has(key, OSSL_KEYMGMT_SELECT_KEYPAIR), returns 0 (src/wp_lms.c:137-139), so the object is self-inconsistent. This is also inconsistent with the decoder path, which the PR deliberately makes reject keypair selections (wp_lms_selection_ok(), src/wp_lms.c:490-495) and which test/test_lms.c:266-272 asserts. Triggering input: EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, pub_only_params) — currently returns 1 where the decoder equivalent returns 0.
Suggestion:
| if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) == 0)) { | |
| if (ok && !wp_lms_selection_ok(selection)) { | |
| ok = 0; | |
| } | |
| if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) == 0)) { | |
| ok = 0; | |
| } |
Recommendation: Reuse wp_lms_selection_ok in wp_lms_import so both paths agree, and add a negative fromdata test with EVP_PKEY_KEYPAIR mirroring the existing decoder assertion.
| int bits; | ||
|
|
||
| if (ok) { | ||
| bits = (lms->pubLen == WP_LMS_XDR_MIN_SZ) ? 192 : 256; |
There was a problem hiding this comment.
🟠 [High] wp_lms_get_params reports both BITS and SECURITY_BITS at values OpenSSL's own LMS keymgmt does not use - SECURITY_BITS comes out at half the real strength
🚫 BLOCK question
Verified against OpenSSL master. providers/implementations/keymgmt/lms_kmgmt.c sets the three params as:
OSSL_PARAM_set_size_t(p.bits, 8 * ossl_lms_key_get_pub_len(key));
OSSL_PARAM_set_size_t(p.secbits, ossl_lms_key_get_collision_strength_bits(key));
OSSL_PARAM_set_size_t(p.maxsize, ossl_lms_key_get_sig_len(key));and crypto/lms/lms_key.c defines the helpers as pub_len = 24 + n, collision_strength_bits = n * 8, sig_len = 12 + n * (1 + p + h).
Against bits = (lms->pubLen == WP_LMS_XDR_MIN_SZ) ? 192 : 256; followed by OSSL_PARAM_set_int(p, bits) and OSSL_PARAM_set_int(p, bits / 2):
| Key | n | BITS OpenSSL |
BITS here |
SECURITY_BITS OpenSSL |
SECURITY_BITS here |
|---|---|---|---|---|---|
| SHA-256/192, 48-byte XDR | 24 | 384 | 192 | 192 | 96 |
| SHA-256/256, 56-byte XDR | 32 | 448 | 256 | 256 | 128 |
Both diverge. Note the coincidence that makes this easy to miss: the local bits value (192/256) is already exactly what OpenSSL reports for SECURITY_BITS, so the / 2 is what halves it, and BITS is separately short by the 24-byte XDR prefix.
SECURITY_BITS is the one with a consequence beyond cosmetics: OpenSSL gates certificates and TLS on it. Security level 2 requires 112 bits, so at 96 an LMS SHA-256/192 key is rejected wherever wolfProvider is loaded, while the same key under OpenSSL's own provider rates 192 and passes up to level 3.
MAX_SIZE is correct as written and needs no change: wc_LmsKey_GetSigLen() returns the HSS-framed length for a single level, so subtracting WP_LMS_LEVELS_SZ yields 12 + n * (1 + p + h) - the same value OpenSSL reports.
Suggestion:
| bits = (lms->pubLen == WP_LMS_XDR_MIN_SZ) ? 192 : 256; | |
| /* LMS strength is the hash second-preimage resistance (8n), not 4n. */ | |
| if ((p != NULL) && !OSSL_PARAM_set_int(p, bits)) { | |
| ok = 0; | |
| } |
Recommendation: Report BITS as 8 * lms->pubLen and SECURITY_BITS as the existing bits value, dropping the / 2:
p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS);
if ((p != NULL) && !OSSL_PARAM_set_int(p, (int)(8 * lms->pubLen))) {
ok = 0;
}
p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_BITS);
if ((p != NULL) && !OSSL_PARAM_set_int(p, bits)) {
ok = 0;
}A test asserting the exact numbers from the table above would keep this pinned - the current test only checks these are non-zero, which is why the halving passed.
| extern const OSSL_DISPATCH wp_cmac_keymgmt_functions[]; | ||
| extern const OSSL_DISPATCH wp_kdf_keymgmt_functions[]; | ||
| extern const OSSL_DISPATCH wp_mlkem512_keymgmt_functions[]; | ||
| extern const OSSL_DISPATCH wp_lms_keymgmt_functions[]; |
There was a problem hiding this comment.
🔵 [Low] LMS externs split the ML-KEM declaration group in alg_funcs.h and place a decoder in the keymgmt block
🔧 NIT style
wp_lms_keymgmt_functions and wp_lms_xdr_decoder_functions are inserted between wp_mlkem512_keymgmt_functions and wp_mlkem768_keymgmt_functions, splitting the 512/768/1024 group. The decoder declaration also lands in the keymgmt section, while every other decoder is declared further down in the dedicated decoder block.
Suggestion:
| extern const OSSL_DISPATCH wp_lms_keymgmt_functions[]; | |
| extern const OSSL_DISPATCH wp_mlkem512_keymgmt_functions[]; | |
| extern const OSSL_DISPATCH wp_mlkem768_keymgmt_functions[]; | |
| extern const OSSL_DISPATCH wp_mlkem1024_keymgmt_functions[]; | |
| ... | |
| extern const OSSL_DISPATCH wp_lms_keymgmt_functions[]; | |
| /* and wp_lms_xdr_decoder_functions with the other decoders */ |
Recommendation: Move the LMS keymgmt extern after the ML-KEM group and the decoder extern into the decoder block.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #459
Scan targets checked: wolfprovider-bugs, wolfprovider-src
Findings: 6
6 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
| if (err == 0) { | ||
| keyDup = EVP_PKEY_dup(key1); | ||
| err = (keyDup == NULL) || (EVP_PKEY_eq(key1, keyDup) != 1) || | ||
| (EVP_PKEY_get_bits(key1) == 0) || |
There was a problem hiding this comment.
🔵 [Low] Key metadata assertions only check for non-zero, not the computed values · Weak or missing assertions
The only coverage of the new wp_lms_get_params() asserts that bits, security bits and size are non-zero. A wrong bits mapping (192 vs 256 from pubLen) or a wrong OSSL_PKEY_PARAM_MAX_SIZE (sigLen - WP_LMS_LEVELS_SZ) still passes.
Fix: Assert exact values: 192/96 for the 48-byte key, 256/128 for the 56-byte key, and sizeof(lmsVerifySig) for EVP_PKEY_get_size().
| { | ||
| int ok = wolfssl_prov_is_running() && (ctx != NULL); | ||
|
|
||
| (void)params; |
There was a problem hiding this comment.
🔵 [Low] wp_lms_verify_message_init silently discards caller-supplied OSSL_PARAMs · API contract violations
params is cast to void and ignored, and the signature dispatch table registers no OSSL_FUNC_SIGNATURE_SET_CTX_PARAMS. A caller passing parameters to EVP_PKEY_verify_message_init() gets success with the parameters silently dropped instead of an error.
Fix: Fail the init when params contains any entry, or add a set_ctx_params/settable_ctx_params pair that rejects unknown parameters.
| if (ok) { | ||
| XMEMSET(rawSig, 0, WP_LMS_LEVELS_SZ); | ||
| XMEMCPY(rawSig + WP_LMS_LEVELS_SZ, sig, sigLen); | ||
| if (msg == NULL) { |
There was a problem hiding this comment.
🔵 [Low] Empty-message verify path in wp_lms_verify is never exercised · Missing edge-case coverage on a function the PR also changed
wp_lms_verify() accepts msg == NULL when msgLen == 0 and substitutes a one-byte empty sentinel before calling wc_LmsKey_Verify(). No test in test_lms.c calls EVP_PKEY_verify() with a zero-length or NULL message, so this branch is entirely uncovered.
Fix: Add a case in test_lms_verify that calls EVP_PKEY_verify() with msg = NULL, msgLen = 0 and with a valid pointer and msgLen = 0.
| if (ok) { | ||
| p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_PUB_KEY); | ||
| ok = (p != NULL) && | ||
| OSSL_PARAM_get_octet_string_ptr(p, &pub, &pubLen) && |
There was a problem hiding this comment.
🟠 [Medium] Public-key buffer leaked on every LMS key import · Memory management
pub is NULL-initialised and passed to OSSL_PARAM_get_octet_string_ptr(). For an OSSL_PARAM_OCTET_STRING param (what EVP_PKEY_fromdata and EVP_PKEY_new_raw_public_key_ex supply) that call falls back to OSSL_PARAM_get_octet_string(), which mallocs a copy; wp_lms_import never frees it, leaking 48-56 bytes per imported key.
Related known finding #6681 (similar but distinct): Both concern unreleased provider-side allocations, but this occurs in wp_lms_import when parameter extraction allocates a public-key copy due to the wrong accessor; issue 6681 occurs in ECX/ML-DSA encoders when a returned BIO wrapper is not freed. The faulting operations, root causes, functions, and required patches differ.
Fix: Use the repo's zero-copy helper wp_params_get_octet_string_ptr() as every other keymgmt import does.
| { | ||
| int ok = wolfssl_prov_is_running() && (ctx != NULL); | ||
|
|
||
| (void)params; |
There was a problem hiding this comment.
🔵 [Low] LMS verify_message_init silently discards caller-supplied OSSL_PARAMs · OpenSSL API contract violations
params is cast to void and ignored, and the dispatch table registers no OSSL_FUNC_SIGNATURE_SET_CTX_PARAMS. An application passing parameters to EVP_PKEY_verify_message_init() gets a success return even though none of them were applied.
Fix: Fail the init when params is non-NULL and contains any entry, or add a set_ctx_params handler that rejects unknown keys.
| EVP_PKEY_PUBLIC_KEY, &key); | ||
| if (err == 0) { | ||
| signature = EVP_SIGNATURE_fetch(wpLibCtx, "LMS", NULL); | ||
| ctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, key, NULL); |
There was a problem hiding this comment.
🔵 [Low] No test exercises the LMS signature-context duplication path · Missing edge-case coverage on a function the PR also changed
wp_lms_sig_dupctx() (src/wp_lms.c:379) is new code containing a key up-ref and an allocation-failure cleanup branch, but no test calls EVP_PKEY_CTX_dup() on an LMS verify context, so neither the refcount transfer nor verification through the duplicated context is validated.
Fix: Add a case that duplicates the LMS verify context with EVP_PKEY_CTX_dup() and verifies the KAT signature through the copy.
Description