Skip to content

Add RFC 9802 HSS/LMS and XMSS/XMSS^MT X.509 certificate verification#9

Open
Frauschi wants to merge 1 commit intomasterfrom
claude/add-lms-xmss-x509-EFpVu
Open

Add RFC 9802 HSS/LMS and XMSS/XMSS^MT X.509 certificate verification#9
Frauschi wants to merge 1 commit intomasterfrom
claude/add-lms-xmss-x509-EFpVu

Conversation

@Frauschi
Copy link
Copy Markdown
Owner

@Frauschi Frauschi commented Apr 24, 2026

Summary

Wires wolfCrypt's existing stateful hash-based signature implementations into the X.509 parse and verify paths per RFC 9802. Parsing, loading into a WOLFSSL_CERT_MANAGER, and ConfirmSignature all now recognise:

  • id-alg-hss-lms-hashsig1.2.840.113549.1.9.16.3.17
  • id-alg-xmss-hashsig1.3.6.1.5.5.7.6.34
  • id-alg-xmssmt-hashsig1.3.6.1.5.5.7.6.35

with parameters absent per RFC 9802 sec 2 and no pre-hash of the TBSCertificate.

Scope is verify-only on WOLFSSL_HAVE_LMS / WOLFSSL_HAVE_XMSS. Certificate generation, TLS handshake signatures and OpenSSL compat shims are intentionally out of scope.

Baseline

Rebased on top of upstream master, which now contains:

Implementation notes

wolfCrypt level

  • wc_LmsKey_ImportPubRaw auto-derives the parameter set from u32str(L) || lmsType || lmOtsType when key->params == NULL, and validates against pre-set params when they are set. The auto-derived LmsParams is held in a local until the length check passes so a failing length check can't leave key->params half-set. Closes external bug 3057.
  • wc_XmssKey_ImportPubRaw_ex is new. XMSS and XMSS^MT share a numeric OID namespace (both start at 1) and produce public keys of identical length, so disambiguation requires an is_xmssmt hint that the X.509 caller takes from the outer AlgorithmIdentifier. Accepts INITED, PARMSET, VERIFYONLY states (rejects OK, since overwriting a public key while a private key is loaded would silently desync priv/pub); in INITED it derives params from the OID prefix using the hint and commits to the key only after the length check passes, otherwise it checks the OID prefix and is_xmssmt against the configured params and returns BAD_FUNC_ARG on mismatch. Keeps the OID→param-set mapping in wc_xmss.c rather than duplicating it in asn.c. The plain wc_XmssKey_ImportPubRaw is unchanged.
  • wc_LmsKey_GetSigLen and wc_XmssKey_GetSigLen now NULL-check key->params, matching GetPubLen / GetPrivLen. Closes external bug 3058.

asn.c

  • New OID arrays sigHssLmsOid / sigXmssOid / sigXmssMtOid and matching keyHssLmsOid / keyXmssOid / keyXmssMtOid, inserted after the SLH-DSA arrays.
  • OidFromId oidSigType / oidKeyType dispatch additions.
  • StoreKey guard extended; GetCertKey switch handles the three new keyOIDs.
  • IsSigAlgoECC / SigOidMatchesKeyOid updated; HashForSignature skips pre-hashing for these OIDs.
  • SignatureCtx union gains LmsKey / XmssKey members; FreeSignatureCtx handles cleanup.
  • Three ConfirmSignature states (SIG_STATE_KEY / SIG_STATE_DO / SIG_STATE_CHECK) handle LMS and XMSS / XMSS^MT.

Other

  • scripts/asn1_oid_sum.pl gains the three OIDs; wolfssl/wolfcrypt/oid_sum.h regenerated. The manually-curated WOLFSSL_ACME_OID block is preserved.
  • enum cert_enums reserves HSS_LMS_KEY / XMSS_KEY / XMSSMT_KEY (36/37/38) for future cert-gen support, slotting after SLH_DSA_SHAKE_256F_KEY.
  • DYNAMIC_TYPE_XMSS (109) added; DYNAMIC_TYPE_LMS already existed.

Interoperability

