diff --git a/src/sig_stfl/lms/external/common_defs.h b/src/sig_stfl/lms/external/common_defs.h index c0543f7d5b..3388662d84 100644 --- a/src/sig_stfl/lms/external/common_defs.h +++ b/src/sig_stfl/lms/external/common_defs.h @@ -27,6 +27,18 @@ #define MIN_HSS_LEVELS 1 /* Minumum levels we allow */ #define MAX_HSS_LEVELS 8 /* Maximum levels we allow */ +/* RFC 8554 u32 field width in serialized HSS/LMS structures */ +#define HSS_U32_LEN 4 + +/* + * Incremental HSS signature prefix parsed by hss_validate_signature_init(): + * u32(levels-1) || u32(q) || u32(lm_ots_type) + */ +#define HSS_SIG_LEVELS_M1 0 +#define HSS_SIG_Q (HSS_SIG_LEVELS_M1 + HSS_U32_LEN) +#define HSS_SIG_LM_OTS (HSS_SIG_Q + HSS_U32_LEN) +#define HSS_SIG_INC_HEADER_LEN (HSS_SIG_LM_OTS + HSS_U32_LEN) + /* This is the length of our internal seed values */ #define SEED_LEN 32 /* Enough to make Grover's infeasible */ diff --git a/src/sig_stfl/lms/external/hss_verify_inc.c b/src/sig_stfl/lms/external/hss_verify_inc.c index 366b531394..a3077793f6 100644 --- a/src/sig_stfl/lms/external/hss_verify_inc.c +++ b/src/sig_stfl/lms/external/hss_verify_inc.c @@ -34,7 +34,11 @@ * Further, the signature LM-OTS types must those present in the public key. */ static bool is_hss_public_key(const unsigned char *public_key, - const unsigned char *signature) { + const unsigned char *signature, + size_t signature_len) { + if (signature_len < HSS_SIG_INC_HEADER_LEN) { + return false; + } uint_fast32_t sig_levels = (uint_fast32_t)get_bigendian(signature, 4) + 1; uint_fast32_t sig_lm_ots = (uint_fast32_t)get_bigendian(signature + 8, 4); @@ -61,12 +65,13 @@ static bool is_hss_public_key(const unsigned char *public_key, * Validate by matching the fields in the signature buffer. */ static uint_fast32_t public_key_levels(const unsigned char *public_key, - const unsigned char *signature) { + const unsigned char *signature, + size_t signature_len) { param_set_t w0 = (param_set_t)get_bigendian(public_key, 4); param_set_t w1 = (param_set_t)get_bigendian(public_key + 4, 4); param_set_t w2 = (param_set_t)get_bigendian(public_key + 8, 4); - if (is_hss_public_key(public_key, signature)) { + if (is_hss_public_key(public_key, signature, signature_len)) { uint_fast32_t levels = (uint_fast32_t)w0; if (levels < MIN_HSS_LEVELS || levels > MAX_HSS_LEVELS) { return 0; @@ -109,15 +114,18 @@ bool hss_validate_signature_init( const unsigned char *orig_signature = signature; - /* Get the number of levels the signature claims */ - if (signature_len < 4) { + /* Get the number of levels the signature claims. The header parsed here + * is u32(levels-1) || u32(q) || u32(lm_ots), and is_hss_public_key() + * below reads the LM-OTS type at offset 8, so HSS_SIG_INC_HEADER_LEN + * bytes must be present */ + if (signature_len < HSS_SIG_INC_HEADER_LEN) { ctx->status = info->error_code = hss_error_bad_signature; return false; } uint_fast32_t levels = (uint_fast32_t)get_bigendian( signature, 4 ) + 1; /* +1 because what's in the signature is levels-1 */ - uint_fast32_t pub_levels = public_key_levels(public_key, orig_signature); - if (is_hss_public_key(public_key, orig_signature)) { + uint_fast32_t pub_levels = public_key_levels(public_key, orig_signature, signature_len); + if (is_hss_public_key(public_key, orig_signature, signature_len)) { public_key += 4; } signature += 4; signature_len -= 4; @@ -205,6 +213,12 @@ bool hss_validate_signature_init( ctx->status = info->error_code = hss_error_bad_signature; return false; } + /* The bottom level signature starts with u32(q), and the LM-OTS + * randomizer C sits at offset 8 and is n bytes wide; both are read below */ + if (signature_len < 8 + (size_t)n) { + ctx->status = info->error_code = hss_error_bad_signature; + return false; + } ctx->h = h; hss_init_hash_context( h, &ctx->hash_ctx ); { diff --git a/tests/test_sig_stfl.c b/tests/test_sig_stfl.c index e8707beae5..726b04a394 100644 --- a/tests/test_sig_stfl.c +++ b/tests/test_sig_stfl.c @@ -410,6 +410,23 @@ static char *convert_method_name_to_file_name(const char *method_name) { #define TEST_XMSS_OID_SHA2_10_256 0x01U #endif +#ifdef OQS_ENABLE_SIG_STFL_LMS +/* test_invalid_sig_lms: HSS pk layout (RFC 8554): u32(levels) || u32(lm_type) || u32(lm_ots) || I[16] || T[32]. */ +#define TEST_INVALID_SIG_LMS_PK_LEN 60 +/* Signature header: u32(levels-1) || u32(q) || u32(lm_ots). Real LMS signatures are kilobytes. */ +#define TEST_INVALID_SIG_LMS_HEADER_LEN 12 +/* The bottom level parse needs u32(q) and the n-byte LM-OTS randomizer C, i.e. 8 + n bytes + * beyond the four the header consumes: 44 for the SHA-256/n=32 parameter sets liboqs supports. + * This is the shortest signature that clears every length bound in the parser. */ +#define TEST_INVALID_SIG_LMS_MIN_BOUNDED_LEN 44 +/* Offsets of the type fields within the pk and the signature header. */ +#define TEST_LMS_LEVELS_OFFSET 3 +#define TEST_LMS_TYPE_OFFSET 7 +#define TEST_LMS_OTS_TYPE_OFFSET 11 +#define TEST_LMS_TYPE_SHA256_H5 5U +#define TEST_LMOTS_TYPE_SHA256_N32_W2 2U +#endif + /* * This function is used to test the invalid signature verification. * @param method_name: The name of the signature algorithm to test. @@ -465,6 +482,73 @@ static OQS_STATUS test_invalid_sig(const char *method_name) { #endif } +/* + * This function tests verification of LMS signatures that are shorter than the + * header the parser reads. The signature buffers are allocated at exactly the + * length passed to verify so that a sanitizer build observes any over-read. + * @param method_name: The name of the signature algorithm to test. + * @return OQS_SUCCESS if every truncated signature is rejected, OQS_ERROR otherwise. + */ +static OQS_STATUS test_invalid_sig_lms(const char *method_name) { + if (method_name == NULL) { + return OQS_ERROR; + } +#ifndef OQS_ENABLE_SIG_STFL_LMS + (void)method_name; + return OQS_SUCCESS; +#else + OQS_SIG_STFL *sig = OQS_SIG_STFL_new(method_name); + if (sig == NULL) { + return OQS_ERROR; + } + + /* hss_validate_signature_init derives every parameter from the pk and + * signature bytes, so one well-formed single-level pk covers all variants. */ + uint8_t pk[TEST_INVALID_SIG_LMS_PK_LEN] = {0}; + pk[TEST_LMS_LEVELS_OFFSET] = 1; + pk[TEST_LMS_TYPE_OFFSET] = TEST_LMS_TYPE_SHA256_H5; + pk[TEST_LMS_OTS_TYPE_OFFSET] = TEST_LMOTS_TYPE_SHA256_N32_W2; + + uint8_t message[] = "test"; + /* Every length here must be rejected without reading past the buffer: + * 4, 11 shorter than the 12-byte header, which is_hss_public_key() + * reads to offset 8 for the LM-OTS type; + * 12, 39, 40, 43 header present, but fewer than 8 + n bytes remain for q and + * the LM-OTS randomizer C that the bottom level copies -- 43 + * is the largest such length; + * 44 clears both of those bounds, so it reaches + * lm_validate_signature() and must be rejected on length + * there, still without an over-read. */ + const size_t trunc_lens[] = { + 4, 11, + TEST_INVALID_SIG_LMS_HEADER_LEN, 39, 40, 43, + TEST_INVALID_SIG_LMS_MIN_BOUNDED_LEN + }; + + for (size_t i = 0; i < sizeof(trunc_lens) / sizeof(trunc_lens[0]); i++) { + uint8_t *malicious_sig = OQS_MEM_malloc(trunc_lens[i]); + if (malicious_sig == NULL) { + OQS_SIG_STFL_free(sig); + return OQS_ERROR; + } + memset(malicious_sig, 0, trunc_lens[i]); + if (trunc_lens[i] > TEST_LMS_OTS_TYPE_OFFSET) { + malicious_sig[TEST_LMS_OTS_TYPE_OFFSET] = TEST_LMOTS_TYPE_SHA256_N32_W2; + } + + OQS_STATUS status = OQS_SIG_STFL_verify(sig, message, sizeof(message) - 1, malicious_sig, trunc_lens[i], pk); + OQS_MEM_insecure_free(malicious_sig); + if (status == OQS_SUCCESS) { + OQS_SIG_STFL_free(sig); + return OQS_ERROR; + } + } + + OQS_SIG_STFL_free(sig); + return OQS_SUCCESS; +#endif +} + static OQS_STATUS sig_stfl_test_correctness(const char *method_name, const char *katfile, bool bitflips_all[2], size_t bitflips[2]) { OQS_SIG_STFL *sig = NULL; @@ -1097,6 +1181,8 @@ void *test_correctness_wrapper(void *arg) { td->rc = sig_stfl_test_correctness(td->alg_name, td->katfile, td->bitflips_all, td->bitflips); if (strstr(td->alg_name, "XMSS") != NULL) { td->rc2 = test_invalid_sig(td->alg_name); + } else if (strstr(td->alg_name, "LMS") != NULL) { + td->rc2 = test_invalid_sig_lms(td->alg_name); } OQS_thread_stop(); return NULL; @@ -1369,6 +1455,8 @@ int main(int argc, char **argv) { rc1 = sig_stfl_test_secret_key(alg_name, katfile); if (is_xmss) { rc2 = test_invalid_sig(alg_name); + } else if (strstr(alg_name, "LMS") != NULL) { + rc2 = test_invalid_sig_lms(alg_name); } OQS_destroy();