diff --git a/CMakeLists.txt b/CMakeLists.txt index b4af397e9e0..b9157ec0ab4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -699,6 +699,10 @@ add_option(WOLFSSL_LMSSHA256192 "Enable the LMS SHA_256_192 truncated variant (default: disabled)" "no" "yes;no") +add_option(WOLFSSL_LMSNOSHA256256 + "Disable the LMS SHA_256_256 standard variant (default: disabled)" + "no" "yes;no") + if (WOLFSSL_LMS) list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_HAVE_LMS") @@ -706,9 +710,13 @@ if (WOLFSSL_LMS) if (WOLFSSL_LMSSHA256192) list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_LMS_SHA256_192") - list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_NO_LMS_SHA256_256") set_wolfssl_definitions("WOLFSSL_LMS_SHA256_192" RESULT) + endif() + + if (WOLFSSL_LMSNOSHA256256) + list(APPEND WOLFSSL_DEFINITIONS "-DWOLFSSL_NO_LMS_SHA256_256") + set_wolfssl_definitions("WOLFSSL_NO_LMS_SHA256_256" RESULT) endif() endif() diff --git a/certs/include.am b/certs/include.am index 0f04eeb4128..66fe06301e7 100644 --- a/certs/include.am +++ b/certs/include.am @@ -161,6 +161,8 @@ include certs/falcon/include.am include certs/rsapss/include.am include certs/dilithium/include.am include certs/slhdsa/include.am +include certs/lms/include.am +include certs/xmss/include.am include certs/rpk/include.am include certs/acert/include.am include certs/mldsa/include.am diff --git a/certs/lms/bc_hss_L2_H5_W8_root.der b/certs/lms/bc_hss_L2_H5_W8_root.der new file mode 100644 index 00000000000..824d5664af8 Binary files /dev/null and b/certs/lms/bc_hss_L2_H5_W8_root.der differ diff --git a/certs/lms/bc_hss_L3_H5_W4_root.der b/certs/lms/bc_hss_L3_H5_W4_root.der new file mode 100644 index 00000000000..43904f7ae7a Binary files /dev/null and b/certs/lms/bc_hss_L3_H5_W4_root.der differ diff --git a/certs/lms/bc_lms_chain_ca.der b/certs/lms/bc_lms_chain_ca.der new file mode 100644 index 00000000000..e3cebcccbb1 Binary files /dev/null and b/certs/lms/bc_lms_chain_ca.der differ diff --git a/certs/lms/bc_lms_chain_leaf.der b/certs/lms/bc_lms_chain_leaf.der new file mode 100644 index 00000000000..4346b7aa159 Binary files /dev/null and b/certs/lms/bc_lms_chain_leaf.der differ diff --git a/certs/lms/bc_lms_native_bc_root.der b/certs/lms/bc_lms_native_bc_root.der new file mode 100644 index 00000000000..88d5df7b502 Binary files /dev/null and b/certs/lms/bc_lms_native_bc_root.der differ diff --git a/certs/lms/bc_lms_sha256_h10_w8_root.der b/certs/lms/bc_lms_sha256_h10_w8_root.der new file mode 100644 index 00000000000..c9ca9bf226c Binary files /dev/null and b/certs/lms/bc_lms_sha256_h10_w8_root.der differ diff --git a/certs/lms/bc_lms_sha256_h5_w4_root.der b/certs/lms/bc_lms_sha256_h5_w4_root.der new file mode 100644 index 00000000000..ef4941ee8c0 Binary files /dev/null and b/certs/lms/bc_lms_sha256_h5_w4_root.der differ diff --git a/certs/lms/include.am b/certs/lms/include.am new file mode 100644 index 00000000000..f9e39c1d04d --- /dev/null +++ b/certs/lms/include.am @@ -0,0 +1,12 @@ +# vim:ft=automake +# All paths should be given relative to the root +# + +EXTRA_DIST += \ + certs/lms/bc_lms_sha256_h5_w4_root.der \ + certs/lms/bc_lms_sha256_h10_w8_root.der \ + certs/lms/bc_hss_L2_H5_W8_root.der \ + certs/lms/bc_hss_L3_H5_W4_root.der \ + certs/lms/bc_lms_chain_ca.der \ + certs/lms/bc_lms_chain_leaf.der \ + certs/lms/bc_lms_native_bc_root.der diff --git a/certs/xmss/bc_xmss_chain_ca.der b/certs/xmss/bc_xmss_chain_ca.der new file mode 100644 index 00000000000..31c8690e5b5 Binary files /dev/null and b/certs/xmss/bc_xmss_chain_ca.der differ diff --git a/certs/xmss/bc_xmss_chain_leaf.der b/certs/xmss/bc_xmss_chain_leaf.der new file mode 100644 index 00000000000..cf168ee3e2b Binary files /dev/null and b/certs/xmss/bc_xmss_chain_leaf.der differ diff --git a/certs/xmss/bc_xmss_sha2_10_256_root.der b/certs/xmss/bc_xmss_sha2_10_256_root.der new file mode 100644 index 00000000000..12d70a002ef Binary files /dev/null and b/certs/xmss/bc_xmss_sha2_10_256_root.der differ diff --git a/certs/xmss/bc_xmss_sha2_16_256_root.der b/certs/xmss/bc_xmss_sha2_16_256_root.der new file mode 100644 index 00000000000..91f3bf55466 Binary files /dev/null and b/certs/xmss/bc_xmss_sha2_16_256_root.der differ diff --git a/certs/xmss/bc_xmssmt_sha2_20_2_256_root.der b/certs/xmss/bc_xmssmt_sha2_20_2_256_root.der new file mode 100644 index 00000000000..24b47019e1b Binary files /dev/null and b/certs/xmss/bc_xmssmt_sha2_20_2_256_root.der differ diff --git a/certs/xmss/bc_xmssmt_sha2_20_4_256_root.der b/certs/xmss/bc_xmssmt_sha2_20_4_256_root.der new file mode 100644 index 00000000000..b3037316d2b Binary files /dev/null and b/certs/xmss/bc_xmssmt_sha2_20_4_256_root.der differ diff --git a/certs/xmss/bc_xmssmt_sha2_40_8_256_root.der b/certs/xmss/bc_xmssmt_sha2_40_8_256_root.der new file mode 100644 index 00000000000..870faa2c18b Binary files /dev/null and b/certs/xmss/bc_xmssmt_sha2_40_8_256_root.der differ diff --git a/certs/xmss/include.am b/certs/xmss/include.am new file mode 100644 index 00000000000..ff3bfbee4df --- /dev/null +++ b/certs/xmss/include.am @@ -0,0 +1,12 @@ +# vim:ft=automake +# All paths should be given relative to the root +# + +EXTRA_DIST += \ + certs/xmss/bc_xmss_sha2_10_256_root.der \ + certs/xmss/bc_xmss_sha2_16_256_root.der \ + certs/xmss/bc_xmssmt_sha2_20_2_256_root.der \ + certs/xmss/bc_xmssmt_sha2_20_4_256_root.der \ + certs/xmss/bc_xmssmt_sha2_40_8_256_root.der \ + certs/xmss/bc_xmss_chain_ca.der \ + certs/xmss/bc_xmss_chain_leaf.der diff --git a/scripts/asn1_oid_sum.pl b/scripts/asn1_oid_sum.pl index b67fa26036a..3f2a0a51f4f 100755 --- a/scripts/asn1_oid_sum.pl +++ b/scripts/asn1_oid_sum.pl @@ -314,6 +314,9 @@ sub print_footer { my @slhdsa_shake_192f = (2, 16, 840, 1, 101, 3, 4, 3, 29); my @slhdsa_shake_256s = (2, 16, 840, 1, 101, 3, 4, 3, 30); my @slhdsa_shake_256f = (2, 16, 840, 1, 101, 3, 4, 3, 31); +my @hss_lms = ( 1, 2, 840, 113549, 1, 9, 16, 3, 17 ); +my @xmss = ( 1, 3, 6, 1, 5, 5, 7, 6, 34 ); +my @xmssmt = ( 1, 3, 6, 1, 5, 5, 7, 6, 35 ); my @keys = ( { name => "ANON", oid => \@anon }, @@ -348,6 +351,9 @@ sub print_footer { { name => "SLH_DSA_SHAKE_192F", oid => \@slhdsa_shake_192f }, { name => "SLH_DSA_SHAKE_256S", oid => \@slhdsa_shake_256s }, { name => "SLH_DSA_SHAKE_256F", oid => \@slhdsa_shake_256f }, + { name => "HSS_LMS", oid => \@hss_lms }, + { name => "XMSS", oid => \@xmss }, + { name => "XMSSMT", oid => \@xmssmt }, ); print_sum_enum("Key", "k", \@keys); @@ -1161,6 +1167,12 @@ sub print_footer { same => 1 }, { name => "CTC_SLH_DSA_SHAKE_256F", oid => \@slhdsa_shake_256f, same => 1 }, + { name => "CTC_HSS_LMS", oid => \@hss_lms, + same => 1 }, + { name => "CTC_XMSS", oid => \@xmss, + same => 1 }, + { name => "CTC_XMSSMT", oid => \@xmssmt, + same => 1 }, ); print_enum("Ctc_SigType", "", \@sig_types, 32, 48); diff --git a/tests/api.c b/tests/api.c index 389cf8e5180..258a838e771 100644 --- a/tests/api.c +++ b/tests/api.c @@ -37714,6 +37714,8 @@ int stopOnFail = 0; /*----------------------------------------------------------------------------*/ int test_wc_LmsKey_sign_verify(void); int test_wc_LmsKey_reload_cache(void); +int test_rfc9802_lms_x509_verify(void); +int test_rfc9802_xmss_x509_verify(void); #if defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) @@ -37879,6 +37881,722 @@ int test_wc_LmsKey_reload_cache(void) return EXPECT_RESULT(); } +/*----------------------------------------------------------------------------*/ +/* RFC 9802 (HSS/LMS and XMSS/XMSS^MT in X.509) tests */ +/*----------------------------------------------------------------------------*/ + +/* For every committed self-signed test certificate confirm: + * - wc_ParseCert succeeds on the RFC 9802 AlgorithmIdentifier encoding + * (OID-only SEQUENCE, no NULL parameters) + * - keyOID and signatureOID are set to the expected values + * - loading as a trust anchor and verifying the same bytes through + * wolfSSL_CertManagerVerifyBuffer exercises the ConfirmSignature + * path and succeeds on a valid cert + * - flipping a byte in the signature AND flipping a byte in the + * TBSCertificate both cause verification to fail. + * + * Test vectors are in certs/lms/ and certs/xmss/, generated with Bouncy + * Castle 1.81. BC's default XMSS / XMSS^MT X.509 encoding uses pre- + * standard ISARA OIDs and wraps the raw RFC 8391 pub key in an OCTET + * STRING, so the fixtures were produced with a small generator that + * overrides the AlgorithmIdentifier and SPKI to match RFC 9802. */ +#if (defined(WOLFSSL_HAVE_LMS) || defined(WOLFSSL_HAVE_XMSS)) && \ + !defined(NO_FILESYSTEM) && !defined(NO_CERTS) +/* Sanity bound on a test fixture cert. The largest BC-generated + * fixture we ship (XMSS^MT 40/8) is ~19 KiB; 1 MiB is well above + * any realistic RFC 9802 cert and catches a wild XFTELL. Typed as + * long to match XFTELL's return so the size comparison below isn't + * a mixed long-vs-int compare. */ +#define RFC9802_TEST_MAX_CERT_SIZE ((long)(1L << 20)) + +/* Load a whole file into a freshly-allocated buffer. Caller frees. */ +static int rfc9802_load_file(const char* path, byte** out, int* outLen) +{ + EXPECT_DECLS; + XFILE f = XBADFILE; + long sz = 0; + size_t got = 0; + byte* buf = NULL; + + *out = NULL; + *outLen = 0; + ExpectTrue((f = XFOPEN(path, "rb")) != XBADFILE); + if (f == XBADFILE) + return TEST_FAIL; + if (XFSEEK(f, 0, XSEEK_END) == 0) + sz = XFTELL(f); + (void)XFSEEK(f, 0, XSEEK_SET); + ExpectIntGT(sz, 0); + ExpectIntLT(sz, RFC9802_TEST_MAX_CERT_SIZE); + /* Hard-fail before XMALLOC if XFSEEK / XFTELL produced an unusable + * size: ExpectInt* records the failure but doesn't short-circuit, + * so without this guard a -1 from XFTELL would cast to a multi-GiB + * (size_t) allocation, and a 0 would request a zero-byte malloc. */ + if (sz <= 0 || sz >= RFC9802_TEST_MAX_CERT_SIZE) { + XFCLOSE(f); + return TEST_FAIL; + } + ExpectNotNull(buf = (byte*)XMALLOC((size_t)sz, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + if (buf != NULL) { + got = XFREAD(buf, 1, (size_t)sz, f); + ExpectIntEQ(got, (size_t)sz); + /* On a short read the caller would otherwise proceed with a + * partially-initialized buffer and produce cascading parse + * failures driven by the uninitialized tail. Free here so the + * caller's `if (buf == NULL) return TEST_FAIL;` short-circuits + * cleanly with a single recorded failure. */ + if (got != (size_t)sz) { + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + buf = NULL; + sz = 0; + } + } + XFCLOSE(f); + *out = buf; + *outLen = (int)sz; + return EXPECT_RESULT(); +} + +static int rfc9802_verify_one_cert(const char* path, word32 expectedKeyOID, + word32 expectedSigOID) +{ + EXPECT_DECLS; + byte* buf = NULL; + byte* tampered = NULL; + int bytes = 0; + DecodedCert cert; + WOLFSSL_CERT_MANAGER* cm = NULL; + word32 certBegin = 0; + word32 sigIndex = 0; + + ExpectIntEQ(rfc9802_load_file(path, &buf, &bytes), TEST_SUCCESS); + if (buf == NULL) + return TEST_FAIL; + + /* Parse + check OIDs, capture certBegin and sigIndex for later tamper. */ + wc_InitDecodedCert(&cert, buf, (word32)bytes, NULL); + ExpectIntEQ(wc_ParseCert(&cert, CERT_TYPE, NO_VERIFY, NULL), 0); + ExpectIntEQ((int)cert.keyOID, (int)expectedKeyOID); + ExpectIntEQ((int)cert.signatureOID, (int)expectedSigOID); + certBegin = cert.certBegin; + sigIndex = cert.sigIndex; + wc_FreeDecodedCert(&cert); + + /* Full verify against a self-installed trust anchor. */ + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, buf, (long)bytes, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, buf, (long)bytes, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + + ExpectNotNull(tampered = (byte*)XMALLOC((size_t)bytes, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + + /* Negative 1: flip a byte inside the signatureValue BIT STRING. + * Everything after sigIndex is the signatureAlgorithm + the BIT + * STRING payload, so flipping the last byte is always inside the + * signature content. */ + if (tampered != NULL) { + XMEMCPY(tampered, buf, (size_t)bytes); + tampered[bytes - 1] ^= 0x01; + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, buf, (long)bytes, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, tampered, + (long)bytes, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + } + + /* Negative 2: flip a byte at the midpoint of the TBSCertificate. The + * TBS is the first element of the outer Certificate SEQUENCE and + * its bytes lie between (certBegin + outerSeqHeader) and sigIndex. + * Picking the midpoint ensures we're inside TBS regardless of the + * fixture's DN / extensions layout. */ + if (tampered != NULL && sigIndex > certBegin + 8u) { + word32 midTbs = certBegin + 8 + ((sigIndex - (certBegin + 8)) / 2); + XMEMCPY(tampered, buf, (size_t)bytes); + tampered[midTbs] ^= 0x01; + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, buf, (long)bytes, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, tampered, + (long)bytes, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + } + + /* The fixtures MUST carry a KeyUsage extension with at least one of + * digitalSignature / nonRepudiation / keyCertSign / cRLSign set per + * RFC 9802 sec 3. Re-parse and assert that wolfSSL recorded a non- + * empty set of KeyUsage bits from one of those values. */ + wc_InitDecodedCert(&cert, buf, (word32)bytes, NULL); + ExpectIntEQ(wc_ParseCert(&cert, CERT_TYPE, NO_VERIFY, NULL), 0); + ExpectIntEQ(cert.extKeyUsageSet, 1); + ExpectIntNE(cert.extKeyUsage & (KEYUSE_DIGITAL_SIG | KEYUSE_CONTENT_COMMIT | + KEYUSE_KEY_CERT_SIGN | KEYUSE_CRL_SIGN), 0); + wc_FreeDecodedCert(&cert); + + XFREE(tampered, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return EXPECT_RESULT(); +} +#endif + +/* Direct wolfCrypt-level negative tests for the parameter-derivation + * helpers used by the RFC 9802 parse path. These exercise failure modes + * (unknown algorithm bytes, truncated inputs, mismatches) that a real + * cert body wouldn't easily reach. */ +#if defined(WOLFSSL_HAVE_LMS) +static int rfc9802_lms_import_negative(void) +{ + EXPECT_DECLS; + LmsKey key; + /* 60-byte buffer matches HSS_PUBLIC_KEY_LEN(32), just like a valid + * SHA-256/M32/H5 key; the algorithm-type bytes are junk so param + * derivation must fail cleanly. */ + byte junk[60]; + + XMEMSET(junk, 0, sizeof(junk)); + /* levels=1, lmsType=0xFFFFFFFF, lmOtsType=0xFFFFFFFF. */ + junk[3] = 1; + XMEMSET(junk + 4, 0xFF, 4); + XMEMSET(junk + 8, 0xFF, 4); + + /* Unknown algorithm types must be rejected. */ + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_LmsKey_ImportPubRaw(&key, junk, sizeof(junk)), + WC_NO_ERR_TRACE(NOT_COMPILED_IN)); + wc_LmsKey_Free(&key); + + /* Too-short buffer: only L + lmsType, no lmOtsType. */ + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_LmsKey_ImportPubRaw(&key, junk, 8), + WC_NO_ERR_TRACE(BUFFER_E)); + wc_LmsKey_Free(&key); + +#if !defined(WOLFSSL_NO_LMS_SHA256_256) + /* The two cases below pin specific SHA-256/M32 parameter codes + * (L1_H5_W8, L1_H5_W4, L1_H10_W2). Skip them in builds where the + * SHA-256/M32 family is disabled -- the family-agnostic checks + * above (junk algorithm types, too-short buffer, GetSigLen on + * unconfigured key) still cover the universal invariants. */ + + /* Pre-set params that disagree with the raw key's algorithm bytes: + * configure H=5/W=8 but feed buffer that claims H=10 / W=2. */ + XMEMSET(junk, 0, sizeof(junk)); + junk[3] = 1; /* levels=1 */ + junk[7] = 6; /* lmsType = LMS_SHA256_M32_H10 = 6 */ + junk[11] = 2; /* lmOtsType = LMOTS_SHA256_N32_W2 = 2 */ + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_LmsKey_SetParameters(&key, 1, 5, 8), 0); + ExpectIntEQ(wc_LmsKey_ImportPubRaw(&key, junk, sizeof(junk)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_LmsKey_Free(&key); +#endif /* !WOLFSSL_NO_LMS_SHA256_256 */ + + /* GetSigLen on a key with no params set must not NULL-deref the + * params pointer; it must return BAD_FUNC_ARG instead. */ + { + word32 sigLen = 0; + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_LmsKey_GetSigLen(&key, &sigLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_LmsKey_Free(&key); + } + +#if !defined(WOLFSSL_NO_LMS_SHA256_256) + /* Partial-write invariant: a length mismatch after a successful + * auto-derive must leave key->params NULL. Build a buffer whose + * leading u32str(L) || lmsType || lmOtsType identifies a known + * parameter set, but truncate to one byte less than the real pub + * key length so the post-derive length check fails. */ + { + byte truncated[59]; /* HSS_PUBLIC_KEY_LEN(32) is 60 */ + XMEMSET(truncated, 0, sizeof(truncated)); + truncated[3] = 1; /* L = 1 */ + truncated[7] = 5; /* lmsType = LMS_SHA256_M32_H5 */ + truncated[11] = 4; /* lmOtsType = LMOTS_SHA256_N32_W4 */ + ExpectIntEQ(wc_LmsKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectNull(key.params); + ExpectIntEQ(wc_LmsKey_ImportPubRaw(&key, truncated, + sizeof(truncated)), WC_NO_ERR_TRACE(BUFFER_E)); + ExpectNull(key.params); + wc_LmsKey_Free(&key); + } +#endif /* !WOLFSSL_NO_LMS_SHA256_256 */ + + return EXPECT_RESULT(); +} +#endif + +#if defined(WOLFSSL_HAVE_XMSS) +static int rfc9802_xmss_import_negative(void) +{ + EXPECT_DECLS; + XmssKey key; + byte junk[8]; + + XMEMSET(junk, 0, sizeof(junk)); + + /* Too-short buffer. */ + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, junk, 2, 0), + WC_NO_ERR_TRACE(BUFFER_E)); + wc_XmssKey_Free(&key); + + /* Unknown OID (all-zero) for both XMSS and XMSS^MT. */ + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, junk, sizeof(junk), 0), + WC_NO_ERR_TRACE(NOT_COMPILED_IN)); + wc_XmssKey_Free(&key); + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, junk, sizeof(junk), 1), + WC_NO_ERR_TRACE(NOT_COMPILED_IN)); + wc_XmssKey_Free(&key); + + /* NULL key / input. */ + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(NULL, junk, sizeof(junk), 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, NULL, 8, 0), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_XmssKey_Free(&key); + + /* GetSigLen on a key with no params set must not NULL-deref the + * params pointer; it must return BAD_FUNC_ARG instead. */ + { + word32 sigLen = 0; + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_GetSigLen(&key, &sigLen), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_XmssKey_Free(&key); + } + + /* Once params have been configured (state != INITED), the OID + * prefix in the raw key MUST match key->oid and is_xmssmt MUST + * match key->is_xmssmt. Set XMSS-SHA2_10_256 and feed a valid- + * sized buffer whose 4-byte OID prefix is bogus -> BAD_FUNC_ARG. */ + { + byte mismatch[XMSS_SHA256_PUBLEN]; + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, "XMSS-SHA2_10_256"), 0); + XMEMSET(mismatch, 0, sizeof(mismatch)); + mismatch[3] = 0x77; /* nonsense OID */ + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, mismatch, + sizeof(mismatch), 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + /* Same buffer with the correct OID, but is_xmssmt hint + * contradicts the configured family -> BAD_FUNC_ARG. */ + mismatch[3] = 0x01; /* WC_XMSS_OID_SHA2_10_256 */ + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, mismatch, + sizeof(mismatch), 1), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + wc_XmssKey_Free(&key); + } + + /* Partial-write invariant: a length mismatch after a successful + * auto-derive must leave the key in its INITED state, with + * key->params NULL. */ + { + byte truncated[XMSS_SHA256_PUBLEN - 1]; + XMEMSET(truncated, 0, sizeof(truncated)); + truncated[3] = 0x01; + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectNull(key.params); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, truncated, + sizeof(truncated), 0), WC_NO_ERR_TRACE(BUFFER_E)); + ExpectNull(key.params); + wc_XmssKey_Free(&key); + } + + /* is_xmssmt disambiguation: XMSS oid=1 and XMSS^MT oid=1 share + * the wire-numeric value but resolve to different parameter sets. + * Importing the same 68-byte buffer with hint=0 vs hint=1 must + * land in different tables and produce distinct is_xmssmt. */ + { + byte buf[XMSS_SHA256_PUBLEN]; + XMEMSET(buf, 0, sizeof(buf)); + buf[3] = 0x01; + + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, buf, sizeof(buf), 0), 0); + ExpectIntEQ((int)key.is_xmssmt, 0); + wc_XmssKey_Free(&key); + + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, buf, sizeof(buf), 1), 0); + ExpectIntEQ((int)key.is_xmssmt, 1); + wc_XmssKey_Free(&key); + } + + /* Lenient state: re-importing the same pub key into a VERIFYONLY + * key (params set, no private material) succeeds. The second + * call exercises the lenient-state branch. */ + { + byte buf[XMSS_SHA256_PUBLEN]; + XMEMSET(buf, 0, sizeof(buf)); + buf[3] = 0x01; + + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, buf, sizeof(buf), 0), 0); + ExpectIntEQ((int)key.state, (int)WC_XMSS_STATE_VERIFYONLY); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, buf, sizeof(buf), 0), 0); + ExpectIntEQ((int)key.state, (int)WC_XMSS_STATE_VERIFYONLY); + wc_XmssKey_Free(&key); + } + + /* Strict signature-length check: wc_XmssKey_Verify rejects any + * sigLen != key->params->sig_len. This guards every consumer + * (RFC 9802 X.509, PKCS#7, CMS, ...) against a longer wrapper that + * happens to start with a valid signature. Construct a key in + * VERIFYONLY state, then verify with sig_len + 1 and sig_len - 1 + * byte buffers; both must fail with BUFFER_E before any crypto + * runs. The buffer contents are irrelevant since the length check + * fires first. */ + { + byte pub[XMSS_SHA256_PUBLEN]; + byte* sigBuf = NULL; + word32 sigLen = 0; + const byte msg[1] = { 0 }; + + XMEMSET(pub, 0, sizeof(pub)); + pub[3] = 0x01; + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, pub, sizeof(pub), 0), 0); + ExpectIntEQ((int)key.state, (int)WC_XMSS_STATE_VERIFYONLY); + ExpectIntEQ(wc_XmssKey_GetSigLen(&key, &sigLen), 0); + ExpectIntGT(sigLen, 0); + ExpectNotNull(sigBuf = (byte*)XMALLOC((size_t)sigLen + 1, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + if (sigBuf != NULL) { + XMEMSET(sigBuf, 0, (size_t)sigLen + 1); + ExpectIntEQ(wc_XmssKey_Verify(&key, sigBuf, sigLen + 1, + msg, (int)sizeof(msg)), WC_NO_ERR_TRACE(BUFFER_E)); + ExpectIntEQ(wc_XmssKey_Verify(&key, sigBuf, sigLen - 1, + msg, (int)sizeof(msg)), WC_NO_ERR_TRACE(BUFFER_E)); + XFREE(sigBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + wc_XmssKey_Free(&key); + } + + /* BAD_STATE_E branch: WC_XMSS_STATE_OK must be rejected. Reaching + * OK normally requires a successful private-key Reload / sign, + * which is unavailable in WOLFSSL_XMSS_VERIFY_ONLY builds. Force + * the state directly to exercise the rejection without coupling + * this helper to the signing test fixture; sk stays NULL so Free + * is still safe. */ + { + byte pub[XMSS_SHA256_PUBLEN]; + + XMEMSET(pub, 0, sizeof(pub)); + pub[3] = 0x01; + ExpectIntEQ(wc_XmssKey_Init(&key, NULL, INVALID_DEVID), 0); + ExpectIntEQ(wc_XmssKey_SetParamStr(&key, "XMSS-SHA2_10_256"), 0); + key.state = WC_XMSS_STATE_OK; + ExpectIntEQ(wc_XmssKey_ImportPubRaw_ex(&key, pub, sizeof(pub), 0), + WC_NO_ERR_TRACE(BAD_STATE_E)); + wc_XmssKey_Free(&key); + } + + return EXPECT_RESULT(); +} +#endif + +/* Walk the AlgorithmIdentifier SEQUENCE that begins at sigIndex and + * locate the byte offset of the last byte of its OID content. Handles + * both short-form (length < 128) and long-form DER length encodings, + * so a future fixture-regenerator that emits longer OIDs / SEQUENCEs + * still drives this test rather than tripping the loud-fail branch. + * + * Returns 0 on success with *oidLastByte set; returns -1 on any DER + * shape mismatch. */ +#if defined(WOLFSSL_HAVE_XMSS) && !defined(NO_FILESYSTEM) && !defined(NO_CERTS) +static int rfc9802_find_sig_alg_oid_last_byte(const byte* buf, word32 bufLen, + word32 sigIndex, word32* oidLastByte) +{ + word32 idx = sigIndex; + word32 oidContentLen = 0; + + /* AlgorithmIdentifier ::= SEQUENCE { algorithm OID, ... } */ + if (idx >= bufLen || buf[idx] != 0x30) + return -1; + idx++; + /* Skip SEQUENCE length (short or long form). */ + if (idx >= bufLen) + return -1; + if (buf[idx] < 0x80) { + idx++; + } + else { + word32 nbytes = (word32)(buf[idx] & 0x7F); + if (nbytes == 0 || nbytes > 4 || idx + 1 + nbytes > bufLen) + return -1; + idx += 1 + nbytes; + } + /* algorithm OID tag. */ + if (idx >= bufLen || buf[idx] != 0x06) + return -1; + idx++; + /* OID length (short or long form). */ + if (idx >= bufLen) + return -1; + if (buf[idx] < 0x80) { + oidContentLen = buf[idx]; + idx++; + } + else { + word32 nbytes = (word32)(buf[idx] & 0x7F); + word32 i; + if (nbytes == 0 || nbytes > 4 || idx + 1 + nbytes > bufLen) + return -1; + for (i = 0; i < nbytes; i++) + oidContentLen = (oidContentLen << 8) | buf[idx + 1 + i]; + idx += 1 + nbytes; + } + if (oidContentLen == 0 || idx + oidContentLen > bufLen) + return -1; + *oidLastByte = idx + oidContentLen - 1; + return 0; +} + +/* Helper: load fixture, locate last byte of outer signatureAlgorithm + * OID, patch it from `expected` to `swap`, and assert that verifying + * the patched cert against itself as a trust anchor fails. */ +static int rfc9802_assert_oid_patch_breaks_verify(const char* path, + byte expectedLastByte, byte patchedLastByte) +{ + EXPECT_DECLS; + byte* buf = NULL; + int bytes = 0; + DecodedCert cert; + WOLFSSL_CERT_MANAGER* cm = NULL; + word32 sigIndex = 0; + word32 lastOidByte = 0; + + ExpectIntEQ(rfc9802_load_file(path, &buf, &bytes), TEST_SUCCESS); + if (buf == NULL) + return TEST_FAIL; + + wc_InitDecodedCert(&cert, buf, (word32)bytes, NULL); + ExpectIntEQ(wc_ParseCert(&cert, CERT_TYPE, NO_VERIFY, NULL), 0); + sigIndex = cert.sigIndex; + wc_FreeDecodedCert(&cert); + + ExpectIntEQ(rfc9802_find_sig_alg_oid_last_byte(buf, (word32)bytes, + sigIndex, &lastOidByte), 0); + /* Sanity-check the fixture matches the family the caller asserted, + * so a future regenerator swapping fixtures fails loudly here + * rather than silently testing the wrong direction. */ + ExpectIntEQ((int)buf[lastOidByte], (int)expectedLastByte); + + if (lastOidByte < (word32)bytes && + buf[lastOidByte] == expectedLastByte) { + buf[lastOidByte] = patchedLastByte; + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + /* After the patch the cert's outer signatureAlgorithm and SPKI + * disagree. Verification must fail somewhere (at parse, at + * load, or at ConfirmSignature). The load is best-effort - + * some shape changes get caught there, others only at verify. */ + (void)wolfSSL_CertManagerLoadCABuffer(cm, buf, (long)bytes, + WOLFSSL_FILETYPE_ASN1); + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, buf, + (long)bytes, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + } + + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return EXPECT_RESULT(); +} + +/* X.509-level negative: swap the outer signatureAlgorithm OID byte so + * the cert declares XMSS where the SPKI is XMSS^MT, and vice versa. + * SigOidMatchesKeyOid must reject both directions before any crypto. */ +static int rfc9802_xmss_sig_oid_mismatch(void) +{ + EXPECT_DECLS; + /* XMSS sigOID ends 0x22; XMSS^MT sigOID ends 0x23. Patch each + * direction so the asymmetric-key path is exercised both ways - + * a regression that only stripped the check from one branch of + * SigOidMatchesKeyOid would otherwise be missed. */ + ExpectIntEQ(rfc9802_assert_oid_patch_breaks_verify( + "./certs/xmss/bc_xmss_sha2_10_256_root.der", + /* expected XMSS */ 0x22, /* patched to XMSS^MT */ 0x23), + TEST_SUCCESS); + ExpectIntEQ(rfc9802_assert_oid_patch_breaks_verify( + "./certs/xmss/bc_xmssmt_sha2_20_2_256_root.der", + /* expected XMSS^MT */ 0x23, /* patched to XMSS */ 0x22), + TEST_SUCCESS); + return EXPECT_RESULT(); +} +#endif + +/* Exercise a real CA -> leaf certificate chain, not just self-signed. + * Loads the CA as a trust anchor and verifies the leaf against it. */ +#if defined(WOLFSSL_HAVE_LMS) && !defined(NO_FILESYSTEM) && !defined(NO_CERTS) +static int rfc9802_lms_chain_verify(void) +{ + EXPECT_DECLS; + byte* caBuf = NULL; + byte* leafBuf = NULL; + int caLen = 0; + int leafLen = 0; + WOLFSSL_CERT_MANAGER* cm = NULL; + + ExpectIntEQ(rfc9802_load_file("./certs/lms/bc_lms_chain_ca.der", + &caBuf, &caLen), TEST_SUCCESS); + ExpectIntEQ(rfc9802_load_file("./certs/lms/bc_lms_chain_leaf.der", + &leafBuf, &leafLen), TEST_SUCCESS); + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + /* Only the CA is a trust anchor; the leaf is verified against it. */ + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, caBuf, (long)caLen, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, leafBuf, (long)leafLen, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + + /* Without loading the CA the leaf must NOT verify. */ + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, leafBuf, (long)leafLen, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + + XFREE(leafBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(caBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return EXPECT_RESULT(); +} +#endif + +/* Mirror of rfc9802_lms_chain_verify but for an XMSS CA -> leaf pair. */ +#if defined(WOLFSSL_HAVE_XMSS) && !defined(NO_FILESYSTEM) && !defined(NO_CERTS) +static int rfc9802_xmss_chain_verify(void) +{ + EXPECT_DECLS; + byte* caBuf = NULL; + byte* leafBuf = NULL; + int caLen = 0; + int leafLen = 0; + WOLFSSL_CERT_MANAGER* cm = NULL; + + ExpectIntEQ(rfc9802_load_file("./certs/xmss/bc_xmss_chain_ca.der", + &caBuf, &caLen), TEST_SUCCESS); + ExpectIntEQ(rfc9802_load_file("./certs/xmss/bc_xmss_chain_leaf.der", + &leafBuf, &leafLen), TEST_SUCCESS); + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, caBuf, (long)caLen, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, leafBuf, (long)leafLen, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntNE(wolfSSL_CertManagerVerifyBuffer(cm, leafBuf, (long)leafLen, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + if (cm != NULL) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } + + XFREE(leafBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(caBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return EXPECT_RESULT(); +} +#endif + +int test_rfc9802_lms_x509_verify(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_LMS) +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \ + !defined(WOLFSSL_NO_LMS_SHA256_256) + /* Mixed single-level LMS and multi-level HSS fixtures. The HSS + * public key carries only the top-level LMS/LM-OTS types, so + * wc_LmsKey_ImportPubRaw's auto-derive path searches the map + * by (levels, lmsType, lmOtsType). The bc_lms_native_bc_root + * fixture is generated through Bouncy Castle's stock + * JcaContentSignerBuilder("LMS") + JcaX509v3CertificateBuilder + * with no overrides; including it here is the cross-impl interop + * gate (BC's native LMS X.509 path is RFC 9802-compliant for HSS/ + * LMS, so wolfSSL must accept it end-to-end). + * + * All fixtures use the SHA-256/M32 family, so the whole block + * is gated on that family being compiled in. Truncated SHA-256/192 + * or SHAKE-only builds skip this block. */ + static const char* const lmsFiles[] = { + "./certs/lms/bc_lms_sha256_h5_w4_root.der", + "./certs/lms/bc_lms_sha256_h10_w8_root.der", + "./certs/lms/bc_hss_L2_H5_W8_root.der", + "./certs/lms/bc_hss_L3_H5_W4_root.der", + "./certs/lms/bc_lms_native_bc_root.der", + }; + size_t i; + for (i = 0; i < sizeof(lmsFiles) / sizeof(lmsFiles[0]); i++) { + ExpectIntEQ(rfc9802_verify_one_cert(lmsFiles[i], + HSS_LMSk, CTC_HSS_LMS), TEST_SUCCESS); + } + ExpectIntEQ(rfc9802_lms_chain_verify(), TEST_SUCCESS); +#endif /* !NO_FILESYSTEM && !NO_CERTS && !WOLFSSL_NO_LMS_SHA256_256 */ + /* Pure wolfCrypt-level negative tests don't need filesystem or cert + * support, so they run for any LMS-enabled build. */ + ExpectIntEQ(rfc9802_lms_import_negative(), TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +int test_rfc9802_xmss_x509_verify(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_HAVE_XMSS) +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) + static const char* const xmssFiles[] = { + "./certs/xmss/bc_xmss_sha2_10_256_root.der", + "./certs/xmss/bc_xmss_sha2_16_256_root.der", + }; + static const char* const xmssmtFiles[] = { + "./certs/xmss/bc_xmssmt_sha2_20_2_256_root.der", + "./certs/xmss/bc_xmssmt_sha2_20_4_256_root.der", + "./certs/xmss/bc_xmssmt_sha2_40_8_256_root.der", + }; + size_t i; + for (i = 0; i < sizeof(xmssFiles) / sizeof(xmssFiles[0]); i++) { + ExpectIntEQ(rfc9802_verify_one_cert(xmssFiles[i], + XMSSk, CTC_XMSS), TEST_SUCCESS); + } + for (i = 0; i < sizeof(xmssmtFiles) / sizeof(xmssmtFiles[0]); i++) { + ExpectIntEQ(rfc9802_verify_one_cert(xmssmtFiles[i], + XMSSMTk, CTC_XMSSMT), TEST_SUCCESS); + } + ExpectIntEQ(rfc9802_xmss_sig_oid_mismatch(), TEST_SUCCESS); + ExpectIntEQ(rfc9802_xmss_chain_verify(), TEST_SUCCESS); +#endif /* !NO_FILESYSTEM && !NO_CERTS */ + /* Pure wolfCrypt-level negative tests don't need filesystem or cert + * support, so they run for any XMSS-enabled build. */ + ExpectIntEQ(rfc9802_xmss_import_negative(), TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + #if defined(WOLFSSL_SNIFFER) && defined(WOLFSSL_SNIFFER_CHAIN_INPUT) static int test_sniffer_chain_input_overflow(void) { @@ -39153,6 +39871,10 @@ TEST_CASE testCases[] = { TEST_DECL_GROUP("lms", test_wc_LmsKey_sign_verify), TEST_DECL_GROUP("lms", test_wc_LmsKey_reload_cache), + /* RFC 9802 (HSS/LMS and XMSS/XMSS^MT in X.509) */ + TEST_DECL_GROUP("lms", test_rfc9802_lms_x509_verify), + TEST_DECL_GROUP("xmss", test_rfc9802_xmss_x509_verify), + /* PEM and DER APIs. */ TEST_DECL(test_wc_PemToDer), TEST_DECL(test_wc_AllocDer), diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 6a7c6d4f676..48e71459a21 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -4647,6 +4647,17 @@ static int ParseCRL_Extensions(DecodedCRL* dcrl, const byte* buf, word32* inOutI /* SLH-DSA-SHAKE-256f: 2.16.840.1.101.3.4.3.31 */ static const byte sigSlhDsa_Shake_256fOid[] = {96, 134, 72, 1, 101, 3, 4, 3, 31}; #endif /* WOLFSSL_HAVE_SLHDSA */ +#ifdef WOLFSSL_HAVE_LMS + /* RFC 9802 id-alg-hss-lms-hashsig: 1.2.840.113549.1.9.16.3.17 */ + static const byte sigHssLmsOid[] = + {42, 134, 72, 134, 247, 13, 1, 9, 16, 3, 17}; +#endif /* WOLFSSL_HAVE_LMS */ +#ifdef WOLFSSL_HAVE_XMSS + /* RFC 9802 id-alg-xmss-hashsig: 1.3.6.1.5.5.7.6.34 */ + static const byte sigXmssOid[] = {43, 6, 1, 5, 5, 7, 6, 34}; + /* RFC 9802 id-alg-xmssmt-hashsig: 1.3.6.1.5.5.7.6.35 */ + static const byte sigXmssMtOid[] = {43, 6, 1, 5, 5, 7, 6, 35}; +#endif /* WOLFSSL_HAVE_XMSS */ /* keyType */ #ifndef NO_DSA @@ -4978,6 +4989,17 @@ static int SlhDsaParamToKeyType(enum SlhDsaParam param) } #endif /* WOLFSSL_CERT_GEN */ #endif /* WOLFSSL_HAVE_SLHDSA */ +#ifdef WOLFSSL_HAVE_LMS + /* RFC 9802 id-alg-hss-lms-hashsig: 1.2.840.113549.1.9.16.3.17 */ + static const byte keyHssLmsOid[] = + {42, 134, 72, 134, 247, 13, 1, 9, 16, 3, 17}; +#endif /* WOLFSSL_HAVE_LMS */ +#ifdef WOLFSSL_HAVE_XMSS + /* RFC 9802 id-alg-xmss-hashsig: 1.3.6.1.5.5.7.6.34 */ + static const byte keyXmssOid[] = {43, 6, 1, 5, 5, 7, 6, 34}; + /* RFC 9802 id-alg-xmssmt-hashsig: 1.3.6.1.5.5.7.6.35 */ + static const byte keyXmssMtOid[] = {43, 6, 1, 5, 5, 7, 6, 35}; +#endif /* WOLFSSL_HAVE_XMSS */ /* curveType */ #ifdef HAVE_ECC @@ -5870,6 +5892,22 @@ const byte* OidFromId(word32 id, word32 type, word32* oidSz) *oidSz = sizeof(sigSlhDsa_Shake_256fOid); break; #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case CTC_HSS_LMS: + oid = sigHssLmsOid; + *oidSz = sizeof(sigHssLmsOid); + break; + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case CTC_XMSS: + oid = sigXmssOid; + *oidSz = sizeof(sigXmssOid); + break; + case CTC_XMSSMT: + oid = sigXmssMtOid; + *oidSz = sizeof(sigXmssMtOid); + break; + #endif /* WOLFSSL_HAVE_XMSS */ default: break; } @@ -6019,6 +6057,22 @@ const byte* OidFromId(word32 id, word32 type, word32* oidSz) *oidSz = sizeof(keySlhDsa_Shake_256fOid); break; #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + oid = keyHssLmsOid; + *oidSz = sizeof(keyHssLmsOid); + break; + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + oid = keyXmssOid; + *oidSz = sizeof(keyXmssOid); + break; + case XMSSMTk: + oid = keyXmssMtOid; + *oidSz = sizeof(keyXmssMtOid); + break; + #endif /* WOLFSSL_HAVE_XMSS */ default: break; } @@ -12465,7 +12519,8 @@ void wc_FreeDecodedCert(DecodedCert* cert) } #if defined(HAVE_ED25519) || defined(HAVE_ED448) || defined(HAVE_FALCON) || \ - defined(HAVE_DILITHIUM) || defined(WOLFSSL_HAVE_SLHDSA) + defined(HAVE_DILITHIUM) || defined(WOLFSSL_HAVE_SLHDSA) || \ + defined(WOLFSSL_HAVE_LMS) || defined(WOLFSSL_HAVE_XMSS) /* Store the key data under the BIT_STRING in dynamically allocated data. * * @param [in, out] cert Certificate object. @@ -13386,6 +13441,22 @@ static int GetCertKey(DecodedCert* cert, const byte* source, word32* inOutIdx, ret = StoreKey(cert, source, &srcIdx, maxIdx); break; #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + cert->pkCurveOID = HSS_LMSk; + ret = StoreKey(cert, source, &srcIdx, maxIdx); + break; + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + cert->pkCurveOID = XMSSk; + ret = StoreKey(cert, source, &srcIdx, maxIdx); + break; + case XMSSMTk: + cert->pkCurveOID = XMSSMTk; + ret = StoreKey(cert, source, &srcIdx, maxIdx); + break; + #endif /* WOLFSSL_HAVE_XMSS */ #ifndef NO_DSA case DSAk: cert->publicKey = source + pubIdx; @@ -15805,14 +15876,16 @@ static WC_INLINE int IsSigAlgoECDSA(word32 algoOID) } #endif -/* Determines if OID is for an EC signing algorithm including ECDSA and EdDSA - * and post-quantum algorithms. +/* Determines whether the signature algorithm's AlgorithmIdentifier omits + * the trailing NULL parameters element. True for ECC / EdDSA / SM2 and + * for the post-quantum families (Falcon, ML-DSA , SLH-DSA, LMS, XMSS). * * @param [in] algoOID Algorithm OID. - * @return 1 when is EC signing algorithm. + * @return 1 when the algorithm encodes its AlgorithmIdentifier without + * a NULL parameters element. * @return 0 otherwise. */ -static WC_INLINE int IsSigAlgoECC(word32 algoOID) +static WC_INLINE int IsSigAlgoNoParams(word32 algoOID) { (void)algoOID; @@ -15863,6 +15936,13 @@ static WC_INLINE int IsSigAlgoECC(word32 algoOID) || (algoOID == SLH_DSA_SHA2_192Sk) || (algoOID == SLH_DSA_SHA2_256Sk) #endif + #ifdef WOLFSSL_HAVE_LMS + || (algoOID == HSS_LMSk) + #endif + #ifdef WOLFSSL_HAVE_XMSS + || (algoOID == XMSSk) + || (algoOID == XMSSMTk) + #endif ); } @@ -15907,7 +15987,7 @@ static word32 SetAlgoIDImpl(int algoOID, byte* output, int type, int curveSz, SetASN_OID(&dataASN[ALGOIDASN_IDX_OID], (word32)algoOID, (word32)type); /* Hashes, signatures not ECC and keys not RSA output NULL tag. */ if (!(type == oidHashType || - (type == oidSigType && !IsSigAlgoECC((word32)algoOID)) || + (type == oidSigType && !IsSigAlgoNoParams((word32)algoOID)) || (type == oidKeyType && algoOID == RSAk))) { /* Don't put out NULL DER item. */ dataASN[ALGOIDASN_IDX_NULL].noOut = 1; @@ -16218,6 +16298,25 @@ void FreeSignatureCtx(SignatureCtx* sigCtx) #endif break; #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + wc_LmsKey_Free(sigCtx->key.lms); + #ifndef WOLFSSL_NO_MALLOC + XFREE(sigCtx->key.lms, sigCtx->heap, DYNAMIC_TYPE_LMS); + sigCtx->key.lms = NULL; + #endif + break; + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + case XMSSMTk: + wc_XmssKey_Free(sigCtx->key.xmss); + #ifndef WOLFSSL_NO_MALLOC + XFREE(sigCtx->key.xmss, sigCtx->heap, DYNAMIC_TYPE_XMSS); + sigCtx->key.xmss = NULL; + #endif + break; + #endif /* WOLFSSL_HAVE_XMSS */ default: break; } /* switch (keyOID) */ @@ -16416,6 +16515,17 @@ static int HashForSignature(const byte* buf, word32 bufSz, word32 sigOID, /* Hashes done in signing operation. */ break; #endif + #ifdef WOLFSSL_HAVE_LMS + case CTC_HSS_LMS: + /* RFC 9802 sec 2: no digest is applied before signing. */ + break; + #endif + #ifdef WOLFSSL_HAVE_XMSS + case CTC_XMSS: + case CTC_XMSSMT: + /* RFC 9802 sec 2: no digest is applied before signing. */ + break; + #endif default: ret = HASH_TYPE_E; @@ -16613,6 +16723,16 @@ static int SigOidMatchesKeyOid(word32 sigOID, word32 keyOID) case SLH_DSA_SHA2_256Sk: return (sigOID == CTC_SLH_DSA_SHA2_256S); #endif + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + return (sigOID == CTC_HSS_LMS); + #endif + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + return (sigOID == CTC_XMSS); + case XMSSMTk: + return (sigOID == CTC_XMSSMT); + #endif } /* Default to reject unknown key types */ @@ -17141,6 +17261,54 @@ int ConfirmSignature(SignatureCtx* sigCtx, break; } #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + { + sigCtx->verify = 0; + #ifndef WOLFSSL_NO_MALLOC + sigCtx->key.lms = (LmsKey*)XMALLOC(sizeof(LmsKey), + sigCtx->heap, DYNAMIC_TYPE_LMS); + if (sigCtx->key.lms == NULL) { + ERROR_OUT(MEMORY_E, exit_cs); + } + #endif + if ((ret = wc_LmsKey_Init(sigCtx->key.lms, + sigCtx->heap, sigCtx->devId)) < 0) { + goto exit_cs; + } + if ((ret = wc_LmsKey_ImportPubRaw(sigCtx->key.lms, + key, keySz)) < 0) { + WOLFSSL_MSG("ASN Key import error HSS/LMS"); + goto exit_cs; + } + break; + } + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + case XMSSMTk: + { + int is_xmssmt = (keyOID == XMSSMTk); + sigCtx->verify = 0; + #ifndef WOLFSSL_NO_MALLOC + sigCtx->key.xmss = (XmssKey*)XMALLOC(sizeof(XmssKey), + sigCtx->heap, DYNAMIC_TYPE_XMSS); + if (sigCtx->key.xmss == NULL) { + ERROR_OUT(MEMORY_E, exit_cs); + } + #endif + if ((ret = wc_XmssKey_Init(sigCtx->key.xmss, + sigCtx->heap, sigCtx->devId)) < 0) { + goto exit_cs; + } + if ((ret = wc_XmssKey_ImportPubRaw_ex(sigCtx->key.xmss, + key, keySz, is_xmssmt)) < 0) { + WOLFSSL_MSG("ASN Key import error XMSS/XMSS^MT"); + goto exit_cs; + } + break; + } + #endif /* WOLFSSL_HAVE_XMSS */ default: WOLFSSL_MSG("Verify Key type unknown"); ret = ASN_UNKNOWN_OID_E; @@ -17353,6 +17521,25 @@ int ConfirmSignature(SignatureCtx* sigCtx, break; } #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + { + ret = wc_LmsKey_Verify(sigCtx->key.lms, sig, sigSz, + buf, (int)bufSz); + sigCtx->verify = (ret == 0); + break; + } + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + case XMSSMTk: + { + ret = wc_XmssKey_Verify(sigCtx->key.xmss, sig, sigSz, + buf, (int)bufSz); + sigCtx->verify = (ret == 0); + break; + } + #endif /* WOLFSSL_HAVE_XMSS */ default: break; } /* switch (keyOID) */ @@ -17575,6 +17762,33 @@ int ConfirmSignature(SignatureCtx* sigCtx, break; } #endif /* WOLFSSL_HAVE_SLHDSA */ + #ifdef WOLFSSL_HAVE_LMS + case HSS_LMSk: + { + if (sigCtx->verify == 1) { + ret = 0; + } + else { + WOLFSSL_MSG("HSS/LMS Verify didn't match"); + ret = ASN_SIG_CONFIRM_E; + } + break; + } + #endif /* WOLFSSL_HAVE_LMS */ + #ifdef WOLFSSL_HAVE_XMSS + case XMSSk: + case XMSSMTk: + { + if (sigCtx->verify == 1) { + ret = 0; + } + else { + WOLFSSL_MSG("XMSS/XMSS^MT Verify didn't match"); + ret = ASN_SIG_CONFIRM_E; + } + break; + } + #endif /* WOLFSSL_HAVE_XMSS */ default: break; } /* switch (keyOID) */ @@ -21237,7 +21451,7 @@ static int DecodeCertInternal(DecodedCert* cert, int verify, int* criticalExt, ret = ASN_SIG_OID_E; } /* Parameters not allowed after ECDSA or EdDSA algorithm OID. */ - else if (IsSigAlgoECC(cert->signatureOID)) { + else if (IsSigAlgoNoParams(cert->signatureOID)) { #ifndef WOLFSSL_ECC_SIGALG_PARAMS_NULL_ALLOWED if (dataASN[X509CERTASN_IDX_SIGALGO_PARAMS_NULL].tag != 0) { WOLFSSL_ERROR_VERBOSE(ASN_PARSE_E); @@ -27917,7 +28131,7 @@ int AddSignature(byte* buf, int bodySz, const byte* sig, int sigSz, } } if (ret == 0) { - if (IsSigAlgoECC((word32)sigAlgoType)) { + if (IsSigAlgoNoParams((word32)sigAlgoType)) { /* ECDSA and EdDSA doesn't have NULL tagged item. */ dataASN[SIGASN_IDX_SIGALGO_NULL].noOut = 1; } @@ -28128,7 +28342,7 @@ static int MakeAnyCert(Cert* cert, byte* derBuffer, word32 derSz, (word32)cert->sigType, oidSigType); } - if (IsSigAlgoECC((word32)cert->sigType)) { + if (IsSigAlgoNoParams((word32)cert->sigType)) { /* No NULL tagged item with ECDSA and EdDSA signature OIDs. */ dataASN[X509CERTASN_IDX_TBS_ALGOID_PARAMS_NULL].noOut = 1; } @@ -37972,7 +38186,7 @@ int ParseX509Acert(DecodedAcert* acert, int verify) } /* Parameters not allowed after ECDSA or EdDSA algorithm OID. */ - if (IsSigAlgoECC(acert->signatureOID)) { + if (IsSigAlgoNoParams(acert->signatureOID)) { if ((dataASN[ACERT_IDX_SIGALGO_PARAMS_NULL].tag != 0) #ifdef WC_RSA_PSS || (dataASN[ACERT_IDX_SIGALGO_PARAMS].tag != 0) diff --git a/wolfcrypt/src/asn_orig.c b/wolfcrypt/src/asn_orig.c index 4572cfaa29e..7a72a8f7760 100644 --- a/wolfcrypt/src/asn_orig.c +++ b/wolfcrypt/src/asn_orig.c @@ -2947,7 +2947,7 @@ static word32 SetAlgoIDImpl(int algoOID, byte* output, int type, int curveSz, word32 length = 0; tagSz = ((type == oidHashType || - (type == oidSigType && !IsSigAlgoECC((word32)algoOID)) || + (type == oidSigType && !IsSigAlgoNoParams((word32)algoOID)) || (type == oidKeyType && algoOID == RSAk)) && (absentParams == FALSE)) ? 2U : 0U; algoName = OidFromId((word32)algoOID, (word32)type, &algoSz); diff --git a/wolfcrypt/src/wc_lms.c b/wolfcrypt/src/wc_lms.c index 9268f85bea4..7f8ad2a5976 100644 --- a/wolfcrypt/src/wc_lms.c +++ b/wolfcrypt/src/wc_lms.c @@ -1418,39 +1418,127 @@ int wc_LmsKey_ExportPubRaw(const LmsKey* key, byte* out, word32* outLen) /* Imports a raw public key buffer from in array to LmsKey key. * - * The LMS parameters must be set first with wc_LmsKey_SetLmsParm or - * wc_LmsKey_SetParameters, and inLen must match the length returned - * by wc_LmsKey_GetPubLen. + * If the LMS parameters have already been configured (via + * wc_LmsKey_SetLmsParm or wc_LmsKey_SetParameters), the levels / + * lms_algorithm_type / lmots_algorithm_type encoded in the raw key are + * checked for consistency and inLen must match wc_LmsKey_GetPubLen. * - * Call wc_LmsKey_GetPubLen beforehand to determine pubLen. + * If the parameters have not yet been set (key->params == NULL), they + * are derived from the raw public key prefix (RFC 8554 sec 3.3 / sec + * 6.1: u32str(L) || lms_algorithm_type || lmots_algorithm_type) and + * matched against the static parameter map. The candidate is held in + * a local until the length check passes, so a length mismatch leaves + * key->params NULL. + * + * Accepts a key in INITED, PARMSET or VERIFYONLY state. WC_LMS_STATE_OK + * is rejected because the key already has private material loaded and + * silently overwriting key->pub would create an inconsistent priv/pub + * pair. * * @param [in, out] key LMS key to put public key in. * @param [in] in Buffer holding encoded public key. * @param [in] inLen Length of encoded public key in bytes. * @return 0 on success. - * @return BAD_FUNC_ARG when key or in is NULL. - * @return BUFFER_E when inLen does not match public key length by parameters. + * @return BAD_FUNC_ARG when key or in is NULL, or when the raw key's + * levels / lmsType / lmOtsType disagree with pre-set params. + * @return BAD_STATE_E when wrong state for operation. + * @return BUFFER_E when inLen is too small to contain the LMS type + * fields, or doesn't match the public key length determined + * by parameters. + * @return NOT_COMPILED_IN when the derived parameter set isn't built in. */ int wc_LmsKey_ImportPubRaw(LmsKey* key, const byte* in, word32 inLen) { - int ret = 0; + int ret = 0; + const LmsParams* matched = NULL; /* Validate parameters. */ if ((key == NULL) || (in == NULL)) { ret = BAD_FUNC_ARG; } + /* Reject states where re-importing the public bytes would desync + * the key. INITED (params unset, will derive), PARMSET (params set + * but no private material) and VERIFYONLY (already a pub-only key) + * are all safe. OK means a private key is loaded; silently + * overwriting key->pub would create a priv/pub mismatch. Mirrors + * the wc_XmssKey_ImportPubRaw_ex post-condition added in the same + * RFC 9802 series. */ if ((ret == 0) && - (inLen != (word32)HSS_PUBLIC_KEY_LEN(key->params->hash_len))) { - /* Something inconsistent. Parameters weren't set, or input - * pub key is wrong.*/ - return BUFFER_E; + (key->state != WC_LMS_STATE_INITED) && + (key->state != WC_LMS_STATE_PARMSET) && + (key->state != WC_LMS_STATE_VERIFYONLY)) { + WOLFSSL_MSG("error: LMS key not ready for import"); + ret = BAD_STATE_E; + } + /* Need at least L || lmsType || lmOtsType to derive or validate. */ + if ((ret == 0) && (inLen < (word32)(LMS_L_LEN + 2 * LMS_TYPE_LEN))) { + ret = BUFFER_E; } if (ret == 0) { - XMEMCPY(key->pub, in, inLen); + word32 levels = 0; + word32 lmsType = 0; + word32 lmOtsType = 0; + + /* RFC 8554 sec 3.3 / sec 6.1: HSS public key = u32str(L) || pub[0], + * where pub[0] starts with lms_algorithm_type || lmots_algorithm_type. + */ + ato32(in + 0, &levels); + ato32(in + LMS_L_LEN, &lmsType); + ato32(in + LMS_L_LEN + LMS_TYPE_LEN, &lmOtsType); + + /* The wire format carries only the RFC type code (low 12 bits); + * params->lmsType / lmOtsType also pack a wolfSSL-internal hash + * family flag in the high 4 bits (LMS_HASH_MASK). Compare on the + * RFC code only -- safe as long as low-12-bit codes stay globally + * distinct across hash families (see wc_lms_impl.c step 3.d-e + * note). */ + if (key->params == NULL) { + /* Auto-derive: find matching entry in the static map. Hold + * the candidate in a local until the length check passes to + * avoid leaving key->params half-set on failure. */ + int i; + ret = WC_NO_ERR_TRACE(NOT_COMPILED_IN); + for (i = 0; i < WC_LMS_MAP_LEN; i++) { + if (((word32)wc_lms_map[i].params.levels == levels) && + ((word32)(wc_lms_map[i].params.lmsType & LMS_H_W_MASK) == + lmsType) && + ((word32)(wc_lms_map[i].params.lmOtsType& LMS_H_W_MASK) == + lmOtsType)) { + matched = &wc_lms_map[i].params; + ret = 0; + break; + } + } + if (ret != 0) { + WOLFSSL_MSG("error: LMS params from pub key not supported"); + } + } + else { + /* Validate against pre-set params. */ + if (((word32)key->params->levels != levels) || + ((word32)(key->params->lmsType & LMS_H_W_MASK) != lmsType) || + ((word32)(key->params->lmOtsType & LMS_H_W_MASK) != lmOtsType)){ + WOLFSSL_MSG("error: LMS pub key doesn't match set params"); + ret = BAD_FUNC_ARG; + } + else { + matched = key->params; + } + } + } + if ((ret == 0) && + (inLen != (word32)HSS_PUBLIC_KEY_LEN(matched->hash_len))) { + ret = BUFFER_E; + } - if (key->state != WC_LMS_STATE_OK) - key->state = WC_LMS_STATE_VERIFYONLY; + if (ret == 0) { + /* Commit params (no-op when already set) and copy the key. + * State is INITED/PARMSET/VERIFYONLY here (OK is rejected + * above), so promoting to VERIFYONLY is always correct. */ + key->params = matched; + XMEMCPY(key->pub, in, inLen); + key->state = WC_LMS_STATE_VERIFYONLY; } return ret; @@ -1465,14 +1553,15 @@ int wc_LmsKey_ImportPubRaw(LmsKey* key, const byte* in, word32 inLen) * @param [in] key LMS key. * @param [out] len Length of a signature in bytes. * @return 0 on success. - * @return BAD_FUNC_ARG when key or len is NULL. + * @return BAD_FUNC_ARG when key or len is NULL, or when the LMS + * parameters have not been configured on the key. */ int wc_LmsKey_GetSigLen(const LmsKey* key, word32* len) { int ret = 0; /* Validate parameters. */ - if ((key == NULL) || (len == NULL)) { + if ((key == NULL) || (len == NULL) || (key->params == NULL)) { ret = BAD_FUNC_ARG; } diff --git a/wolfcrypt/src/wc_xmss.c b/wolfcrypt/src/wc_xmss.c index 58019c787bb..c3f4dbaa399 100644 --- a/wolfcrypt/src/wc_xmss.c +++ b/wolfcrypt/src/wc_xmss.c @@ -1493,10 +1493,156 @@ int wc_XmssKey_ExportPubRaw(const XmssKey* key, byte* out, word32* outLen) return ret; } +/* Imports a raw public key buffer from in array to XmssKey key, taking + * an is_xmssmt hint to disambiguate the XMSS / XMSS^MT OID namespaces + * when params have not yet been configured on the key. + * + * Accepts a key in INITED, PARMSET or VERIFYONLY state. WC_XMSS_STATE_OK + * is rejected because the key already has private material loaded and + * silently overwriting key->pk would create an inconsistent priv/pub + * pair. When state is INITED, params are derived from the 4-byte OID + * prefix at the start of the raw key (RFC 8391 Appendix B.1 / C.1) + * using is_xmssmt to pick the XMSS or XMSS^MT table; key->oid, + * key->is_xmssmt and key->params are populated only after the public + * key length check passes, so a length mismatch leaves the key in its + * original state. When params have already been set, the 4-byte OID + * prefix and the is_xmssmt hint are checked for consistency. + * + * @param [in, out] key XMSS key. + * @param [in] in Array holding public key. + * @param [in] inLen Length of array in bytes. + * @param [in] is_xmssmt 0 to search the XMSS table, non-zero to + * search the XMSS^MT table. + * + * @return 0 on success. + * @return BAD_FUNC_ARG when a parameter is NULL or the OID prefix / + * is_xmssmt hint contradicts pre-set params. + * @return BUFFER_E if array is incorrect size. + * @return BAD_STATE_E when wrong state for operation. + * @return NOT_COMPILED_IN when the derived parameter set isn't built in. + */ +int wc_XmssKey_ImportPubRaw_ex(XmssKey* key, const byte* in, word32 inLen, + int is_xmssmt) +{ + int ret = 0; + word32 oid = 0; + const XmssParams* matched = NULL; + + /* Validate parameters. */ + if ((key == NULL) || (in == NULL)) { + ret = BAD_FUNC_ARG; + } + if ((ret == 0) && (inLen < XMSS_OID_LEN)) { + ret = BUFFER_E; + } + + /* Reject states where the key is unusable for re-import. INITED + * means params are unset (we'll derive them); PARMSET / VERIFYONLY + * means params are set without a working private key (we just + * overwrite the pub bytes). OK means a private key is already + * loaded; overwriting key->pk silently would desync priv/pub. */ + if ((ret == 0) && + (key->state != WC_XMSS_STATE_INITED) && + (key->state != WC_XMSS_STATE_PARMSET) && + (key->state != WC_XMSS_STATE_VERIFYONLY)) { + WOLFSSL_MSG("error: XMSS key not ready for import"); + ret = BAD_STATE_E; + } + + if (ret == 0) { + /* OID is encoded big-endian in the first 4 bytes. */ + ato32(in, &oid); + + if (key->state == WC_XMSS_STATE_INITED) { + /* Auto-derive params from OID prefix, using is_xmssmt hint. + * Hold the candidate in a local; commit to the key only + * after the length check below succeeds. The compile-time + * gates here mirror the wc_xmss_alg / wc_xmssmt_alg table + * definitions exactly, so a build with one family disabled + * still rejects pubkeys for that family with NOT_COMPILED_IN + * rather than referring to undefined WC_*_ALG_LEN. */ + ret = WC_NO_ERR_TRACE(NOT_COMPILED_IN); + if (is_xmssmt) { + #if WOLFSSL_XMSS_MAX_HEIGHT >= 20 + unsigned int i; + for (i = 0; i < WC_XMSSMT_ALG_LEN; i++) { + if (wc_xmssmt_alg[i].oid == oid) { + matched = &wc_xmssmt_alg[i].params; + ret = 0; + break; + } + } + #else + /* XMSS^MT disabled at compile time; ret stays at + * NOT_COMPILED_IN. */ + (void)oid; + #endif + } + else { + #if WOLFSSL_XMSS_MIN_HEIGHT <= 20 + unsigned int i; + for (i = 0; i < WC_XMSS_ALG_LEN; i++) { + if (wc_xmss_alg[i].oid == oid) { + matched = &wc_xmss_alg[i].params; + ret = 0; + break; + } + } + #else + /* XMSS disabled at compile time; ret stays at + * NOT_COMPILED_IN. */ + (void)oid; + #endif + } + + if (ret != 0) { + WOLFSSL_MSG("error: XMSS OID from pub key not supported"); + } + } + else { + /* Params already set; OID prefix and family must match. */ + if (oid != key->oid) { + WOLFSSL_MSG("error: XMSS pub OID doesn't match set params"); + ret = BAD_FUNC_ARG; + } + else if ((is_xmssmt ? 1 : 0) != key->is_xmssmt) { + WOLFSSL_MSG("error: XMSS is_xmssmt hint contradicts set params"); + ret = BAD_FUNC_ARG; + } + else { + matched = key->params; + } + } + } + + /* Length check using the candidate (auto-derived) or pre-set + * params, without committing yet. */ + if ((ret == 0) && (inLen != (word32)(XMSS_OID_LEN + matched->pk_len))) { + ret = BUFFER_E; + } + + if (ret == 0) { + /* Commit (no-op when params were already set) and copy pub + * bytes (skipping the OID prefix). */ + if (key->state == WC_XMSS_STATE_INITED) { + key->params = matched; + key->oid = oid; + key->is_xmssmt = is_xmssmt ? 1 : 0; + } + XMEMCPY(key->pk, in + XMSS_OID_LEN, matched->pk_len); + key->state = WC_XMSS_STATE_VERIFYONLY; + } + + return ret; +} + /* Imports a raw public key buffer from in array to XmssKey key. * * The XMSS parameters must be set first with wc_XmssKey_SetParamStr, * and inLen must match the length returned by wc_XmssKey_GetPubLen. + * If the caller only has the raw public-key bytes and has not yet + * configured the parameter set, use wc_XmssKey_ImportPubRaw_ex which + * derives parameters from the OID prefix at the start of the buffer. * * @param [in, out] key XMSS key. * @param [in] in Array holding public key. @@ -1565,12 +1711,13 @@ int wc_XmssKey_GetSigLen(const XmssKey* key, word32* len) int ret = 0; /* Validate parameters. */ - if ((key == NULL) || (len == NULL)) { + if ((key == NULL) || (len == NULL) || (key->params == NULL)) { ret = BAD_FUNC_ARG; } /* Validate state. */ if ((ret == 0) && (key->state != WC_XMSS_STATE_OK) && - (key->state != WC_XMSS_STATE_PARMSET)) { + (key->state != WC_XMSS_STATE_PARMSET) && + (key->state != WC_XMSS_STATE_VERIFYONLY)) { ret = BAD_STATE_E; } @@ -1601,7 +1748,8 @@ int wc_XmssKey_GetSigLen(const XmssKey* key, word32* len) * @return SIG_VERIFY_E when signature did not verify message. * @return BAD_FUNC_ARG when a parameter is NULL. * @return BAD_STATE_E when wrong state for operation. - * @return BUFFER_E when sigLen is too small. + * @return BUFFER_E when sigLen does not exactly match the parameter-set + * signature length (use wc_XmssKey_GetSigLen). */ int wc_XmssKey_Verify(XmssKey* key, const byte* sig, word32 sigLen, const byte* m, int mLen) @@ -1620,9 +1768,10 @@ int wc_XmssKey_Verify(XmssKey* key, const byte* sig, word32 sigLen, WOLFSSL_MSG("error: XMSS key not ready for verification"); ret = BAD_STATE_E; } - /* Check the signature is the big enough. */ - if ((ret == 0) && (sigLen < key->params->sig_len)) { - /* Signature buffer too small. */ + /* Check the signature length is exactly the parameter-set size. + * XMSS / XMSS^MT signatures are fixed-length per parameter set, so + * any buffer that's longer or shorter than sig_len is malformed. */ + if ((ret == 0) && (sigLen != key->params->sig_len)) { ret = BUFFER_E; } diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index aee161ec65b..651b2d189db 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -70,6 +70,12 @@ that can be serialized and deserialized in a cross-platform way. #ifdef WOLFSSL_HAVE_SLHDSA #include #endif +#ifdef WOLFSSL_HAVE_LMS + #include +#endif +#ifdef WOLFSSL_HAVE_XMSS + #include +#endif #ifdef HAVE_FALCON #include #endif @@ -1542,7 +1548,8 @@ struct SignatureCtx { #endif #if defined(HAVE_ECC) || defined(HAVE_ED25519) || defined(HAVE_ED448) || \ !defined(NO_DSA) || defined(HAVE_DILITHIUM) || defined(HAVE_FALCON) || \ - defined(WOLFSSL_HAVE_SLHDSA) + defined(WOLFSSL_HAVE_SLHDSA) || defined(WOLFSSL_HAVE_LMS) || \ + defined(WOLFSSL_HAVE_XMSS) int verify; #endif union { @@ -1602,6 +1609,20 @@ struct SignatureCtx { SlhDsaKey* slhdsa; #endif #endif + #ifdef WOLFSSL_HAVE_LMS + #ifdef WOLFSSL_NO_MALLOC + LmsKey lms[1]; + #else + LmsKey* lms; + #endif + #endif + #ifdef WOLFSSL_HAVE_XMSS + #ifdef WOLFSSL_NO_MALLOC + XmssKey xmss[1]; + #else + XmssKey* xmss; + #endif + #endif #ifndef WOLFSSL_NO_MALLOC void* ptr; #endif @@ -1864,13 +1885,15 @@ struct DecodedCert { #if defined(HAVE_ECC) || defined(HAVE_ED25519) || defined(HAVE_ED448) || \ defined(HAVE_DILITHIUM) || defined(HAVE_FALCON) || \ - defined(WOLFSSL_HAVE_SLHDSA) + defined(WOLFSSL_HAVE_SLHDSA) || defined(WOLFSSL_HAVE_LMS) || \ + defined(WOLFSSL_HAVE_XMSS) word32 pkCurveOID; /* Public Key's curve OID */ #ifdef WOLFSSL_CUSTOM_CURVES int pkCurveSize; /* Public Key's curve size */ #endif #endif /* HAVE_ECC || HAVE_ED25519 || HAVE_ED448 || HAVE_DILITHIUM || - * HAVE_FALCON || WOLFSSL_HAVE_SLHDSA */ + * HAVE_FALCON || WOLFSSL_HAVE_SLHDSA || WOLFSSL_HAVE_LMS || + * WOLFSSL_HAVE_XMSS */ const byte* beforeDate; int beforeDateLen; const byte* afterDate; diff --git a/wolfssl/wolfcrypt/oid_sum.h b/wolfssl/wolfcrypt/oid_sum.h index 8df91a8546f..e2568c1641f 100644 --- a/wolfssl/wolfcrypt/oid_sum.h +++ b/wolfssl/wolfcrypt/oid_sum.h @@ -219,7 +219,13 @@ enum Key_Sum { /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1e */ SLH_DSA_SHAKE_256Sk = 444, /* 2.16.840.1.101.3.4.3.30 */ /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1f */ - SLH_DSA_SHAKE_256Fk = 445 /* 2.16.840.1.101.3.4.3.31 */ + SLH_DSA_SHAKE_256Fk = 445, /* 2.16.840.1.101.3.4.3.31 */ + /* 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x09,0x10,0x03,0x11 */ + HSS_LMSk = 688, /* 1.2.840.113549.1.9.16.3.17 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x22 */ + XMSSk = 107, /* 1.3.6.1.5.5.7.6.34 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x23 */ + XMSSMTk = 108 /* 1.3.6.1.5.5.7.6.35 */ #else /* 0x00 */ ANONk = 0x7fffffff, /* 0.0 */ @@ -284,7 +290,13 @@ enum Key_Sum { /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1e */ SLH_DSA_SHAKE_256Sk = 0x7db37ae4, /* 2.16.840.1.101.3.4.3.30 */ /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1f */ - SLH_DSA_SHAKE_256Fk = 0x7db37ae5 /* 2.16.840.1.101.3.4.3.31 */ + SLH_DSA_SHAKE_256Fk = 0x7db37ae5, /* 2.16.840.1.101.3.4.3.31 */ + /* 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x09,0x10,0x03,0x11 */ + HSS_LMSk = 0x70a78832, /* 1.2.840.113549.1.9.16.3.17 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x22 */ + XMSSk = 0x2707012e, /* 1.3.6.1.5.5.7.6.34 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x23 */ + XMSSMTk = 0x2607012e /* 1.3.6.1.5.5.7.6.35 */ #endif }; @@ -1627,7 +1639,13 @@ enum Ctc_SigType { /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1e */ CTC_SLH_DSA_SHAKE_256S = 444, /* 2.16.840.1.101.3.4.3.30 */ /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1f */ - CTC_SLH_DSA_SHAKE_256F = 445 /* 2.16.840.1.101.3.4.3.31 */ + CTC_SLH_DSA_SHAKE_256F = 445, /* 2.16.840.1.101.3.4.3.31 */ + /* 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x09,0x10,0x03,0x11 */ + CTC_HSS_LMS = 688, /* 1.2.840.113549.1.9.16.3.17 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x22 */ + CTC_XMSS = 107, /* 1.3.6.1.5.5.7.6.34 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x23 */ + CTC_XMSSMT = 108 /* 1.3.6.1.5.5.7.6.35 */ #else /* 0x2a,0x86,0x48,0xce,0x38,0x04,0x03 */ CTC_SHAwDSA = 0x314b8212, /* 1.2.840.10040.4.3 */ @@ -1720,7 +1738,13 @@ enum Ctc_SigType { /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1e */ CTC_SLH_DSA_SHAKE_256S = 0x7db37ae4, /* 2.16.840.1.101.3.4.3.30 */ /* 0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x03,0x1f */ - CTC_SLH_DSA_SHAKE_256F = 0x7db37ae5 /* 2.16.840.1.101.3.4.3.31 */ + CTC_SLH_DSA_SHAKE_256F = 0x7db37ae5, /* 2.16.840.1.101.3.4.3.31 */ + /* 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x09,0x10,0x03,0x11 */ + CTC_HSS_LMS = 0x70a78832, /* 1.2.840.113549.1.9.16.3.17 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x22 */ + CTC_XMSS = 0x2707012e, /* 1.3.6.1.5.5.7.6.34 */ + /* 0x2b,0x06,0x01,0x05,0x05,0x07,0x06,0x23 */ + CTC_XMSSMT = 0x2607012e /* 1.3.6.1.5.5.7.6.35 */ #endif }; diff --git a/wolfssl/wolfcrypt/types.h b/wolfssl/wolfcrypt/types.h index 01e71a3ebb5..a0819064263 100644 --- a/wolfssl/wolfcrypt/types.h +++ b/wolfssl/wolfcrypt/types.h @@ -1381,6 +1381,7 @@ enum { DYNAMIC_TYPE_SHA = 106, DYNAMIC_TYPE_SLHDSA = 107, DYNAMIC_TYPE_OCSP_RESPONSE = 108, + DYNAMIC_TYPE_XMSS = 109, DYNAMIC_TYPE_SNIFFER_SERVER = 1000, DYNAMIC_TYPE_SNIFFER_SESSION = 1001, DYNAMIC_TYPE_SNIFFER_PB = 1002, diff --git a/wolfssl/wolfcrypt/wc_xmss.h b/wolfssl/wolfcrypt/wc_xmss.h index b7604fadf1d..b2e046da526 100644 --- a/wolfssl/wolfcrypt/wc_xmss.h +++ b/wolfssl/wolfcrypt/wc_xmss.h @@ -427,6 +427,8 @@ WOLFSSL_API int wc_XmssKey_ExportPubRaw(const XmssKey* key, byte* out, word32* outLen); WOLFSSL_API int wc_XmssKey_ImportPubRaw(XmssKey* key, const byte* in, word32 inLen); +WOLFSSL_API int wc_XmssKey_ImportPubRaw_ex(XmssKey* key, const byte* in, + word32 inLen, int is_xmssmt); WOLFSSL_API int wc_XmssKey_Verify(XmssKey* key, const byte* sig, word32 sigSz, const byte* msg, int msgSz);