All test fixtures are generated with Bouncy Castle 1.81 and committed under certs/lms/ and certs/xmss/. BC's default XMSS / XMSS^MT encoding uses pre-standard ISARA OIDs and wraps the raw RFC 8391 pub key in an OCTET STRING, so the generator overrides both the AlgorithmIdentifier and the SPKI to match RFC 9802.

Algorithm OID Fixture Key structure
LMS HSS_LMS bc_lms_sha256_h5_w4_root.der L1 H5 W4 SHA-256
LMS HSS_LMS bc_lms_sha256_h10_w8_root.der L1 H10 W8 SHA-256
HSS HSS_LMS bc_hss_L2_H5_W8_root.der 2-level HSS
HSS HSS_LMS bc_hss_L3_H5_W4_root.der 3-level HSS
LMS HSS_LMS bc_lms_chain_ca.der + bc_lms_chain_leaf.der CA→leaf chain
XMSS XMSS bc_xmss_sha2_10_256_root.der H=10 SHA-256
XMSS XMSS bc_xmss_sha2_16_256_root.der H=16 SHA-256
XMSS^MT XMSSMT bc_xmssmt_sha2_20_2_256_root.der H=20, 2 subtrees
XMSS^MT XMSSMT bc_xmssmt_sha2_20_4_256_root.der H=20, 4 subtrees
XMSS^MT XMSSMT bc_xmssmt_sha2_40_8_256_root.der H=40, 8 subtrees, ~19 KB

All fixtures carry BasicConstraints (CA:TRUE on issuers, CA:FALSE on leaf) and KeyUsage per RFC 9802 sec 3 / RFC 5280 sec 4.2.1.9.

test_rfc9802_x509_verify exercises every fixture through:

  1. wc_ParseCert with OID assertions (keyOID, signatureOID).
  2. Full wolfSSL_CertManagerVerifyBuffer against a self-installed trust anchor.
  3. Signature-byte tamper → verification MUST fail.
  4. TBSCertificate-interior tamper at the midpoint of [certBegin, sigIndex) → verification MUST fail.
  5. KeyUsage extension presence + at least one of digitalSignature / nonRepudiation / keyCertSign / cRLSign.
  6. Real CA→leaf chain: leaf verifies against loaded CA, fails to verify without it.

Plus wolfCrypt-level negative tests:

  • Unknown lmsType / lmOtsType, truncated input, pre-set params disagreeing with raw bytes.
  • Unknown XMSS/XMSS^MT OID, truncated input, NULL args.
  • GetSigLen on a key with no params set returns BAD_FUNC_ARG (no NULL deref).
  • LMS partial-write invariant: a length mismatch leaves key->params NULL.
  • XMSS _ex partial-write invariant: a length mismatch after a valid OID prefix leaves the key in INITED state with key->params == NULL.
  • XMSS _ex PARMSET-mismatch: OID prefix or is_xmssmt hint disagreeing with set params returns BAD_FUNC_ARG.
  • XMSS _ex is_xmssmt disambiguation: same 4-byte OID prefix with hint=0 vs hint=1 lands in different tables and produces distinct is_xmssmt.
  • XMSS _ex lenient-state positive: re-importing the same pub key into a VERIFYONLY key succeeds.
  • Cross-OID mismatch between SPKI and outer signatureAlgorithm in a tampered cert.

Test plan

  • ./configure (no LMS/XMSS) — builds clean, unit tests pass
  • ./configure --enable-lms — builds clean, test_rfc9802_x509_verify passes
  • ./configure --enable-xmss — builds clean, test_rfc9802_x509_verify passes
  • ./configure --enable-lms --enable-xmss — builds clean, test_rfc9802_x509_verify passes
  • ./configure --enable-lms --enable-xmss --enable-certgen — builds clean, test_rfc9802_x509_verify passes
  • ./tests/unit.test — full pass including test_rfc9802_x509_verify
  • ./wolfcrypt/test/testwolfcrypt — existing LMS/XMSS tests still pass
  • ./testsuite/testsuite.test — no regression in TLS handshake paths

Out of scope (explicit)

  • Certificate generation (wc_MakeCert, EncodePublicKey, wc_SignCert_ex dispatch, public DER/PEM export APIs).
  • TLS 1.3 SignatureScheme registration / CertificateVerify.
  • OpenSSL-compat shims (wolfSSL_LMS_*).
  • OCSP / CRL signed with LMS/XMSS (same codepath will likely work once verify is in, but untested here).

