Add RFC 9802 HSS/LMS and XMSS/XMSS^MT X.509 certificate verification#9
Add RFC 9802 HSS/LMS and XMSS/XMSS^MT X.509 certificate verification#9
Conversation
4598010 to
a0b8bd2
Compare
| /* 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 */ |
There was a problem hiding this comment.
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
| 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. |
There was a problem hiding this comment.
| * 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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| word32 lmsType = 0; | ||
| word32 lmOtsType = 0; | ||
|
|
||
| /* RFC 8554 §3.3/§6.1: HSS public key = u32str(L) || pub[0], where |
There was a problem hiding this comment.
Only use ASCII symbols, so no §
There was a problem hiding this comment.
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
| } | ||
|
|
||
| /* Validate state. */ | ||
| if ((ret == 0) && (key->state != WC_XMSS_STATE_INITED)) { |
There was a problem hiding this comment.
How about other potential state a key could already be in when importing a public key?
There was a problem hiding this comment.
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
| #ifdef WOLFSSL_HAVE_LMS | ||
| case HSS_LMSk: | ||
| { | ||
| /* RFC 9802: no pre-hash; TBSCertificate bytes are fed |
There was a problem hiding this comment.
2ada881 to
433994a
Compare
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
1b7c949 to
202d133
Compare
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, andConfirmSignatureall now recognise:id-alg-hss-lms-hashsig—1.2.840.113549.1.9.16.3.17id-alg-xmss-hashsig—1.3.6.1.5.5.7.6.34id-alg-xmssmt-hashsig—1.3.6.1.5.5.7.6.35with 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:
wc_LmsParm.liblms/libxmssintegrations. All guards in this PR therefore use onlyWOLFSSL_HAVE_LMS/WOLFSSL_HAVE_XMSS.78564f0c,d9361e2d) — the_eximport added here propagateskey->heapcorrectly via the existingwc_XmssKey_Initandwc_XmssKey_GetPubLenpaths.Implementation notes
wolfCrypt level
wc_LmsKey_ImportPubRawauto-derives the parameter set fromu32str(L) || lmsType || lmOtsTypewhenkey->params == NULL, and validates against pre-set params when they are set. The auto-derivedLmsParamsis held in a local until the length check passes so a failing length check can't leavekey->paramshalf-set. Closes external bug 3057.wc_XmssKey_ImportPubRaw_exis new. XMSS and XMSS^MT share a numeric OID namespace (both start at 1) and produce public keys of identical length, so disambiguation requires anis_xmssmthint that the X.509 caller takes from the outerAlgorithmIdentifier. AcceptsINITED,PARMSET,VERIFYONLYstates (rejectsOK, since overwriting a public key while a private key is loaded would silently desync priv/pub); inINITEDit 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 andis_xmssmtagainst the configured params and returnsBAD_FUNC_ARGon mismatch. Keeps the OID→param-set mapping inwc_xmss.crather than duplicating it inasn.c. The plainwc_XmssKey_ImportPubRawis unchanged.wc_LmsKey_GetSigLenandwc_XmssKey_GetSigLennow NULL-checkkey->params, matchingGetPubLen/GetPrivLen. Closes external bug 3058.asn.c
sigHssLmsOid/sigXmssOid/sigXmssMtOidand matchingkeyHssLmsOid/keyXmssOid/keyXmssMtOid, inserted after the SLH-DSA arrays.OidFromIdoidSigType/oidKeyTypedispatch additions.StoreKeyguard extended;GetCertKeyswitch handles the three new keyOIDs.IsSigAlgoECC/SigOidMatchesKeyOidupdated;HashForSignatureskips pre-hashing for these OIDs.SignatureCtxunion gainsLmsKey/XmssKeymembers;FreeSignatureCtxhandles cleanup.ConfirmSignaturestates (SIG_STATE_KEY/SIG_STATE_DO/SIG_STATE_CHECK) handle LMS and XMSS / XMSS^MT.Other
scripts/asn1_oid_sum.plgains the three OIDs;wolfssl/wolfcrypt/oid_sum.hregenerated. The manually-curatedWOLFSSL_ACME_OIDblock is preserved.enum cert_enumsreservesHSS_LMS_KEY/XMSS_KEY/XMSSMT_KEY(36/37/38) for future cert-gen support, slotting afterSLH_DSA_SHAKE_256F_KEY.DYNAMIC_TYPE_XMSS(109) added;DYNAMIC_TYPE_LMSalready existed.Interoperability
All test fixtures are generated with Bouncy Castle 1.81 and committed under
certs/lms/andcerts/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 theAlgorithmIdentifierand the SPKI to match RFC 9802.HSS_LMSbc_lms_sha256_h5_w4_root.derHSS_LMSbc_lms_sha256_h10_w8_root.derHSS_LMSbc_hss_L2_H5_W8_root.derHSS_LMSbc_hss_L3_H5_W4_root.derHSS_LMSbc_lms_chain_ca.der+bc_lms_chain_leaf.derXMSSbc_xmss_sha2_10_256_root.derXMSSbc_xmss_sha2_16_256_root.derXMSSMTbc_xmssmt_sha2_20_2_256_root.derXMSSMTbc_xmssmt_sha2_20_4_256_root.derXMSSMTbc_xmssmt_sha2_40_8_256_root.derAll fixtures carry
BasicConstraints(CA:TRUE on issuers, CA:FALSE on leaf) andKeyUsageper RFC 9802 sec 3 / RFC 5280 sec 4.2.1.9.test_rfc9802_x509_verifyexercises every fixture through:wc_ParseCertwith OID assertions (keyOID,signatureOID).wolfSSL_CertManagerVerifyBufferagainst a self-installed trust anchor.[certBegin, sigIndex)→ verification MUST fail.KeyUsageextension presence + at least one of digitalSignature / nonRepudiation / keyCertSign / cRLSign.Plus wolfCrypt-level negative tests:
lmsType/lmOtsType, truncated input, pre-set params disagreeing with raw bytes.GetSigLenon a key with no params set returnsBAD_FUNC_ARG(no NULL deref).key->paramsNULL._expartial-write invariant: a length mismatch after a valid OID prefix leaves the key inINITEDstate withkey->params == NULL._exPARMSET-mismatch: OID prefix oris_xmssmthint disagreeing with set params returnsBAD_FUNC_ARG._exis_xmssmtdisambiguation: same 4-byte OID prefix with hint=0 vs hint=1 lands in different tables and produces distinctis_xmssmt._exlenient-state positive: re-importing the same pub key into aVERIFYONLYkey succeeds.Test plan
./configure(no LMS/XMSS) — builds clean, unit tests pass./configure --enable-lms— builds clean,test_rfc9802_x509_verifypasses./configure --enable-xmss— builds clean,test_rfc9802_x509_verifypasses./configure --enable-lms --enable-xmss— builds clean,test_rfc9802_x509_verifypasses./configure --enable-lms --enable-xmss --enable-certgen— builds clean,test_rfc9802_x509_verifypasses./tests/unit.test— full pass includingtest_rfc9802_x509_verify./wolfcrypt/test/testwolfcrypt— existing LMS/XMSS tests still pass./testsuite/testsuite.test— no regression in TLS handshake pathsOut of scope (explicit)
wc_MakeCert,EncodePublicKey,wc_SignCert_exdispatch, public DER/PEM export APIs).SignatureSchemeregistration /CertificateVerify.wolfSSL_LMS_*).https://claude.ai/code/session_01SnSQMb145Hkyyf7hQQQ8cq