From 476ce9a5cf0b781ee3be76be052d3a581cdad964 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 30 Sep 2026 14:08:29 -0600 Subject: [PATCH] Add wc_MlDsaKey_MakePublicKey --- doc/dox_comments/header_files/wc_mldsa.h | 37 +++ wolfcrypt/src/wc_mldsa.c | 366 ++++++++++++++++++++++- wolfssl/wolfcrypt/wc_mldsa.h | 10 + 3 files changed, 412 insertions(+), 1 deletion(-) diff --git a/doc/dox_comments/header_files/wc_mldsa.h b/doc/dox_comments/header_files/wc_mldsa.h index 32d4956f1b..f55e32ec39 100644 --- a/doc/dox_comments/header_files/wc_mldsa.h +++ b/doc/dox_comments/header_files/wc_mldsa.h @@ -267,6 +267,43 @@ int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng); */ int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed); +/*! + \ingroup ML_DSA + + \brief Derives public key for a wc_MlDsaKey with private key set. + If the public key is already set, it is only checked against the + private key's tr (H(pk)); use wc_MlDsaKey_CheckKey() for a full key pair + check. The private key is still required: a public-only key returns + BAD_FUNC_ARG. + Available when WC_MLDSA_HAVE_MAKE_PUBLIC_KEY is defined: that is, when + WOLFSSL_MLDSA_ASSIGN_KEY, WOLFSSL_MLDSA_NO_MAKE_KEY and + WOLFSSL_MLDSA_VERIFY_ONLY are not defined. + + Derivation is always done in software from the local private key bytes, + including for a key initialized with a devId: a key whose private key was + imported with wc_MlDsaKey_ImportPrivRaw() (or generated in software) + derives normally. A key that only references a private key held on a + device (wc_MlDsaKey_InitId() / wc_MlDsaKey_InitLabel()) has no local + private bytes, so it returns BAD_FUNC_ARG. + On PUBLIC_KEY_E an already set public key is left in place; set the + matching public key, or re-initialize the key, before deriving. + + \return 0 on success or already set. + \return BAD_FUNC_ARG if invalid args or the private key is not set. + \return MEMORY_E on allocation failure. + \return PUBLIC_KEY_E if the public key does not match the private key's + tr or, when derived, its t0. + \return NO_VALID_DEVID with WOLF_CRYPTO_CB_ONLY_MLDSA. + \return Other negative on error. + + \param [in,out] key Pointer to wc_MlDsaKey. + + \sa wc_MlDsaKey_ImportPrivRaw + \sa wc_MlDsaKey_MakeKey + \sa wc_MlDsaKey_CheckKey +*/ +int wc_MlDsaKey_MakePublicKey(wc_MlDsaKey* key); + /*! \ingroup ML_DSA diff --git a/wolfcrypt/src/wc_mldsa.c b/wolfcrypt/src/wc_mldsa.c index c1b49a5fa1..1d8deccf4b 100644 --- a/wolfcrypt/src/wc_mldsa.c +++ b/wolfcrypt/src/wc_mldsa.c @@ -1338,6 +1338,7 @@ static int mldsa_vec_encode_eta_bits(const sword32* s, byte d, byte eta, #endif /* !WOLFSSL_MLDSA_NO_MAKE_KEY */ #if !defined(WOLFSSL_MLDSA_NO_SIGN) || defined(WOLFSSL_MLDSA_CHECK_KEY) || \ + defined(WC_MLDSA_HAVE_MAKE_PUBLIC_KEY) || \ (!defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM)) @@ -1485,6 +1486,7 @@ static int mldsa_decode_eta_4_bits(const byte* p, sword32* s) #endif #if defined(WOLFSSL_MLDSA_CHECK_KEY) || \ + defined(WC_MLDSA_HAVE_MAKE_PUBLIC_KEY) || \ (!defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM)) || \ (!defined(WOLFSSL_MLDSA_NO_SIGN) && \ @@ -1579,7 +1581,8 @@ static int mldsa_vec_decode_eta_bits(const byte* p, byte eta, sword32* s, return ret; } #endif -#endif /* !WOLFSSL_MLDSA_NO_SIGN || WOLFSSL_MLDSA_CHECK_KEY */ +#endif /* !WOLFSSL_MLDSA_NO_SIGN || WOLFSSL_MLDSA_CHECK_KEY || + * WC_MLDSA_HAVE_MAKE_PUBLIC_KEY */ #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY /* Encode t into t0 and t1. @@ -13022,6 +13025,367 @@ int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed) { return mldsa_key_from_seed_dev(key, seed, 1); } + +#ifdef WC_MLDSA_HAVE_MAKE_PUBLIC_KEY + +#ifndef WOLF_CRYPTO_CB_ONLY_MLDSA +/* Encoded size of one polynomial of t0 and of t1. */ +#define MLDSA_T0_POLY_ENC_SZ (MLDSA_D * MLDSA_N / 8) +#define MLDSA_T1_POLY_ENC_SZ (MLDSA_U * MLDSA_N / 8) +/* Buffer headroom required for AVX2 encoder. Do not reduce to 0. */ +#define MLDSA_POLY_ENC_SLACK 8 +#ifndef WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM +/* Polynomials encoded per call. Two keeps the paired AVX-512 encoder, which + * needs an even count, in use. */ +#define MLDSA_T_ENC_POLYS 2 +#else +/* t is calculated a polynomial at a time. */ +#define MLDSA_T_ENC_POLYS 1 +#endif +/* Encode scratch size for MLDSA_T_ENC_POLYS polynomials of t0. */ +#define MLDSA_T0_ENC_SCRATCH_SZ \ + ((MLDSA_T_ENC_POLYS * MLDSA_T0_POLY_ENC_SZ) + MLDSA_POLY_ENC_SLACK) + +/* Encode t into t1 and check its t0 against the private key's t0. + * + * t1 is written straight into the public key, which has the encoder's + * headroom as in key generation. t0 is encoded into scratch, + * MLDSA_T_ENC_POLYS polynomials at a time. + * + * @param [in] t Vector t (k polynomials), made positive. + * @param [in] k Number of polynomials in t. + * @param [in] t0p Encoded t0 from the private key to compare against. + * @param [out] t1 Buffer to write encoded t1 into. + * @param [in, out] t0Enc Encode scratch of MLDSA_T0_ENC_SCRATCH_SZ bytes. + * @param [in, out] diff Non-zero bits are accumulated on a t0 mismatch. + * @return 0 on success. + * @return Negative when encoding fails. + */ +static int mldsa_encode_t_check(const sword32* t, byte k, const byte* t0p, + byte* t1, byte* t0Enc, int* diff) +{ + int ret = 0; + unsigned int i; + unsigned int n; + + for (i = 0; i < (unsigned int)k; i += n) { + n = (unsigned int)k - i; + if (n > MLDSA_T_ENC_POLYS) { + n = MLDSA_T_ENC_POLYS; + } + ret = mldsa_vec_encode_t0_t1(t + ((size_t)i * MLDSA_N), (byte)n, t0Enc, + t1 + ((size_t)i * MLDSA_T1_POLY_ENC_SZ)); + if (ret != 0) { + break; + } + + /* Accumulate rather than break early so the work stays independent + * of where a mismatch falls. */ + *diff |= ConstantCompare(t0Enc, + t0p + ((size_t)i * MLDSA_T0_POLY_ENC_SZ), + (int)(n * MLDSA_T0_POLY_ENC_SZ)); + } + + return ret; +} + +/* Check the public key against the private key's tr, which is H(pk). + * + * @param [in, out] key ML-DSA key with both halves set. + * @return 0 when the public key belongs to the private key. + * @return PUBLIC_KEY_E when it does not. + * @return Negative on hash error. + */ +static int mldsa_check_pub_tr(wc_MlDsaKey* key) +{ + int ret; + byte trCalc[MLDSA_TR_SZ]; + const byte* tr = key->k + MLDSA_PUB_SEED_SZ + MLDSA_K_SZ; + + ret = mldsa_shake256(&key->shake, key->p, key->params->pkSz, trCalc, + MLDSA_TR_SZ); + if ((ret == 0) && (ConstantCompare(trCalc, tr, MLDSA_TR_SZ) != 0)) { + ret = PUBLIC_KEY_E; + } + + return ret; +} + +/* Derive the public key (rho || t1) from the private key. + * + * @param [in, out] key ML-DSA key with the private key set and no public key. + * @return 0 on success. + * @return PUBLIC_KEY_E when the derived key does not match the private key. + * @return MEMORY_E when dynamic memory allocation fails. + * @return Other negative when an error occurs. + */ +static int mldsa_make_pub_key(wc_MlDsaKey* key) +{ + int ret = 0; + const wc_MlDsaParams* params = key->params; + sword32* s1 = NULL; + sword32* t = NULL; + sword32* a = NULL; +#ifndef WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM + sword32* s2 = NULL; +#else + sword32* s2t = NULL; + sword64* t64 = NULL; + byte* h = NULL; +#endif + WC_DECLARE_VAR(t0Enc, byte, MLDSA_T0_ENC_SCRATCH_SZ, NULL); + unsigned int allocSz = 0; + int diff = 0; + +#ifdef WOLFSSL_MLDSA_DYNAMIC_KEYS + ret = mldsa_alloc_pub_buf(key); +#endif + +#ifndef WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM +#ifdef WC_MLDSA_CACHE_MATRIX_A +#ifndef WC_MLDSA_FIXED_ARRAY + /* Importing or generating the private key allocates the matrix cache, + * so this only guards against a key without one. */ + if ((ret == 0) && (key->a == NULL)) { + key->a = (sword32*)XMALLOC(params->aSz, key->heap, + DYNAMIC_TYPE_MLDSA); + if (key->a == NULL) { + ret = MEMORY_E; + } + else { + key->aSet = 0; + } + } +#endif + if (ret == 0) { + a = key->a; + } +#endif + + /* s1, s2 and t are always local: the private-vector cache holds the + * NTT-domain vectors the next sign reuses, so it is left intact. */ + if (ret == 0) { + /* s1-l, s2-k, t-k. Note: t has same size as s2 */ + allocSz = (unsigned int)params->s1Sz + params->s2Sz + + params->s2Sz; +#ifndef WC_MLDSA_CACHE_MATRIX_A + /* A-KxL */ + allocSz += params->aSz; +#endif + + s1 = (sword32*)XMALLOC(allocSz, key->heap, DYNAMIC_TYPE_MLDSA); + if (s1 == NULL) { + ret = MEMORY_E; + } + else { + s2 = s1 + params->s1Sz / sizeof(*s1); + t = s2 + params->s2Sz / sizeof(*s2); +#ifndef WC_MLDSA_CACHE_MATRIX_A + a = t + params->s2Sz / sizeof(*t); +#endif + } + } +#else + /* Matrix A is expanded a polynomial at a time, so any cache is left + * alone. */ + if (ret == 0) { + /* s1-l, t-1, s2-1, a-1, [t64], h. h is last as its size may be + * odd. */ + allocSz = (unsigned int)params->s1Sz + + 3U * (unsigned int)MLDSA_POLY_SIZE + + (unsigned int)MLDSA_REJ_NTT_POLY_H_SIZE; + #ifdef WOLFSSL_MLDSA_SMALL_MEM_POLY64 + /* t64 */ + allocSz += 2U * (unsigned int)MLDSA_POLY_SIZE; + #endif + + s1 = (sword32*)XMALLOC(allocSz, key->heap, DYNAMIC_TYPE_MLDSA); + if (s1 == NULL) { + ret = MEMORY_E; + } + else { + t = s1 + params->s1Sz / sizeof(*s1); + s2t = t + MLDSA_N; + a = s2t + MLDSA_N; + #ifdef WOLFSSL_MLDSA_SMALL_MEM_POLY64 + t64 = (sword64*)(a + MLDSA_N); + h = (byte*)(t64 + MLDSA_N); + #else + h = (byte*)(a + MLDSA_N); + #endif + } + } +#endif + + if (ret == 0) { + /* MLDSA_T_ENC_POLYS polynomials of encode scratch, so up to about + * 850 bytes on the stack unless WOLFSSL_SMALL_STACK asks for it to + * be allocated. */ + WC_ALLOC_VAR_EX(t0Enc, byte, MLDSA_T0_ENC_SCRATCH_SZ, key->heap, + DYNAMIC_TYPE_MLDSA, ret = MEMORY_E); + } + + if (ret == 0) { + const byte* rho = key->k; + const byte* s1p = key->k + MLDSA_PUB_SEED_SZ + MLDSA_K_SZ + + MLDSA_TR_SZ; + const byte* s2p = s1p + params->s1EncSz; + const byte* t0p = s2p + params->s2EncSz; + byte* t1 = key->p + MLDSA_PUB_SEED_SZ; + + XMEMCPY(key->p, rho, MLDSA_PUB_SEED_SZ); +#ifdef WC_MLDSA_CACHE_PUB_VECTORS + /* Clear cached NTT(t1) since key->p is being rewritten, + * preventing reused keys from verifying against a stale t1. */ + key->pubVecSet = 0; +#endif + + ret = mldsa_vec_decode_eta_bits(s1p, params->eta, s1, params->l); +#ifndef WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM + if (ret == 0) { + ret = mldsa_vec_decode_eta_bits(s2p, params->eta, s2, + params->k); + } + + /* Expand matrix A, unless already cached. */ + #ifdef WC_MLDSA_CACHE_MATRIX_A + if ((ret == 0) && (!key->aSet)) + #else + if (ret == 0) + #endif + { + ret = mldsa_expand_a(&key->shake, rho, params->k, params->l, + a, key->heap); + #ifdef WC_MLDSA_CACHE_MATRIX_A + key->aSet = (ret == 0); + #endif + } + + /* Calculate t = NTT-1(A o NTT(s1)) + s2 */ + if (ret == 0) { + ret = mldsa_calc_t(t, a, s1, s2, params->k, params->l); + } + /* Make t positive for decomposing. */ + if (ret == 0) { + ret = mldsa_vec_make_pos(t, params->k); + } + /* Encode t1 into the public key and check the derived t0 against + * the private key's authentic t0. (Since t0/t1 split t, + * derivation faults are caught here). */ + if (ret == 0) { + ret = mldsa_encode_t_check(t, params->k, t0p, t1, t0Enc, &diff); + } +#else + if (ret == 0) { + ret = mldsa_vec_ntt_small_full(s1, params->l); + } + if (ret == 0) { + byte aseed[MLDSA_GEN_A_SEED_SZ]; + word32 s2Stride = (word32)params->s2EncSz / params->k; + unsigned int r; + + XMEMCPY(aseed, rho, MLDSA_PUB_SEED_SZ); + for (r = 0; (ret == 0) && (r < params->k); r++) { + /* Calculate row r of t = NTT-1(A o NTT(s1)) + s2 */ + ret = mldsa_calc_t_row(&key->shake, aseed, (byte)r, s1, + params->l, s2p + (r * s2Stride), params->eta, t, s2t, a, + h, t64); + /* Encode row r of t1 into the public key and check row r + * of the derived t0 against the private key's t0. */ + if (ret == 0) { + ret = mldsa_encode_t_check(t, 1, + t0p + (r * MLDSA_T0_POLY_ENC_SZ), + t1 + (r * MLDSA_T1_POLY_ENC_SZ), t0Enc, &diff); + } + } + } +#endif + if ((ret == 0) && (diff != 0)) { + ret = PUBLIC_KEY_E; + } + + /* Once t0 matches, also verify the derived public key against the + * private key's authentic 'tr' hash. Combined with the t0 check, + * every bit of derived t is verified. */ + if (ret == 0) { + ret = mldsa_check_pub_tr(key); + } + if (ret == 0) { + key->pubKeySet = 1; + } + else { + /* Don't leave a partial or mismatched public key behind. */ + XMEMSET(key->p, 0, params->pkSz); + } + } + + /* --- Cleanup --------------------------------------------------- */ + if (s1 != NULL) { +#ifndef WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM + /* s1, s2 and t at the front are secret; any A after is public. */ + ForceZero(s1, (unsigned int)params->s1Sz + (2U * params->s2Sz)); +#else + /* The public a and h sit between and after the secret s1, t, s2 and + * t64, so zeroize the entire allocation. */ + ForceZero(s1, allocSz); +#endif + XFREE(s1, key->heap, DYNAMIC_TYPE_MLDSA); + } + /* t0 is private key material, so scrub the encode buffer. */ + if (WC_VAR_OK(t0Enc)) { + ForceZero(t0Enc, MLDSA_T0_ENC_SCRATCH_SZ); + } + WC_FREE_VAR_EX(t0Enc, key->heap, DYNAMIC_TYPE_MLDSA); + + return ret; +} +#endif /* !WOLF_CRYPTO_CB_ONLY_MLDSA */ + +/* Derive the public key (rho || t1) from the private key. + * + * When the public key is already set it is only checked against the private + * key's tr. + * + * @param [in, out] key ML-DSA key with the private key set. + * @return 0 on success, or when the set public key matches tr. + * @return BAD_FUNC_ARG when key is NULL, or the private key or parameters + * are not set. + * @return PUBLIC_KEY_E when the public key does not match the private key. + * @return NO_VALID_DEVID when the software implementation is not compiled + * in. + * @return MEMORY_E when dynamic memory allocation fails. + * @return Other negative when an error occurs. + */ +int wc_MlDsaKey_MakePublicKey(wc_MlDsaKey* key) +{ + int ret = 0; + + /* Derivation needs the local private key bytes: a devId-bound key with + * imported private material derives in software, while a key that only + * references a device-held private key (id/label) has prvKeySet == 0. */ + if ((key == NULL) || (!key->prvKeySet) || (key->params == NULL)) { + ret = BAD_FUNC_ARG; + } + +#ifdef WOLF_CRYPTO_CB_ONLY_MLDSA + /* No software check or derivation, and no device operation for it. */ + if (ret == 0) { + ret = NO_VALID_DEVID; + } +#else + /* A private-only import leaves any earlier public key set, so check + * that it belongs to this private key before reporting success. */ + if ((ret == 0) && key->pubKeySet) { + ret = mldsa_check_pub_tr(key); + } + else if (ret == 0) { + ret = mldsa_make_pub_key(key); + } +#endif /* WOLF_CRYPTO_CB_ONLY_MLDSA */ + + return ret; +} +#endif /* WC_MLDSA_HAVE_MAKE_PUBLIC_KEY */ #endif #ifndef WOLFSSL_MLDSA_NO_SIGN diff --git a/wolfssl/wolfcrypt/wc_mldsa.h b/wolfssl/wolfcrypt/wc_mldsa.h index 40459970a0..be6d3c8e15 100644 --- a/wolfssl/wolfcrypt/wc_mldsa.h +++ b/wolfssl/wolfcrypt/wc_mldsa.h @@ -111,6 +111,12 @@ #define WC_MLDSA_CACHE_MATRIX_A #endif +/* wc_MlDsaKey_MakePublicKey() is available. */ +#if !defined(WOLFSSL_MLDSA_ASSIGN_KEY) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) + #define WC_MLDSA_HAVE_MAKE_PUBLIC_KEY +#endif + #ifdef __cplusplus extern "C" { #endif @@ -734,6 +740,10 @@ WOLFSSL_API int wc_MlDsaKey_MakeKey(wc_MlDsaKey* key, WC_RNG* rng); WOLFSSL_API int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed); +#ifdef WC_MLDSA_HAVE_MAKE_PUBLIC_KEY +WOLFSSL_API +int wc_MlDsaKey_MakePublicKey(wc_MlDsaKey* key); +#endif /* Legacy sign API without context parameter (pre-FIPS 204). * Only available when WOLFSSL_MLDSA_NO_CTX is defined.