https://claude.ai/code/session_01SnSQMb145Hkyyf7hQQQ8cq

@Frauschi Frauschi closed this Apr 24, 2026
@Frauschi Frauschi reopened this Apr 24, 2026
@Frauschi Frauschi force-pushed the claude/add-lms-xmss-x509-EFpVu branch from 4598010 to a0b8bd2 Compare April 30, 2026 17:16
/* 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x09,0x10,0x01,0x17 */
AUTH_ENVELOPED_DATA = 692, /* 1.2.840.113549.1.9.16.1.23 */
/* 0x60,0x86,0x48,0x01,0x65,0x02,0x01,0x02,0x4E,0x02 */
ENCRYPTED_KEY_PACKAGE = 489 /* 2.16.840.1.101.2.1.2.78.2 */
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this removed?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored. The regression came from regenerating oid_sum.h with a stale local scripts/asn1_oid_sum.pl that predated upstream's liblms / liboqs cleanup; the script also reverted the Falcon OIDs and dropped the WC_16BIT_CPU header note. I've reset the perl script + oid_sum.h to upstream and re-added only the three new OIDs.


Generated by Claude Code

Comment thread wolfssl/wolfcrypt/wc_lms.h Outdated
WOLFSSL_API int wc_LmsKey_ExportPub(LmsKey * keyDst, const LmsKey * keySrc);
WOLFSSL_API int wc_LmsKey_ExportPubRaw(const LmsKey * key, byte * out,
word32 * outLen);
/* Import an RFC 8554 raw HSS public key.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.


Generated by Claude Code

Comment thread wolfssl/wolfcrypt/wc_xmss.h Outdated
* Returns 0 on success. BAD_FUNC_ARG when key or in is NULL, BUFFER_E
* when inLen < 4, BAD_STATE_E if key is not in the INITED state, and
* NOT_COMPILED_IN when the derived parameter set isn't built in. */
WOLFSSL_API int wc_XmssKey_SetParamsFromPubRaw(XmssKey* key, const byte* in,
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this new method, isn‘t it better to create a ImportPubRaw_ex() method, which does this internally and has another argument for the user to provide the is_xmssmt hint? This way, I don’t need two calls on the user side

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored. wc_XmssKey_SetParamsFromPubRaw is gone; the X.509 caller now makes a single wc_XmssKey_ImportPubRaw_ex(key, in, inLen, is_xmssmt) call. The _ex variant accepts INITED, PARMSET, OK, or VERIFYONLY state — when the key is in INITED, params are derived from the raw OID prefix using the hint; when params are already set, the hint is ignored and the OID prefix is checked for consistency. The plain wc_XmssKey_ImportPubRaw is unchanged.


Generated by Claude Code

Comment thread wolfcrypt/src/wc_lms.c Outdated
word32 lmsType = 0;
word32 lmOtsType = 0;

/* RFC 8554 §3.3/§6.1: HSS public key = u32str(L) || pub[0], where
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only use ASCII symbols, so no §

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Replaced every § in our changes (this comment, two in asn.c and one in tests/api.c) with sec, and confirmed the modified files contain no non-ASCII bytes.


Generated by Claude Code

Comment thread wolfcrypt/src/wc_xmss.c Outdated
}

/* Validate state. */
if ((ret == 0) && (key->state != WC_XMSS_STATE_INITED)) {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about other potential state a key could already be in when importing a public key?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded into the wc_XmssKey_ImportPubRaw_ex refactor. The combined function now accepts INITED (auto-derive params from OID prefix), PARMSET, OK, and VERIFYONLY (params already set, OID prefix checked for consistency). FREED and BAD still return BAD_STATE_E.


Generated by Claude Code

Comment thread wolfcrypt/src/asn.c Outdated
#ifdef WOLFSSL_HAVE_LMS
case HSS_LMSk:
{
/* RFC 9802: no pre-hash; TBSCertificate bytes are fed
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.


Generated by Claude Code

@Frauschi Frauschi force-pushed the claude/add-lms-xmss-x509-EFpVu branch 2 times, most recently from 2ada881 to 433994a Compare May 5, 2026 20:21
Wires wolfCrypt's stateful hash-based signature implementations into
the X.509 parse and verify paths per RFC 9802. Parsing, loading into a
WOLFSSL_CERT_MANAGER, and ConfirmSignature now recognise:

  * id-alg-hss-lms-hashsig    1.2.840.113549.1.9.16.3.17
  * id-alg-xmss-hashsig       1.3.6.1.5.5.7.6.34
  * id-alg-xmssmt-hashsig     1.3.6.1.5.5.7.6.35

with parameters absent per RFC 9802 sec 2 and no pre-hash of the TBS.
Scope is verify-only on WOLFSSL_HAVE_LMS / WOLFSSL_HAVE_XMSS.

wolfCrypt-level changes:
  * wc_LmsKey_ImportPubRaw auto-derives the parameter set from
    u32str(L) || lmsType || lmOtsType when key->params == NULL, and
    validates against pre-set params when they are set.
  * wc_LmsKey_GetSigLen and wc_XmssKey_GetSigLen now NULL-check
    key->params (matches sibling GetPubLen / GetPrivLen).
  * wc_XmssKey_ImportPubRaw_ex is new: derives params from the
    4-byte OID prefix of a raw RFC 8391 public key, with an
    is_xmssmt hint to disambiguate the overlapping OID namespaces.
    Accepts INITED, PARMSET, OK and VERIFYONLY states; rejects a
    contradictory is_xmssmt hint when params are already set.

X.509 wiring in asn.c (anchored on the new SLH-DSA landmarks after
upstream's SPHINCS+ -> SLH-DSA replacement):
  * New OID arrays sigHssLmsOid / sigXmssOid / sigXmssMtOid + key
    counterparts.
  * GetObjectId / oidSigType / oidKeyType dispatch.
  * StoreKey guard extended; GetCertKey switch handles the three
    new keyOIDs.
  * IsSigAlgoECC / SigOidMatchesKeyOid / HashForSignature updated
    to omit AlgorithmIdentifier parameters and skip pre-hashing.
  * SignatureCtx union gets LmsKey / XmssKey members; FreeSignatureCtx
    handles cleanup.
  * Three ConfirmSignature states (KEY / DO / CHECK) gain LMS and
    XMSS / XMSS^MT cases.

scripts/asn1_oid_sum.pl gains the three OIDs; oid_sum.h regenerated
with HSS_LMSk / XMSSk / XMSSMTk and CTC_HSS_LMS / CTC_XMSS / CTC_XMSSMT
plus the manually-curated WOLFSSL_ACME_OID block reapplied.

enum cert_enums reserves HSS_LMS_KEY / XMSS_KEY / XMSSMT_KEY (36/37/38)
for future cert-gen support, slotting after SLH_DSA_SHAKE_256F_KEY.

Tests in test_rfc9802_x509_verify exercise:
  * 9 Bouncy Castle 1.81-generated fixtures (LMS h5/w4 and h10/w8;
    HSS L2_H5_W8 and L3_H5_W4; XMSS H=10 and H=16; XMSS^MT 20/2,
    20/4 and 40/8) plus a CA->leaf LMS chain.
  * wc_ParseCert with OID assertions, full
    wolfSSL_CertManagerVerifyBuffer against a self-installed trust
    anchor, signature-byte tamper, TBSCertificate-interior tamper at
    the midpoint of [certBegin, sigIndex), KeyUsage extension presence.
  * wolfCrypt-level negative tests: unknown lmsType / lmOtsType,
    unknown XMSS/XMSS^MT OID, truncated input, NULL args, GetSigLen
    on a key with no params set, partial-write invariant on length
    mismatch (key->params stays NULL), and PARMSET-state mismatch
    (OID prefix or is_xmssmt hint disagreeing with set params).
  * X.509-level negative: cross-OID mismatch between SPKI and outer
    signatureAlgorithm.

All fixtures carry BasicConstraints (CA:TRUE on issuers, CA:FALSE on
leaves) and KeyUsage per RFC 9802 sec 3 / RFC 5280 sec 4.2.1.9. BC's
default XMSS / XMSS^MT encoding uses pre-standard ISARA OIDs and an
OCTET STRING SPKI wrapper, so the fixture generator overrides both to
match RFC 9802.

Verified across configure permutations:
  * default (no LMS/XMSS)
  * --enable-lms
  * --enable-xmss
  * --enable-lms --enable-xmss
  * --enable-lms --enable-xmss --enable-certgen

Out of scope: cert generation, TLS 1.3 SignatureScheme, OpenSSL compat
shims (wolfSSL_LMS_*), OCSP/CRL signed with these algorithms.

https://claude.ai/code/session_01SnSQMb145Hkyyf7hQQQ8cq

RFC 9802: tighten _ex semantics, more negative tests

wc_XmssKey_ImportPubRaw_ex
  * Hold the auto-derived params / oid / is_xmssmt in locals and only
    commit them to the key after the public-key length check passes,
    so a length mismatch leaves an INITED key untouched. Mirrors the
    same fix applied to wc_LmsKey_ImportPubRaw earlier.
  * Reject WC_XMSS_STATE_OK. Importing public-key bytes when private
    material is already loaded would silently desync priv/pub; INITED,
    PARMSET and VERIFYONLY are still accepted.
  * Drop redundant (int) cast on the byte-typed key->is_xmssmt field
    in the consistency check.
  * Replace colloquial "compiled out" comments with the project's
    "disabled at compile time" wording.

Tests
  * Partial-write invariant for XMSS _ex: a length mismatch after a
    valid OID prefix returns BUFFER_E and leaves key.params NULL.
  * is_xmssmt disambiguation: feeding the same 4-byte OID prefix
    (0x00000001, valid in both XMSS and XMSS^MT tables) with hint=0
    vs hint=1 lands in different parameter sets and persists distinct
    is_xmssmt values, locking in that the hint actually drives the
    table selection.
  * Lenient-state positive test: re-importing the same public key
    into a VERIFYONLY key succeeds, exercising the lenient-state
    branch that complements the OID/hint mismatch rejection.

https://claude.ai/code/session_01SnSQMb145Hkyyf7hQQQ8cq

RFC 9802: split LMS / XMSS X.509 tests; add XMSS chain fixture

The unified test_rfc9802_x509_verify worked when both --enable-lms and
--enable-xmss were on, but a user with only one of the two would still
see the test registered (just compiled to a no-op). Split it into two
functions, test_rfc9802_lms_x509_verify (group "lms") and
test_rfc9802_xmss_x509_verify (group "xmss"), so each runs only when
its feature is built and is filterable individually via --group.

Also add an XMSS CA -> leaf chain fixture (bc_xmss_chain_ca.der,
bc_xmss_chain_leaf.der) generated with Bouncy Castle, and a
rfc9802_xmss_chain_verify helper that mirrors the existing LMS chain
verify: load CA as a trust anchor, verify leaf, then re-verify with no
CA installed and assert failure.

https://claude.ai/code/session_01SnSQMb145Hkyyf7hQQQ8cq

RFC 9802: add BC-native LMS cert as cross-impl interop gate

For HSS/LMS, Bouncy Castle's stock X.509 path is already RFC 9802-
compliant: JcaContentSignerBuilder("LMS") emits id-alg-hss-lms-hashsig
(1.2.840.113549.1.9.16.3.17) with absent parameters, and BC's
SubjectPublicKeyInfoFactory carries the raw RFC 8554 public key bytes
directly in the BIT STRING. (BC's XMSS / XMSSMT path still uses
pre-standard private OIDs and an XMSSKeyParams parameters structure,
checked against bc-java main as of May 2026, so the fixtures for those
algorithms still need our Rfc9802Signer override.)

Add bc_lms_native_bc_root.der, generated through BC's stock
JcaContentSignerBuilder + JcaX509v3CertificateBuilder with no
overrides, and pull it into the LMS test list. This is the cross-
impl gate that catches drift in the RFC 9802 encoding wolfSSL accepts:
if BC ships a fully native LMS cert today and we can't verify it,
something has regressed locally.

https://claude.ai/code/session_01SnSQMb145Hkyyf7hQQQ8cq
@Frauschi Frauschi force-pushed the claude/add-lms-xmss-x509-EFpVu branch from 1b7c949 to 202d133 Compare May 6, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants