From aad005da85c72c9b4bafd403ab6c0ea7f4c5b030 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:08:36 -0700 Subject: [PATCH 01/16] Add PQC scaffolding: ML-KEM/ML-DSA macros, names, externs, build flag --- include/wolfprovider/alg_funcs.h | 18 ++++++++++++++++++ include/wolfprovider/settings.h | 12 ++++++++++++ scripts/build-wolfprovider.sh | 6 ++++++ scripts/utils-wolfssl.sh | 5 +++++ 4 files changed, 41 insertions(+) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 6e9bd1af..05ea27c8 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -170,6 +170,16 @@ typedef void (*DFUNC)(void); #define WP_NAMES_DH "DH" #define WP_NAMES_DHX "DHX" +/* ML-KEM names (NIST FIPS 203). */ +#define WP_NAMES_ML_KEM_512 "ML-KEM-512" +#define WP_NAMES_ML_KEM_768 "ML-KEM-768" +#define WP_NAMES_ML_KEM_1024 "ML-KEM-1024" + +/* ML-DSA names (NIST FIPS 204). */ +#define WP_NAMES_ML_DSA_44 "ML-DSA-44" +#define WP_NAMES_ML_DSA_65 "ML-DSA-65" +#define WP_NAMES_ML_DSA_87 "ML-DSA-87" + /* DRBG names. */ #define WP_NAMES_SEED_SRC "SEED-SRC" #define WP_NAMES_CTR_DRBG "CTR-DRBG" @@ -325,12 +335,14 @@ extern const OSSL_DISPATCH wp_ed25519_signature_functions[]; extern const OSSL_DISPATCH wp_ed448_signature_functions[]; extern const OSSL_DISPATCH wp_hmac_signature_functions[]; extern const OSSL_DISPATCH wp_cmac_signature_functions[]; +extern const OSSL_DISPATCH wp_mldsa_signature_functions[]; /* Asymmetric cipher implementations. */ extern const OSSL_DISPATCH wp_rsa_asym_cipher_functions[]; /* KEM implementations. */ extern const OSSL_DISPATCH wp_rsa_asym_kem_functions[]; +extern const OSSL_DISPATCH wp_mlkem_asym_kem_functions[]; /* Key Management implementations. */ extern const OSSL_DISPATCH wp_rsa_keymgmt_functions[]; @@ -344,6 +356,12 @@ extern const OSSL_DISPATCH wp_dh_keymgmt_functions[]; extern const OSSL_DISPATCH wp_hmac_keymgmt_functions[]; extern const OSSL_DISPATCH wp_cmac_keymgmt_functions[]; extern const OSSL_DISPATCH wp_kdf_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mlkem512_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mlkem768_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mlkem1024_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_keymgmt_functions[]; /* Key exchange implementations. */ extern const OSSL_DISPATCH wp_ecdh_keyexch_functions[]; diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index 151bc707..895fef1c 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -169,6 +169,18 @@ #ifdef HAVE_ED448 #define WP_HAVE_ED448 #endif +#ifdef WOLFSSL_HAVE_MLKEM + #define WP_HAVE_MLKEM + #define WP_HAVE_ML_KEM_512 + #define WP_HAVE_ML_KEM_768 + #define WP_HAVE_ML_KEM_1024 +#endif +#ifdef HAVE_DILITHIUM + #define WP_HAVE_MLDSA + #define WP_HAVE_ML_DSA_44 + #define WP_HAVE_ML_DSA_65 + #define WP_HAVE_ML_DSA_87 +#endif #if !defined(NO_AES_CBC) && (defined(WP_HAVE_HMAC) || defined(WP_HAVE_CMAC)) #define WP_HAVE_KBKDF #endif diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index 8f733b40..b4448c9f 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -32,6 +32,8 @@ show_help() { echo " --debug-silent Debug logging compiled in but silent by default. Use WOLFPROV_LOG_LEVEL and WOLFPROV_LOG_COMPONENTS env vars to enable at runtime. Requires --debug." echo " --enable-seed-src Enable SEED-SRC entropy source with /dev/urandom caching for fork-safe entropy." echo " Note: This also enables WC_RNG_SEED_CB in wolfSSL." + echo " --enable-pqc Build wolfSSL with ML-KEM and ML-DSA post-quantum algorithms enabled." + echo " Adds --enable-mlkem --enable-dilithium --enable-experimental to wolfSSL configure." echo "" echo "Environment Variables:" echo " OPENSSL_TAG OpenSSL tag to use (e.g., openssl-3.5.0)" @@ -51,6 +53,7 @@ show_help() { echo " WOLFPROV_FIPS_BASELINE If set to 1, applies FIPS baseline patch to OpenSSL (mutually exclusive with WOLFPROV_REPLACE_DEFAULT)" echo " WOLFPROV_LEAVE_SILENT If set to 1, suppress logging of return 0 in functions where return 0 is expected behavior sometimes." echo " WOLFPROV_SEED_SRC If set to 1, enables SEED-SRC with /dev/urandom caching (also enables WC_RNG_SEED_CB in wolfSSL)" + echo " WOLFPROV_PQC If set to 1, enables ML-KEM and ML-DSA post-quantum algorithms in wolfSSL" echo "" } @@ -146,6 +149,9 @@ for arg in "$@"; do --enable-seed-src) WOLFPROV_SEED_SRC=1 ;; + --enable-pqc) + WOLFPROV_PQC=1 + ;; *) args_wrong+="$arg, " ;; diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 9a79bfca..16c7c813 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -38,6 +38,11 @@ if [ "$WOLFPROV_SEED_SRC" = "1" ]; then WOLFSSL_FIPS_CONFIG_CFLAGS="${WOLFSSL_FIPS_CONFIG_CFLAGS} -DWC_RNG_SEED_CB" fi +# Enable ML-KEM and ML-DSA in wolfSSL when --enable-pqc is requested +if [ "$WOLFPROV_PQC" = "1" ]; then + WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mlkem --enable-dilithium --enable-experimental" +fi + WOLFSSL_DEBUG_ASN_TEMPLATE=${DWOLFSSL_DEBUG_ASN_TEMPLATE:-0} WOLFPROV_DISABLE_ERR_TRACE=${WOLFPROV_DISABLE_ERR_TRACE:-0} WOLFPROV_DEBUG=${WOLFPROV_DEBUG:-0} From d20397979a209a6d07be6f7fa30c24a68226fcac Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:19:17 -0700 Subject: [PATCH 02/16] Add ML-KEM keymgmt and KEM dispatch for 512/768/1024 --- include/wolfprovider/alg_funcs.h | 20 + src/include.am | 2 + src/wp_mlkem_kem.c | 338 +++++++++++ src/wp_mlkem_kmgmt.c | 978 +++++++++++++++++++++++++++++++ src/wp_wolfprov.c | 17 + 5 files changed, 1355 insertions(+) create mode 100644 src/wp_mlkem_kem.c create mode 100644 src/wp_mlkem_kmgmt.c diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 05ea27c8..74c7fcfd 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -232,6 +232,26 @@ void wp_ecx_free(wp_Ecx* ecx); void* wp_ecx_get_key(wp_Ecx* ecx); wolfSSL_Mutex* wp_ecx_get_mutex(wp_Ecx* ecx); +/* Internal ML-KEM types and functions. */ +typedef struct wp_MlKem wp_MlKem; +typedef struct wp_MlKemData wp_MlKemData; + +int wp_mlkem_up_ref(wp_MlKem* mlkem); +void wp_mlkem_free(wp_MlKem* mlkem); +void* wp_mlkem_get_key(wp_MlKem* mlkem); +const wp_MlKemData* wp_mlkem_get_data(const wp_MlKem* mlkem); +word32 wp_mlkem_data_ct_size(const wp_MlKemData* data); +word32 wp_mlkem_data_ss_size(const wp_MlKemData* data); + +/* Internal ML-DSA types and functions. */ +typedef struct wp_MlDsa wp_MlDsa; + +int wp_mldsa_up_ref(wp_MlDsa* mldsa); +void wp_mldsa_free(wp_MlDsa* mldsa); +void* wp_mldsa_get_key(wp_MlDsa* mldsa); +int wp_mldsa_get_level(const wp_MlDsa* mldsa); +int wp_mldsa_get_sig_size(const wp_MlDsa* mldsa); + /* Internal DH types and functions. */ typedef struct wp_Dh wp_Dh; diff --git a/src/include.am b/src/include.am index 5d8db01b..8ae7d630 100644 --- a/src/include.am +++ b/src/include.am @@ -36,6 +36,8 @@ libwolfprov_la_SOURCES += src/wp_ecx_exch.c libwolfprov_la_SOURCES += src/wp_ecx_sig.c libwolfprov_la_SOURCES += src/wp_dh_kmgmt.c libwolfprov_la_SOURCES += src/wp_dh_exch.c +libwolfprov_la_SOURCES += src/wp_mlkem_kmgmt.c +libwolfprov_la_SOURCES += src/wp_mlkem_kem.c libwolfprov_la_SOURCES += src/wp_drbg.c libwolfprov_la_SOURCES += src/wp_seed_src.c libwolfprov_la_SOURCES += src/wp_dec_pem2der.c diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c new file mode 100644 index 00000000..71c98f80 --- /dev/null +++ b/src/wp_mlkem_kem.c @@ -0,0 +1,338 @@ +/* wp_mlkem_kem.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WP_HAVE_MLKEM + +#include + +/** + * ML-KEM KEM context. + */ +typedef struct wp_MlKemCtx { + /** Provider context. */ + WOLFPROV_CTX* provCtx; + /** wolfProvider ML-KEM key (owned reference). */ + wp_MlKem* mlkem; + /** RNG for encapsulate. */ + WC_RNG rng; +} wp_MlKemCtx; + + +/** + * Create a new ML-KEM KEM context object. + * + * @param [in] provCtx Provider context. + * @return New KEM context on success, NULL on failure. + */ +static wp_MlKemCtx* wp_mlkem_kem_newctx(WOLFPROV_CTX* provCtx) +{ + wp_MlKemCtx* ctx = NULL; + + if (wolfssl_prov_is_running()) { + ctx = (wp_MlKemCtx*)OPENSSL_zalloc(sizeof(*ctx)); + } + if (ctx != NULL) { + int rc = wc_InitRng(&ctx->rng); + if (rc != 0) { + OPENSSL_free(ctx); + ctx = NULL; + } + } + if (ctx != NULL) { + ctx->provCtx = provCtx; + } + return ctx; +} + +/** + * Free an ML-KEM KEM context object. + * + * @param [in, out] ctx KEM context. May be NULL. + */ +static void wp_mlkem_kem_freectx(wp_MlKemCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + wp_mlkem_free(ctx->mlkem); + OPENSSL_free(ctx); + } +} + +/** + * Duplicate an ML-KEM KEM context. + * + * @param [in] srcCtx Source KEM context. + * @return Duplicated context on success, NULL on failure. + */ +static wp_MlKemCtx* wp_mlkem_kem_dupctx(wp_MlKemCtx* srcCtx) +{ + wp_MlKemCtx* dstCtx = NULL; + + if (!wolfssl_prov_is_running()) { + return NULL; + } + + dstCtx = wp_mlkem_kem_newctx(srcCtx->provCtx); + if (dstCtx == NULL) { + return NULL; + } + if (srcCtx->mlkem != NULL) { + if (!wp_mlkem_up_ref(srcCtx->mlkem)) { + wp_mlkem_kem_freectx(dstCtx); + return NULL; + } + dstCtx->mlkem = srcCtx->mlkem; + } + return dstCtx; +} + +/** + * Initialize an ML-KEM KEM context with a key. + * + * @param [in, out] ctx KEM context. + * @param [in] mlkem ML-KEM key (reference taken). + * @param [in] params Parameters. Unused. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_kem_init(wp_MlKemCtx* ctx, wp_MlKem* mlkem, + const OSSL_PARAM params[]) +{ + int ok = 1; + + (void)params; + + if ((ctx == NULL) || (mlkem == NULL)) { + ok = 0; + } + if (ok && !wp_mlkem_up_ref(mlkem)) { + ok = 0; + } + if (ok) { + wp_mlkem_free(ctx->mlkem); + ctx->mlkem = mlkem; + } + return ok; +} + +static int wp_mlkem_kem_encapsulate_init(wp_MlKemCtx* ctx, wp_MlKem* mlkem, + const OSSL_PARAM params[]) +{ + return wp_mlkem_kem_init(ctx, mlkem, params); +} + +static int wp_mlkem_kem_decapsulate_init(wp_MlKemCtx* ctx, wp_MlKem* mlkem, + const OSSL_PARAM params[]) +{ + return wp_mlkem_kem_init(ctx, mlkem, params); +} + +/** + * Encapsulate: produce ciphertext and shared secret. + * + * If out or secret is NULL, just report the output sizes. + * + * @param [in] ctx KEM context. + * @param [out] out Ciphertext buffer. + * @param [in, out] outLen On in, buffer size; on out, ciphertext length. + * @param [out] secret Shared secret buffer. + * @param [in, out] secretLen On in, buffer size; on out, secret length. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, + size_t* outLen, unsigned char* secret, size_t* secretLen) +{ + int ok = 1; + const wp_MlKemData* data; + word32 ctSize; + word32 ssSize; + + if ((ctx == NULL) || (ctx->mlkem == NULL)) { + return 0; + } + + data = wp_mlkem_get_data(ctx->mlkem); + ctSize = wp_mlkem_data_ct_size(data); + ssSize = wp_mlkem_data_ss_size(data); + + if ((out == NULL) || (secret == NULL)) { + if (outLen != NULL) { + *outLen = ctSize; + } + if (secretLen != NULL) { + *secretLen = ssSize; + } + return 1; + } + + if (ok && (*outLen < ctSize)) { + ok = 0; + } + if (ok && (*secretLen < ssSize)) { + ok = 0; + } + if (ok) { + int rc = wc_MlKemKey_Encapsulate( + (MlKemKey*)wp_mlkem_get_key(ctx->mlkem), out, secret, &ctx->rng); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + *outLen = ctSize; + *secretLen = ssSize; + } + return ok; +} + +/** + * Decapsulate: recover shared secret from ciphertext. + * + * If out is NULL, just report the secret size. + * + * @param [in] ctx KEM context. + * @param [out] out Shared secret buffer. + * @param [in, out] outLen On in, buffer size; on out, secret length. + * @param [in] in Ciphertext. + * @param [in] inLen Ciphertext length. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, + size_t* outLen, const unsigned char* in, size_t inLen) +{ + int ok = 1; + const wp_MlKemData* data; + word32 ssSize; + word32 ctSize; + + if ((ctx == NULL) || (ctx->mlkem == NULL)) { + return 0; + } + + data = wp_mlkem_get_data(ctx->mlkem); + ssSize = wp_mlkem_data_ss_size(data); + ctSize = wp_mlkem_data_ct_size(data); + + if (out == NULL) { + if (outLen != NULL) { + *outLen = ssSize; + } + return 1; + } + + if (ok && (*outLen < ssSize)) { + ok = 0; + } + if (ok && (inLen != ctSize)) { + ok = 0; + } + if (ok) { + int rc = wc_MlKemKey_Decapsulate( + (MlKemKey*)wp_mlkem_get_key(ctx->mlkem), out, in, (word32)inLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + *outLen = ssSize; + } + return ok; +} + +/** + * Get ctx params. None supported. + */ +static int wp_mlkem_kem_get_ctx_params(wp_MlKemCtx* ctx, OSSL_PARAM* params) +{ + (void)ctx; + (void)params; + return 1; +} + +static const OSSL_PARAM* wp_mlkem_kem_gettable_ctx_params(wp_MlKemCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlkem_kem_gettable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mlkem_kem_gettable; +} + +/** + * Set ctx params. None supported. + */ +static int wp_mlkem_kem_set_ctx_params(wp_MlKemCtx* ctx, + const OSSL_PARAM params[]) +{ + (void)ctx; + (void)params; + return 1; +} + +static const OSSL_PARAM* wp_mlkem_kem_settable_ctx_params(wp_MlKemCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlkem_kem_settable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mlkem_kem_settable; +} + +/** Dispatch table for ML-KEM KEM (shared across all three levels). */ +const OSSL_DISPATCH wp_mlkem_asym_kem_functions[] = { + { OSSL_FUNC_KEM_NEWCTX, + (DFUNC)wp_mlkem_kem_newctx }, + { OSSL_FUNC_KEM_FREECTX, + (DFUNC)wp_mlkem_kem_freectx }, + { OSSL_FUNC_KEM_DUPCTX, + (DFUNC)wp_mlkem_kem_dupctx }, + { OSSL_FUNC_KEM_ENCAPSULATE_INIT, + (DFUNC)wp_mlkem_kem_encapsulate_init }, + { OSSL_FUNC_KEM_ENCAPSULATE, + (DFUNC)wp_mlkem_kem_encapsulate }, + { OSSL_FUNC_KEM_DECAPSULATE_INIT, + (DFUNC)wp_mlkem_kem_decapsulate_init }, + { OSSL_FUNC_KEM_DECAPSULATE, + (DFUNC)wp_mlkem_kem_decapsulate }, + { OSSL_FUNC_KEM_GET_CTX_PARAMS, + (DFUNC)wp_mlkem_kem_get_ctx_params }, + { OSSL_FUNC_KEM_GETTABLE_CTX_PARAMS, + (DFUNC)wp_mlkem_kem_gettable_ctx_params }, + { OSSL_FUNC_KEM_SET_CTX_PARAMS, + (DFUNC)wp_mlkem_kem_set_ctx_params }, + { OSSL_FUNC_KEM_SETTABLE_CTX_PARAMS, + (DFUNC)wp_mlkem_kem_settable_ctx_params }, + { 0, NULL } +}; + +#endif /* WP_HAVE_MLKEM */ diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c new file mode 100644 index 00000000..ea6061c5 --- /dev/null +++ b/src/wp_mlkem_kmgmt.c @@ -0,0 +1,978 @@ +/* wp_mlkem_kmgmt.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WP_HAVE_MLKEM + +#include +#include + +/** Supported selections (key parts) in this key manager for ML-KEM. */ +#define WP_MLKEM_POSSIBLE_SELECTIONS \ + (OSSL_KEYMGMT_SELECT_KEYPAIR | OSSL_KEYMGMT_SELECT_ALL_PARAMETERS) + +/** + * ML-KEM parameter set data. + */ +typedef struct wp_MlKemData { + /** wolfSSL parameter type (WC_ML_KEM_512/768/1024). */ + int type; + /** Public key size in bytes. */ + word32 pubKeySize; + /** Private key size in bytes. */ + word32 privKeySize; + /** Ciphertext size in bytes. */ + word32 ctSize; + /** Security bits. */ + int securityBits; + /** Algorithm name string. */ + const char* name; +} wp_MlKemData; + +/** + * ML-KEM key object. + */ +struct wp_MlKem { + /** wolfSSL ML-KEM key. */ + MlKemKey key; + /** Parameter set data. */ + const wp_MlKemData* data; + +#ifndef WP_SINGLE_THREADED + /** Mutex for reference count updating. */ + wolfSSL_Mutex mutex; +#endif + /** Count of references to this object. */ + int refCnt; + + /** Provider context. */ + WOLFPROV_CTX* provCtx; + + /** Public key available. */ + unsigned int hasPub:1; + /** Private key available. */ + unsigned int hasPriv:1; +}; + +typedef struct wp_MlKem wp_MlKem; + +/** + * ML-KEM key generation context. + */ +typedef struct wp_MlKemGenCtx { + /** wolfSSL random number generator. */ + WC_RNG rng; + /** Parameter set data. */ + const wp_MlKemData* data; + /** Provider context. */ + WOLFPROV_CTX* provCtx; + /** Parts of key to generate. */ + int selection; +} wp_MlKemGenCtx; + + +/* Parameter set tables. */ +static const wp_MlKemData mlkem512Data = { + WC_ML_KEM_512, + WC_ML_KEM_512_PUBLIC_KEY_SIZE, + WC_ML_KEM_512_PRIVATE_KEY_SIZE, + WC_ML_KEM_512_CIPHER_TEXT_SIZE, + 128, + "ML-KEM-512" +}; + +static const wp_MlKemData mlkem768Data = { + WC_ML_KEM_768, + WC_ML_KEM_768_PUBLIC_KEY_SIZE, + WC_ML_KEM_768_PRIVATE_KEY_SIZE, + WC_ML_KEM_768_CIPHER_TEXT_SIZE, + 192, + "ML-KEM-768" +}; + +static const wp_MlKemData mlkem1024Data = { + WC_ML_KEM_1024, + WC_ML_KEM_1024_PUBLIC_KEY_SIZE, + WC_ML_KEM_1024_PRIVATE_KEY_SIZE, + WC_ML_KEM_1024_CIPHER_TEXT_SIZE, + 256, + "ML-KEM-1024" +}; + + +/** + * Increment reference count for key. + * + * @param [in, out] mlkem ML-KEM key object. + * @return 1 on success, 0 on failure. + */ +int wp_mlkem_up_ref(wp_MlKem* mlkem) +{ +#ifndef WP_SINGLE_THREADED + int ok = 1; + int rc; + + rc = wc_LockMutex(&mlkem->mutex); + if (rc < 0) { + ok = 0; + } + if (ok) { + mlkem->refCnt++; + wc_UnLockMutex(&mlkem->mutex); + } + return ok; +#else + mlkem->refCnt++; + return 1; +#endif +} + +/** + * Get the wolfSSL ML-KEM key from the wp_MlKem object. + * + * @param [in] mlkem ML-KEM key object. + * @return Pointer to wolfSSL MlKemKey, returned as void*. + */ +void* wp_mlkem_get_key(wp_MlKem* mlkem) +{ + return &mlkem->key; +} + +/** + * Get the parameter set data from the wp_MlKem object. + * + * @param [in] mlkem ML-KEM key object. + * @return Pointer to parameter set data. + */ +const wp_MlKemData* wp_mlkem_get_data(const wp_MlKem* mlkem) +{ + return mlkem->data; +} + +/** + * Get the ciphertext size for an ML-KEM parameter set. + * + * @param [in] data Parameter set data. + * @return Ciphertext size in bytes. + */ +word32 wp_mlkem_data_ct_size(const wp_MlKemData* data) +{ + return data->ctSize; +} + +/** + * Get the shared secret size for ML-KEM (constant 32 bytes). + * + * @param [in] data Parameter set data. Unused. + * @return Shared secret size in bytes. + */ +word32 wp_mlkem_data_ss_size(const wp_MlKemData* data) +{ + (void)data; + return WC_ML_KEM_SS_SZ; +} + +/** + * Create a new ML-KEM key object. + * + * @param [in] provCtx Provider context. + * @param [in] data Parameter set data. + * @return New ML-KEM key object on success, NULL on failure. + */ +static wp_MlKem* wp_mlkem_new(WOLFPROV_CTX* provCtx, const wp_MlKemData* data) +{ + wp_MlKem* mlkem = NULL; + + if (wolfssl_prov_is_running()) { + mlkem = (wp_MlKem*)OPENSSL_zalloc(sizeof(*mlkem)); + } + if (mlkem != NULL) { + int ok = 1; + int rc; + + rc = wc_MlKemKey_Init(&mlkem->key, data->type, NULL, INVALID_DEVID); + if (rc != 0) { + ok = 0; + } + #ifndef WP_SINGLE_THREADED + if (ok) { + rc = wc_InitMutex(&mlkem->mutex); + if (rc != 0) { + wc_MlKemKey_Free(&mlkem->key); + ok = 0; + } + } + #endif + if (ok) { + mlkem->provCtx = provCtx; + mlkem->refCnt = 1; + mlkem->data = data; + } + if (!ok) { + OPENSSL_free(mlkem); + mlkem = NULL; + } + } + + return mlkem; +} + +/** + * Dispose of ML-KEM key object. + * + * @param [in, out] mlkem ML-KEM key object. May be NULL. + */ +void wp_mlkem_free(wp_MlKem* mlkem) +{ + if (mlkem != NULL) { + int cnt; + #ifndef WP_SINGLE_THREADED + int rc; + + rc = wc_LockMutex(&mlkem->mutex); + cnt = --mlkem->refCnt; + if (rc == 0) { + wc_UnLockMutex(&mlkem->mutex); + } + #else + cnt = --mlkem->refCnt; + #endif + + if (cnt == 0) { + #ifndef WP_SINGLE_THREADED + wc_FreeMutex(&mlkem->mutex); + #endif + wc_MlKemKey_Free(&mlkem->key); + OPENSSL_free(mlkem); + } + } +} + +/** + * Duplicate ML-KEM key object. + * + * @param [in] src Source ML-KEM key object. + * @param [in] selection Parts of key to include. Unused; always full dup. + * @return New ML-KEM key object on success, NULL on failure. + */ +static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) +{ + wp_MlKem* dst = NULL; + unsigned char* pubBuf = NULL; + unsigned char* privBuf = NULL; + + (void)selection; + + if (!wolfssl_prov_is_running() || (src == NULL)) { + return NULL; + } + + dst = wp_mlkem_new(src->provCtx, src->data); + if (dst == NULL) { + return NULL; + } + + if (src->hasPub) { + int ok = 1; + word32 pubLen = src->data->pubKeySize; + int rc; + + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePublicKey((MlKemKey*)&src->key, pubBuf, + pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_MlKemKey_DecodePublicKey(&dst->key, pubBuf, pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPub = 1; + } + if (!ok) { + OPENSSL_free(pubBuf); + wp_mlkem_free(dst); + return NULL; + } + OPENSSL_free(pubBuf); + } + + if (src->hasPriv) { + int ok = 1; + word32 privLen = src->data->privKeySize; + int rc; + + privBuf = (unsigned char*)OPENSSL_malloc(privLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePrivateKey((MlKemKey*)&src->key, privBuf, + privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_MlKemKey_DecodePrivateKey(&dst->key, privBuf, privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPriv = 1; + } + OPENSSL_clear_free(privBuf, privLen); + if (!ok) { + wp_mlkem_free(dst); + return NULL; + } + } + + return dst; +} + +/** + * Load an ML-KEM key from a reference. + * + * @param [in, out] pMlKem Pointer to an ML-KEM key reference. + * @param [in] size Size of reference object. Unused. + * @return ML-KEM key object on success. + */ +static const wp_MlKem* wp_mlkem_load(const wp_MlKem** pMlKem, size_t size) +{ + const wp_MlKem* mlkem = *pMlKem; + (void)size; + *pMlKem = NULL; + return mlkem; +} + +/** + * Check ML-KEM key object has the components required. + * + * @param [in] mlkem ML-KEM key object. + * @param [in] selection Parts of key required. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_has(const wp_MlKem* mlkem, int selection) +{ + int ok = 1; + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (mlkem == NULL)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + ok &= mlkem->hasPub; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + ok &= mlkem->hasPriv; + } + return ok; +} + +/** + * Compare two ML-KEM keys. + * + * @param [in] a First ML-KEM key. + * @param [in] b Second ML-KEM key. + * @param [in] selection Parts of key to compare. + * @return 1 if match, 0 otherwise. + */ +static int wp_mlkem_match(const wp_MlKem* a, const wp_MlKem* b, int selection) +{ + int ok = 1; + unsigned char* bufA = NULL; + unsigned char* bufB = NULL; + word32 lenA; + word32 lenB; + int rc; + + if (!wolfssl_prov_is_running() || (a == NULL) || (b == NULL)) { + return 0; + } + if (a->data->type != b->data->type) { + return 0; + } + if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + lenA = a->data->pubKeySize; + lenB = b->data->pubKeySize; + bufA = (unsigned char*)OPENSSL_malloc(lenA); + bufB = (unsigned char*)OPENSSL_malloc(lenB); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePublicKey((MlKemKey*)&a->key, bufA, lenA); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_MlKemKey_EncodePublicKey((MlKemKey*)&b->key, bufB, lenB); + if (rc != 0) { + ok = 0; + } + } + if (ok && ((lenA != lenB) || (XMEMCMP(bufA, bufB, lenA) != 0))) { + ok = 0; + } + OPENSSL_free(bufA); + OPENSSL_free(bufB); + bufA = NULL; + bufB = NULL; + } + return ok; +} + +/** + * Import an ML-KEM key from parameters. + * + * @param [in, out] mlkem ML-KEM key object. + * @param [in] selection Parts of key to import. + * @param [in] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_import(wp_MlKem* mlkem, int selection, + const OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + unsigned char* privData = NULL; + unsigned char* pubData = NULL; + size_t privLen = 0; + size_t pubLen = 0; + + if (!wolfssl_prov_is_running() || (mlkem == NULL)) { + ok = 0; + } + if (ok && ((selection & WP_MLKEM_POSSIBLE_SELECTIONS) == 0)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PRIV_KEY, + &privData, &privLen)) { + ok = 0; + } + if (ok && (privData != NULL)) { + rc = wc_MlKemKey_DecodePrivateKey(&mlkem->key, privData, + (word32)privLen); + if (rc != 0) { + ok = 0; + } + if (ok) { + mlkem->hasPriv = 1; + mlkem->hasPub = 1; + } + } + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PUB_KEY, + &pubData, &pubLen)) { + ok = 0; + } + if (ok && (pubData != NULL)) { + rc = wc_MlKemKey_DecodePublicKey(&mlkem->key, pubData, + (word32)pubLen); + if (rc != 0) { + ok = 0; + } + if (ok) { + mlkem->hasPub = 1; + } + } + } + if (ok && (privData == NULL) && (pubData == NULL)) { + ok = 0; + } + return ok; +} + +/** ML-KEM key parameters for import/export type queries. */ +static const OSSL_PARAM wp_mlkem_key_params[] = { + /* 0: none */ + OSSL_PARAM_END, + + /* 1: private only */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_END, + + /* 3: public only */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_END, + + /* 5: both */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_END, +}; + +static const OSSL_PARAM* wp_mlkem_key_types(int selection) +{ + int idx = 0; + int extra = 0; + + if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + idx += 3; + extra++; + } + if ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) { + idx += 1 + extra; + } + return &wp_mlkem_key_params[idx]; +} + +static const OSSL_PARAM* wp_mlkem_import_types(int selection) +{ + return wp_mlkem_key_types(selection); +} + +static const OSSL_PARAM* wp_mlkem_export_types(int selection) +{ + return wp_mlkem_key_types(selection); +} + +/** + * Export ML-KEM key data via callback. + * + * @param [in] mlkem ML-KEM key object. + * @param [in] selection Parts of key to export. + * @param [in] paramCb Callback to receive constructed parameters. + * @param [in] cbArg Argument to pass to callback. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_export(wp_MlKem* mlkem, int selection, + OSSL_CALLBACK* paramCb, void* cbArg) +{ + int ok = 1; + int rc; + OSSL_PARAM params[3]; + int paramsSz = 0; + unsigned char* pubBuf = NULL; + unsigned char* privBuf = NULL; + word32 pubLen = 0; + word32 privLen = 0; + int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; + int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + + if (!wolfssl_prov_is_running() || (mlkem == NULL)) { + ok = 0; + } + XMEMSET(params, 0, sizeof(params)); + + if (ok && expPub && mlkem->hasPub) { + pubLen = mlkem->data->pubKeySize; + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, pubBuf, pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + wp_param_set_octet_string_ptr(¶ms[paramsSz++], + OSSL_PKEY_PARAM_PUB_KEY, pubBuf, pubLen); + } + } + if (ok && expPriv && mlkem->hasPriv) { + privLen = mlkem->data->privKeySize; + privBuf = (unsigned char*)OPENSSL_malloc(privLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, privBuf, privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + wp_param_set_octet_string_ptr(¶ms[paramsSz++], + OSSL_PKEY_PARAM_PRIV_KEY, privBuf, privLen); + } + } + if (ok) { + ok = paramCb(params, cbArg); + } + OPENSSL_free(pubBuf); + OPENSSL_clear_free(privBuf, privLen); + return ok; +} + +/** + * Gettable parameters for ML-KEM key. + * + * @param [in] provCtx Provider context. Unused. + * @return Array of supported gettable parameters. + */ +static const OSSL_PARAM* wp_mlkem_gettable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlkem_supported_gettable_params[] = { + OSSL_PARAM_int(OSSL_PKEY_PARAM_BITS, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_SECURITY_BITS, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_MAX_SIZE, NULL), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_END + }; + (void)provCtx; + return wp_mlkem_supported_gettable_params; +} + +/** + * Get ML-KEM key parameters. + * + * @param [in] mlkem ML-KEM key object. + * @param [in, out] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + OSSL_PARAM* p; + + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); + if ((p != NULL) && !OSSL_PARAM_set_int(p, (int)mlkem->data->pubKeySize * 8)) { + ok = 0; + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_BITS); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, mlkem->data->securityBits)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_MAX_SIZE); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, (int)mlkem->data->ctSize)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PUB_KEY); + if (p != NULL) { + word32 outLen = mlkem->data->pubKeySize; + if (p->data == NULL) { + p->return_size = outLen; + } + else if (mlkem->hasPub) { + rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PRIV_KEY); + if (p != NULL) { + word32 outLen = mlkem->data->privKeySize; + if (p->data == NULL) { + p->return_size = outLen; + } + else if (mlkem->hasPriv) { + rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + } + return ok; +} + +/** + * Settable parameters for ML-KEM key. + * + * @param [in] provCtx Provider context. Unused. + * @return Empty parameter list. + */ +static const OSSL_PARAM* wp_mlkem_settable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlkem_supported_settable_params[] = { + OSSL_PARAM_END + }; + (void)provCtx; + return wp_mlkem_supported_settable_params; +} + +/** + * Set ML-KEM key parameters. None supported. + * + * @param [in] mlkem ML-KEM key object. Unused. + * @param [in] params Array of parameters. Unused. + * @return 1 always. + */ +static int wp_mlkem_set_params(wp_MlKem* mlkem, const OSSL_PARAM params[]) +{ + (void)mlkem; + (void)params; + return 1; +} + +/* + * ML-KEM generation + */ + +/** + * Create ML-KEM generation context object. + * + * @param [in] provCtx Provider context. + * @param [in] selection Parts of the key to generate. + * @param [in] params Parameters to set for generation. + * @param [in] data Parameter set data. + * @return New ML-KEM generation context on success, NULL on failure. + */ +static wp_MlKemGenCtx* wp_mlkem_gen_init_base(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[], const wp_MlKemData* data) +{ + wp_MlKemGenCtx* ctx = NULL; + + (void)params; + + if (wolfssl_prov_is_running() && + ((selection & WP_MLKEM_POSSIBLE_SELECTIONS) != 0)) { + ctx = (wp_MlKemGenCtx*)OPENSSL_zalloc(sizeof(*ctx)); + } + if (ctx != NULL) { + int rc; + int ok = 1; + + rc = wc_InitRng(&ctx->rng); + if (rc != 0) { + ok = 0; + } + if (ok) { + ctx->provCtx = provCtx; + ctx->data = data; + ctx->selection = selection; + } + if (!ok) { + OPENSSL_free(ctx); + ctx = NULL; + } + } + return ctx; +} + +/** + * Generate ML-KEM key pair. + * + * @param [in, out] ctx ML-KEM generation context. + * @param [in] cb Progress callback. Unused. + * @param [in] cbArg Argument for callback. Unused. + * @return ML-KEM key object on success, NULL on failure. + */ +static wp_MlKem* wp_mlkem_gen(wp_MlKemGenCtx* ctx, OSSL_CALLBACK* osslcb, + void* cbarg) +{ + wp_MlKem* mlkem; + int keyPair = (ctx->selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0; + + (void)osslcb; + (void)cbarg; + + mlkem = wp_mlkem_new(ctx->provCtx, ctx->data); + if ((mlkem != NULL) && keyPair) { + int rc = wc_MlKemKey_MakeKey(&mlkem->key, &ctx->rng); + if (rc != 0) { + wp_mlkem_free(mlkem); + mlkem = NULL; + } + else { + mlkem->hasPub = 1; + mlkem->hasPriv = 1; + } + } + return mlkem; +} + +/** + * Set parameters into ML-KEM generation context. None supported. + * + * @param [in] ctx Generation context. Unused. + * @param [in] params Array of parameters. Unused. + * @return 1 always. + */ +static int wp_mlkem_gen_set_params(wp_MlKemGenCtx* ctx, + const OSSL_PARAM params[]) +{ + (void)ctx; + (void)params; + return 1; +} + +/** + * Settable parameters for ML-KEM generation context. + * + * @param [in] ctx Generation context. Unused. + * @param [in] provCtx Provider context. Unused. + * @return Empty parameter list. + */ +static const OSSL_PARAM* wp_mlkem_gen_settable_params(wp_MlKemGenCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static OSSL_PARAM wp_mlkem_gen_settable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mlkem_gen_settable; +} + +/** + * Free ML-KEM generation context. + * + * @param [in, out] ctx Generation context. + */ +static void wp_mlkem_gen_cleanup(wp_MlKemGenCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + OPENSSL_free(ctx); + } +} + +/** + * Return the algorithm name for OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME. + * + * @param [in] op Operation type. Unused. + * @return Empty string (default). + */ +static const char* wp_mlkem_query_operation_name(int op) +{ + (void)op; + return NULL; +} + +/* Per-level new() and gen_init() trampolines. */ + +static wp_MlKem* wp_mlkem512_new(WOLFPROV_CTX* provCtx) +{ + return wp_mlkem_new(provCtx, &mlkem512Data); +} + +static wp_MlKem* wp_mlkem768_new(WOLFPROV_CTX* provCtx) +{ + return wp_mlkem_new(provCtx, &mlkem768Data); +} + +static wp_MlKem* wp_mlkem1024_new(WOLFPROV_CTX* provCtx) +{ + return wp_mlkem_new(provCtx, &mlkem1024Data); +} + +static wp_MlKemGenCtx* wp_mlkem512_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mlkem_gen_init_base(provCtx, selection, params, &mlkem512Data); +} + +static wp_MlKemGenCtx* wp_mlkem768_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mlkem_gen_init_base(provCtx, selection, params, &mlkem768Data); +} + +static wp_MlKemGenCtx* wp_mlkem1024_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mlkem_gen_init_base(provCtx, selection, params, &mlkem1024Data); +} + +/* + * Dispatch tables + */ + +#define IMPLEMENT_MLKEM_KEYMGMT_DISPATCH(alg) \ +const OSSL_DISPATCH wp_##alg##_keymgmt_functions[] = { \ + { OSSL_FUNC_KEYMGMT_NEW, \ + (DFUNC)wp_##alg##_new }, \ + { OSSL_FUNC_KEYMGMT_FREE, (DFUNC)wp_mlkem_free }, \ + { OSSL_FUNC_KEYMGMT_DUP, (DFUNC)wp_mlkem_dup }, \ + { OSSL_FUNC_KEYMGMT_GEN_INIT, \ + (DFUNC)wp_##alg##_gen_init }, \ + { OSSL_FUNC_KEYMGMT_GEN_SET_PARAMS, \ + (DFUNC)wp_mlkem_gen_set_params }, \ + { OSSL_FUNC_KEYMGMT_GEN_SETTABLE_PARAMS, \ + (DFUNC)wp_mlkem_gen_settable_params }, \ + { OSSL_FUNC_KEYMGMT_GEN, (DFUNC)wp_mlkem_gen }, \ + { OSSL_FUNC_KEYMGMT_GEN_CLEANUP, \ + (DFUNC)wp_mlkem_gen_cleanup }, \ + { OSSL_FUNC_KEYMGMT_LOAD, (DFUNC)wp_mlkem_load }, \ + { OSSL_FUNC_KEYMGMT_GET_PARAMS, \ + (DFUNC)wp_mlkem_get_params }, \ + { OSSL_FUNC_KEYMGMT_GETTABLE_PARAMS, \ + (DFUNC)wp_mlkem_gettable_params }, \ + { OSSL_FUNC_KEYMGMT_SET_PARAMS, \ + (DFUNC)wp_mlkem_set_params }, \ + { OSSL_FUNC_KEYMGMT_SETTABLE_PARAMS, \ + (DFUNC)wp_mlkem_settable_params }, \ + { OSSL_FUNC_KEYMGMT_HAS, (DFUNC)wp_mlkem_has }, \ + { OSSL_FUNC_KEYMGMT_MATCH, (DFUNC)wp_mlkem_match }, \ + { OSSL_FUNC_KEYMGMT_IMPORT, (DFUNC)wp_mlkem_import }, \ + { OSSL_FUNC_KEYMGMT_IMPORT_TYPES, \ + (DFUNC)wp_mlkem_import_types }, \ + { OSSL_FUNC_KEYMGMT_EXPORT, (DFUNC)wp_mlkem_export }, \ + { OSSL_FUNC_KEYMGMT_EXPORT_TYPES, \ + (DFUNC)wp_mlkem_export_types }, \ + { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ + (DFUNC)wp_mlkem_query_operation_name }, \ + { 0, NULL } \ +}; + +IMPLEMENT_MLKEM_KEYMGMT_DISPATCH(mlkem512) +IMPLEMENT_MLKEM_KEYMGMT_DISPATCH(mlkem768) +IMPLEMENT_MLKEM_KEYMGMT_DISPATCH(mlkem1024) + +#endif /* WP_HAVE_MLKEM */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index 099d9290..b901c4e8 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -663,6 +663,15 @@ static const OSSL_ALGORITHM wolfprov_keymgmt[] = { { WP_NAMES_TLS1_3_KDF, WOLFPROV_PROPERTIES, wp_kdf_keymgmt_functions, "HKDF" }, +#ifdef WP_HAVE_MLKEM + { WP_NAMES_ML_KEM_512, WOLFPROV_PROPERTIES, + wp_mlkem512_keymgmt_functions, "ML-KEM-512" }, + { WP_NAMES_ML_KEM_768, WOLFPROV_PROPERTIES, + wp_mlkem768_keymgmt_functions, "ML-KEM-768" }, + { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, + wp_mlkem1024_keymgmt_functions, "ML-KEM-1024" }, +#endif + { NULL, NULL, NULL, NULL } }; @@ -741,6 +750,14 @@ static const OSSL_ALGORITHM wolfprov_asym_kem[] = { #ifdef WP_HAVE_RSA { WP_NAMES_RSA, WOLFPROV_PROPERTIES, wp_rsa_asym_kem_functions, "" }, +#endif +#ifdef WP_HAVE_MLKEM + { WP_NAMES_ML_KEM_512, WOLFPROV_PROPERTIES, + wp_mlkem_asym_kem_functions, "" }, + { WP_NAMES_ML_KEM_768, WOLFPROV_PROPERTIES, + wp_mlkem_asym_kem_functions, "" }, + { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, + wp_mlkem_asym_kem_functions, "" }, #endif { NULL, NULL, NULL, NULL } }; From be256273b6e81353f813037a4cc477516c23e52c Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:24:55 -0700 Subject: [PATCH 03/16] Add ML-DSA keymgmt and signature dispatch for 44/65/87 --- src/include.am | 2 + src/wp_mldsa_kmgmt.c | 969 +++++++++++++++++++++++++++++++++++++++++++ src/wp_mldsa_sig.c | 463 +++++++++++++++++++++ src/wp_wolfprov.c | 16 + 4 files changed, 1450 insertions(+) create mode 100644 src/wp_mldsa_kmgmt.c create mode 100644 src/wp_mldsa_sig.c diff --git a/src/include.am b/src/include.am index 8ae7d630..21db6007 100644 --- a/src/include.am +++ b/src/include.am @@ -38,6 +38,8 @@ libwolfprov_la_SOURCES += src/wp_dh_kmgmt.c libwolfprov_la_SOURCES += src/wp_dh_exch.c libwolfprov_la_SOURCES += src/wp_mlkem_kmgmt.c libwolfprov_la_SOURCES += src/wp_mlkem_kem.c +libwolfprov_la_SOURCES += src/wp_mldsa_kmgmt.c +libwolfprov_la_SOURCES += src/wp_mldsa_sig.c libwolfprov_la_SOURCES += src/wp_drbg.c libwolfprov_la_SOURCES += src/wp_seed_src.c libwolfprov_la_SOURCES += src/wp_dec_pem2der.c diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c new file mode 100644 index 00000000..578c0527 --- /dev/null +++ b/src/wp_mldsa_kmgmt.c @@ -0,0 +1,969 @@ +/* wp_mldsa_kmgmt.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WP_HAVE_MLDSA + +#include + +/** Supported selections (key parts) in this key manager for ML-DSA. */ +#define WP_MLDSA_POSSIBLE_SELECTIONS \ + (OSSL_KEYMGMT_SELECT_KEYPAIR | OSSL_KEYMGMT_SELECT_ALL_PARAMETERS) + +/** + * ML-DSA parameter set data. + */ +typedef struct wp_MlDsaData { + /** Level byte passed to wc_MlDsaKey_SetParams (2/3/5). */ + byte level; + /** Public key size in bytes. */ + word32 pubKeySize; + /** Private key size in bytes (raw, excludes embedded pub). */ + word32 privKeySize; + /** Signature size in bytes. */ + word32 sigSize; + /** Security bits. */ + int securityBits; + /** Algorithm name string. */ + const char* name; +} wp_MlDsaData; + +/** + * ML-DSA key object. + */ +struct wp_MlDsa { + /** wolfSSL ML-DSA key. */ + MlDsaKey key; + /** Parameter set data. */ + const wp_MlDsaData* data; + +#ifndef WP_SINGLE_THREADED + /** Mutex for reference count updating. */ + wolfSSL_Mutex mutex; +#endif + /** Count of references to this object. */ + int refCnt; + + /** Provider context. */ + WOLFPROV_CTX* provCtx; + + /** Public key available. */ + unsigned int hasPub:1; + /** Private key available. */ + unsigned int hasPriv:1; +}; + +typedef struct wp_MlDsa wp_MlDsa; + +/** + * ML-DSA key generation context. + */ +typedef struct wp_MlDsaGenCtx { + /** wolfSSL random number generator. */ + WC_RNG rng; + /** Parameter set data. */ + const wp_MlDsaData* data; + /** Provider context. */ + WOLFPROV_CTX* provCtx; + /** Parts of key to generate. */ + int selection; +} wp_MlDsaGenCtx; + + +/* Parameter set tables. */ +static const wp_MlDsaData mldsa44Data = { + WC_ML_DSA_44, + ML_DSA_LEVEL2_PUB_KEY_SIZE, + ML_DSA_LEVEL2_KEY_SIZE, + ML_DSA_LEVEL2_SIG_SIZE, + 128, + "ML-DSA-44" +}; + +static const wp_MlDsaData mldsa65Data = { + WC_ML_DSA_65, + ML_DSA_LEVEL3_PUB_KEY_SIZE, + ML_DSA_LEVEL3_KEY_SIZE, + ML_DSA_LEVEL3_SIG_SIZE, + 192, + "ML-DSA-65" +}; + +static const wp_MlDsaData mldsa87Data = { + WC_ML_DSA_87, + ML_DSA_LEVEL5_PUB_KEY_SIZE, + ML_DSA_LEVEL5_KEY_SIZE, + ML_DSA_LEVEL5_SIG_SIZE, + 256, + "ML-DSA-87" +}; + + +/** + * Increment reference count for key. + * + * @param [in, out] mldsa ML-DSA key object. + * @return 1 on success, 0 on failure. + */ +int wp_mldsa_up_ref(wp_MlDsa* mldsa) +{ +#ifndef WP_SINGLE_THREADED + int ok = 1; + int rc; + + rc = wc_LockMutex(&mldsa->mutex); + if (rc < 0) { + ok = 0; + } + if (ok) { + mldsa->refCnt++; + wc_UnLockMutex(&mldsa->mutex); + } + return ok; +#else + mldsa->refCnt++; + return 1; +#endif +} + +/** + * Get the wolfSSL ML-DSA key from the wp_MlDsa object. + * + * @param [in] mldsa ML-DSA key object. + * @return Pointer to wolfSSL MlDsaKey, returned as void*. + */ +void* wp_mldsa_get_key(wp_MlDsa* mldsa) +{ + return &mldsa->key; +} + +/** + * Get the ML-DSA level (2/3/5) for the key. + * + * @param [in] mldsa ML-DSA key object. + * @return Level value, or 0 if mldsa is NULL. + */ +int wp_mldsa_get_level(const wp_MlDsa* mldsa) +{ + if (mldsa == NULL) { + return 0; + } + return mldsa->data->level; +} + +/** + * Get the maximum signature size for the key. + * + * @param [in] mldsa ML-DSA key object. + * @return Signature size in bytes, or 0 if mldsa is NULL. + */ +int wp_mldsa_get_sig_size(const wp_MlDsa* mldsa) +{ + if (mldsa == NULL) { + return 0; + } + return (int)mldsa->data->sigSize; +} + +/** + * Create a new ML-DSA key object. + * + * @param [in] provCtx Provider context. + * @param [in] data Parameter set data. + * @return New ML-DSA key object on success, NULL on failure. + */ +static wp_MlDsa* wp_mldsa_new(WOLFPROV_CTX* provCtx, const wp_MlDsaData* data) +{ + wp_MlDsa* mldsa = NULL; + + if (wolfssl_prov_is_running()) { + mldsa = (wp_MlDsa*)OPENSSL_zalloc(sizeof(*mldsa)); + } + if (mldsa != NULL) { + int ok = 1; + int rc; + + rc = wc_dilithium_init_ex(&mldsa->key, NULL, INVALID_DEVID); + if (rc != 0) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_set_level(&mldsa->key, data->level); + if (rc != 0) { + wc_dilithium_free(&mldsa->key); + ok = 0; + } + } + #ifndef WP_SINGLE_THREADED + if (ok) { + rc = wc_InitMutex(&mldsa->mutex); + if (rc != 0) { + wc_dilithium_free(&mldsa->key); + ok = 0; + } + } + #endif + if (ok) { + mldsa->provCtx = provCtx; + mldsa->refCnt = 1; + mldsa->data = data; + } + if (!ok) { + OPENSSL_free(mldsa); + mldsa = NULL; + } + } + return mldsa; +} + +/** + * Dispose of ML-DSA key object. + * + * @param [in, out] mldsa ML-DSA key object. May be NULL. + */ +void wp_mldsa_free(wp_MlDsa* mldsa) +{ + if (mldsa != NULL) { + int cnt; + #ifndef WP_SINGLE_THREADED + int rc; + + rc = wc_LockMutex(&mldsa->mutex); + cnt = --mldsa->refCnt; + if (rc == 0) { + wc_UnLockMutex(&mldsa->mutex); + } + #else + cnt = --mldsa->refCnt; + #endif + + if (cnt == 0) { + #ifndef WP_SINGLE_THREADED + wc_FreeMutex(&mldsa->mutex); + #endif + wc_dilithium_free(&mldsa->key); + OPENSSL_free(mldsa); + } + } +} + +/** + * Duplicate ML-DSA key object via raw export/import. + * + * @param [in] src Source ML-DSA key object. + * @param [in] selection Parts of key to include. Unused; always full dup. + * @return New ML-DSA key object on success, NULL on failure. + */ +static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) +{ + wp_MlDsa* dst = NULL; + unsigned char* pubBuf = NULL; + unsigned char* privBuf = NULL; + word32 pubLen; + word32 privLen; + int rc; + int ok = 1; + + (void)selection; + + if (!wolfssl_prov_is_running() || (src == NULL)) { + return NULL; + } + + dst = wp_mldsa_new(src->provCtx, src->data); + if (dst == NULL) { + return NULL; + } + + if (src->hasPub) { + pubLen = src->data->pubKeySize; + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_export_public((MlDsaKey*)&src->key, pubBuf, + &pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_dilithium_import_public(pubBuf, pubLen, &dst->key); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPub = 1; + } + OPENSSL_free(pubBuf); + pubBuf = NULL; + } + + if (ok && src->hasPriv) { + privLen = src->data->privKeySize; + privBuf = (unsigned char*)OPENSSL_malloc(privLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_export_private((MlDsaKey*)&src->key, privBuf, + &privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_dilithium_import_private(privBuf, privLen, &dst->key); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPriv = 1; + } + OPENSSL_clear_free(privBuf, privLen); + } + + if (!ok) { + wp_mldsa_free(dst); + return NULL; + } + return dst; +} + +/** + * Load an ML-DSA key from a reference. + * + * @param [in, out] pMlDsa Pointer to an ML-DSA key reference. + * @param [in] size Size of reference object. Unused. + * @return ML-DSA key object on success. + */ +static const wp_MlDsa* wp_mldsa_load(const wp_MlDsa** pMlDsa, size_t size) +{ + const wp_MlDsa* mldsa = *pMlDsa; + (void)size; + *pMlDsa = NULL; + return mldsa; +} + +/** + * Check ML-DSA key object has the components required. + * + * @param [in] mldsa ML-DSA key object. + * @param [in] selection Parts of key required. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_has(const wp_MlDsa* mldsa, int selection) +{ + int ok = 1; + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (mldsa == NULL)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + ok &= mldsa->hasPub; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + ok &= mldsa->hasPriv; + } + return ok; +} + +/** + * Compare two ML-DSA keys. + * + * @param [in] a First ML-DSA key. + * @param [in] b Second ML-DSA key. + * @param [in] selection Parts of key to compare. + * @return 1 if match, 0 otherwise. + */ +static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) +{ + int ok = 1; + int rc; + unsigned char* bufA = NULL; + unsigned char* bufB = NULL; + word32 lenA; + word32 lenB; + + if (!wolfssl_prov_is_running() || (a == NULL) || (b == NULL)) { + return 0; + } + if (a->data->level != b->data->level) { + return 0; + } + if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + lenA = a->data->pubKeySize; + lenB = b->data->pubKeySize; + bufA = (unsigned char*)OPENSSL_malloc(lenA); + bufB = (unsigned char*)OPENSSL_malloc(lenB); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_export_public((MlDsaKey*)&a->key, bufA, &lenA); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_dilithium_export_public((MlDsaKey*)&b->key, bufB, &lenB); + if (rc != 0) { + ok = 0; + } + } + if (ok && ((lenA != lenB) || (XMEMCMP(bufA, bufB, lenA) != 0))) { + ok = 0; + } + OPENSSL_free(bufA); + OPENSSL_free(bufB); + } + return ok; +} + +/** + * Import an ML-DSA key from parameters. + * + * @param [in, out] mldsa ML-DSA key object. + * @param [in] selection Parts of key to import. + * @param [in] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, + const OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + unsigned char* privData = NULL; + unsigned char* pubData = NULL; + size_t privLen = 0; + size_t pubLen = 0; + + if (!wolfssl_prov_is_running() || (mldsa == NULL)) { + ok = 0; + } + if (ok && ((selection & WP_MLDSA_POSSIBLE_SELECTIONS) == 0)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PRIV_KEY, + &privData, &privLen)) { + ok = 0; + } + if (ok && (privData != NULL)) { + rc = wc_dilithium_import_private(privData, (word32)privLen, + &mldsa->key); + if (rc != 0) { + ok = 0; + } + if (ok) { + mldsa->hasPriv = 1; + } + } + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + if (!wp_params_get_octet_string_ptr(params, OSSL_PKEY_PARAM_PUB_KEY, + &pubData, &pubLen)) { + ok = 0; + } + if (ok && (pubData != NULL)) { + rc = wc_dilithium_import_public(pubData, (word32)pubLen, + &mldsa->key); + if (rc != 0) { + ok = 0; + } + if (ok) { + mldsa->hasPub = 1; + } + } + } + if (ok && (privData == NULL) && (pubData == NULL)) { + ok = 0; + } + return ok; +} + +/** ML-DSA key parameters for import/export type queries. */ +static const OSSL_PARAM wp_mldsa_key_params[] = { + /* 0: none */ + OSSL_PARAM_END, + + /* 1: private only */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_END, + + /* 3: public only */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_END, + + /* 5: both */ + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_END, +}; + +static const OSSL_PARAM* wp_mldsa_key_types(int selection) +{ + int idx = 0; + int extra = 0; + + if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + idx += 3; + extra++; + } + if ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) { + idx += 1 + extra; + } + return &wp_mldsa_key_params[idx]; +} + +static const OSSL_PARAM* wp_mldsa_import_types(int selection) +{ + return wp_mldsa_key_types(selection); +} + +static const OSSL_PARAM* wp_mldsa_export_types(int selection) +{ + return wp_mldsa_key_types(selection); +} + +/** + * Export ML-DSA key data via callback. + * + * @param [in] mldsa ML-DSA key object. + * @param [in] selection Parts of key to export. + * @param [in] paramCb Callback to receive constructed parameters. + * @param [in] cbArg Argument to pass to callback. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, + OSSL_CALLBACK* paramCb, void* cbArg) +{ + int ok = 1; + int rc; + OSSL_PARAM params[3]; + int paramsSz = 0; + unsigned char* pubBuf = NULL; + unsigned char* privBuf = NULL; + word32 pubLen = 0; + word32 privLen = 0; + int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; + int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + + if (!wolfssl_prov_is_running() || (mldsa == NULL)) { + ok = 0; + } + XMEMSET(params, 0, sizeof(params)); + + if (ok && expPub && mldsa->hasPub) { + pubLen = mldsa->data->pubKeySize; + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_export_public(&mldsa->key, pubBuf, &pubLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + wp_param_set_octet_string_ptr(¶ms[paramsSz++], + OSSL_PKEY_PARAM_PUB_KEY, pubBuf, pubLen); + } + } + if (ok && expPriv && mldsa->hasPriv) { + privLen = mldsa->data->privKeySize; + privBuf = (unsigned char*)OPENSSL_malloc(privLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_export_private(&mldsa->key, privBuf, &privLen); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + wp_param_set_octet_string_ptr(¶ms[paramsSz++], + OSSL_PKEY_PARAM_PRIV_KEY, privBuf, privLen); + } + } + if (ok) { + ok = paramCb(params, cbArg); + } + OPENSSL_free(pubBuf); + OPENSSL_clear_free(privBuf, privLen); + return ok; +} + +/** + * Gettable parameters for ML-DSA key. + * + * @param [in] provCtx Provider context. Unused. + * @return Array of supported gettable parameters. + */ +static const OSSL_PARAM* wp_mldsa_gettable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mldsa_supported_gettable_params[] = { + OSSL_PARAM_int(OSSL_PKEY_PARAM_BITS, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_SECURITY_BITS, NULL), + OSSL_PARAM_int(OSSL_PKEY_PARAM_MAX_SIZE, NULL), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, NULL, 0), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0), + OSSL_PARAM_END + }; + (void)provCtx; + return wp_mldsa_supported_gettable_params; +} + +/** + * Get ML-DSA key parameters. + * + * @param [in] mldsa ML-DSA key object. + * @param [in, out] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + OSSL_PARAM* p; + + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, (int)mldsa->data->pubKeySize * 8)) { + ok = 0; + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_BITS); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, mldsa->data->securityBits)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_MAX_SIZE); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, (int)mldsa->data->sigSize)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PUB_KEY); + if (p != NULL) { + word32 outLen = mldsa->data->pubKeySize; + if (p->data == NULL) { + p->return_size = outLen; + } + else if (mldsa->hasPub) { + rc = wc_dilithium_export_public(&mldsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PRIV_KEY); + if (p != NULL) { + word32 outLen = mldsa->data->privKeySize; + if (p->data == NULL) { + p->return_size = outLen; + } + else if (mldsa->hasPriv) { + rc = wc_dilithium_export_private(&mldsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + } + return ok; +} + +/** + * Settable parameters for ML-DSA key. + * + * @param [in] provCtx Provider context. Unused. + * @return Empty parameter list. + */ +static const OSSL_PARAM* wp_mldsa_settable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mldsa_supported_settable_params[] = { + OSSL_PARAM_END + }; + (void)provCtx; + return wp_mldsa_supported_settable_params; +} + +/** + * Set ML-DSA key parameters. None supported. + * + * @param [in] mldsa ML-DSA key object. Unused. + * @param [in] params Array of parameters. Unused. + * @return 1 always. + */ +static int wp_mldsa_set_params(wp_MlDsa* mldsa, const OSSL_PARAM params[]) +{ + (void)mldsa; + (void)params; + return 1; +} + +/* + * ML-DSA generation + */ + +/** + * Create ML-DSA generation context object. + * + * @param [in] provCtx Provider context. + * @param [in] selection Parts of the key to generate. + * @param [in] params Parameters to set for generation. + * @param [in] data Parameter set data. + * @return New ML-DSA generation context on success, NULL on failure. + */ +static wp_MlDsaGenCtx* wp_mldsa_gen_init_base(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[], const wp_MlDsaData* data) +{ + wp_MlDsaGenCtx* ctx = NULL; + + (void)params; + + if (wolfssl_prov_is_running() && + ((selection & WP_MLDSA_POSSIBLE_SELECTIONS) != 0)) { + ctx = (wp_MlDsaGenCtx*)OPENSSL_zalloc(sizeof(*ctx)); + } + if (ctx != NULL) { + int rc; + int ok = 1; + + rc = wc_InitRng(&ctx->rng); + if (rc != 0) { + ok = 0; + } + if (ok) { + ctx->provCtx = provCtx; + ctx->data = data; + ctx->selection = selection; + } + if (!ok) { + OPENSSL_free(ctx); + ctx = NULL; + } + } + return ctx; +} + +/** + * Generate ML-DSA key pair. + * + * @param [in, out] ctx ML-DSA generation context. + * @param [in] cb Progress callback. Unused. + * @param [in] cbArg Argument for callback. Unused. + * @return ML-DSA key object on success, NULL on failure. + */ +static wp_MlDsa* wp_mldsa_gen(wp_MlDsaGenCtx* ctx, OSSL_CALLBACK* osslcb, + void* cbarg) +{ + wp_MlDsa* mldsa; + int keyPair = (ctx->selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0; + + (void)osslcb; + (void)cbarg; + + mldsa = wp_mldsa_new(ctx->provCtx, ctx->data); + if ((mldsa != NULL) && keyPair) { + int rc = wc_dilithium_make_key(&mldsa->key, &ctx->rng); + if (rc != 0) { + wp_mldsa_free(mldsa); + mldsa = NULL; + } + else { + mldsa->hasPub = 1; + mldsa->hasPriv = 1; + } + } + return mldsa; +} + +/** + * Set parameters into ML-DSA generation context. None supported. + * + * @param [in] ctx Generation context. Unused. + * @param [in] params Array of parameters. Unused. + * @return 1 always. + */ +static int wp_mldsa_gen_set_params(wp_MlDsaGenCtx* ctx, + const OSSL_PARAM params[]) +{ + (void)ctx; + (void)params; + return 1; +} + +/** + * Settable parameters for ML-DSA generation context. + * + * @param [in] ctx Generation context. Unused. + * @param [in] provCtx Provider context. Unused. + * @return Empty parameter list. + */ +static const OSSL_PARAM* wp_mldsa_gen_settable_params(wp_MlDsaGenCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static OSSL_PARAM wp_mldsa_gen_settable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mldsa_gen_settable; +} + +/** + * Free ML-DSA generation context. + * + * @param [in, out] ctx Generation context. + */ +static void wp_mldsa_gen_cleanup(wp_MlDsaGenCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + OPENSSL_free(ctx); + } +} + +/** + * Return the algorithm name for OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME. + * + * @param [in] op Operation type. Unused. + * @return NULL (default). + */ +static const char* wp_mldsa_query_operation_name(int op) +{ + (void)op; + return NULL; +} + +/* Per-level new() and gen_init() trampolines. */ + +static wp_MlDsa* wp_mldsa44_new(WOLFPROV_CTX* provCtx) +{ + return wp_mldsa_new(provCtx, &mldsa44Data); +} + +static wp_MlDsa* wp_mldsa65_new(WOLFPROV_CTX* provCtx) +{ + return wp_mldsa_new(provCtx, &mldsa65Data); +} + +static wp_MlDsa* wp_mldsa87_new(WOLFPROV_CTX* provCtx) +{ + return wp_mldsa_new(provCtx, &mldsa87Data); +} + +static wp_MlDsaGenCtx* wp_mldsa44_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mldsa_gen_init_base(provCtx, selection, params, &mldsa44Data); +} + +static wp_MlDsaGenCtx* wp_mldsa65_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mldsa_gen_init_base(provCtx, selection, params, &mldsa65Data); +} + +static wp_MlDsaGenCtx* wp_mldsa87_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mldsa_gen_init_base(provCtx, selection, params, &mldsa87Data); +} + +/* + * Dispatch tables + */ + +#define IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(alg) \ +const OSSL_DISPATCH wp_##alg##_keymgmt_functions[] = { \ + { OSSL_FUNC_KEYMGMT_NEW, \ + (DFUNC)wp_##alg##_new }, \ + { OSSL_FUNC_KEYMGMT_FREE, (DFUNC)wp_mldsa_free }, \ + { OSSL_FUNC_KEYMGMT_DUP, (DFUNC)wp_mldsa_dup }, \ + { OSSL_FUNC_KEYMGMT_GEN_INIT, \ + (DFUNC)wp_##alg##_gen_init }, \ + { OSSL_FUNC_KEYMGMT_GEN_SET_PARAMS, \ + (DFUNC)wp_mldsa_gen_set_params }, \ + { OSSL_FUNC_KEYMGMT_GEN_SETTABLE_PARAMS, \ + (DFUNC)wp_mldsa_gen_settable_params }, \ + { OSSL_FUNC_KEYMGMT_GEN, (DFUNC)wp_mldsa_gen }, \ + { OSSL_FUNC_KEYMGMT_GEN_CLEANUP, \ + (DFUNC)wp_mldsa_gen_cleanup }, \ + { OSSL_FUNC_KEYMGMT_LOAD, (DFUNC)wp_mldsa_load }, \ + { OSSL_FUNC_KEYMGMT_GET_PARAMS, \ + (DFUNC)wp_mldsa_get_params }, \ + { OSSL_FUNC_KEYMGMT_GETTABLE_PARAMS, \ + (DFUNC)wp_mldsa_gettable_params }, \ + { OSSL_FUNC_KEYMGMT_SET_PARAMS, \ + (DFUNC)wp_mldsa_set_params }, \ + { OSSL_FUNC_KEYMGMT_SETTABLE_PARAMS, \ + (DFUNC)wp_mldsa_settable_params }, \ + { OSSL_FUNC_KEYMGMT_HAS, (DFUNC)wp_mldsa_has }, \ + { OSSL_FUNC_KEYMGMT_MATCH, (DFUNC)wp_mldsa_match }, \ + { OSSL_FUNC_KEYMGMT_IMPORT, (DFUNC)wp_mldsa_import }, \ + { OSSL_FUNC_KEYMGMT_IMPORT_TYPES, \ + (DFUNC)wp_mldsa_import_types }, \ + { OSSL_FUNC_KEYMGMT_EXPORT, (DFUNC)wp_mldsa_export }, \ + { OSSL_FUNC_KEYMGMT_EXPORT_TYPES, \ + (DFUNC)wp_mldsa_export_types }, \ + { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ + (DFUNC)wp_mldsa_query_operation_name }, \ + { 0, NULL } \ +}; + +IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(mldsa44) +IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(mldsa65) +IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(mldsa87) + +#endif /* WP_HAVE_MLDSA */ diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c new file mode 100644 index 00000000..1cbe51db --- /dev/null +++ b/src/wp_mldsa_sig.c @@ -0,0 +1,463 @@ +/* wp_mldsa_sig.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WP_HAVE_MLDSA + +#include + +/** + * ML-DSA signature context. + * + * ML-DSA is a pure signature (no streamed digest); digest_sign_* accumulates + * the message in mdBuf and the one-shot signer is called in _final. + */ +typedef struct wp_MlDsaSigCtx { + /** Provider context. */ + WOLFPROV_CTX* provCtx; + /** wolfProvider ML-DSA key (owned reference). */ + wp_MlDsa* mldsa; + /** RNG for signing. */ + WC_RNG rng; + /** Buffer accumulating message bytes from digest_sign_update. */ + unsigned char* mdBuf; + /** Length of accumulated message in bytes. */ + size_t mdLen; + /** Capacity of mdBuf in bytes. */ + size_t mdCap; +} wp_MlDsaSigCtx; + + +/** + * Append data into the streaming message buffer. + * + * @param [in, out] ctx Signature context. + * @param [in] data Data to append. + * @param [in] dataLen Length of data in bytes. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, + size_t dataLen) +{ + int ok = 1; + size_t needed; + unsigned char* tmp; + + needed = ctx->mdLen + dataLen; + if (needed < ctx->mdLen) { + ok = 0; + } + if (ok && (needed > ctx->mdCap)) { + size_t newCap = ctx->mdCap == 0 ? 256 : ctx->mdCap; + while (newCap < needed) { + size_t doubled = newCap * 2; + if (doubled < newCap) { + ok = 0; + break; + } + newCap = doubled; + } + if (ok) { + tmp = (unsigned char*)OPENSSL_realloc(ctx->mdBuf, newCap); + if (tmp == NULL) { + ok = 0; + } + else { + ctx->mdBuf = tmp; + ctx->mdCap = newCap; + } + } + } + if (ok && (dataLen > 0)) { + XMEMCPY(ctx->mdBuf + ctx->mdLen, data, dataLen); + ctx->mdLen += dataLen; + } + return ok; +} + +/** + * Reset the streaming message buffer length to zero (keeps capacity). + * + * @param [in, out] ctx Signature context. + */ +static void wp_mldsa_buf_reset(wp_MlDsaSigCtx* ctx) +{ + ctx->mdLen = 0; +} + +/** + * Create a new ML-DSA signature context object. + * + * @param [in] provCtx Provider context. + * @param [in] propq Property query string. Unused. + * @return New signature context on success, NULL on failure. + */ +static wp_MlDsaSigCtx* wp_mldsa_newctx(WOLFPROV_CTX* provCtx, const char* propq) +{ + wp_MlDsaSigCtx* ctx = NULL; + + (void)propq; + + if (wolfssl_prov_is_running()) { + ctx = (wp_MlDsaSigCtx*)OPENSSL_zalloc(sizeof(*ctx)); + } + if (ctx != NULL) { + int rc = wc_InitRng(&ctx->rng); + if (rc != 0) { + OPENSSL_free(ctx); + ctx = NULL; + } + } + if (ctx != NULL) { + ctx->provCtx = provCtx; + } + return ctx; +} + +/** + * Free an ML-DSA signature context. + * + * @param [in, out] ctx Signature context. May be NULL. + */ +static void wp_mldsa_freectx(wp_MlDsaSigCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + wp_mldsa_free(ctx->mldsa); + OPENSSL_clear_free(ctx->mdBuf, ctx->mdCap); + OPENSSL_free(ctx); + } +} + +/** + * Duplicate an ML-DSA signature context (key reference incremented). + * + * @param [in] srcCtx Source signature context. + * @return New context on success, NULL on failure. + */ +static wp_MlDsaSigCtx* wp_mldsa_dupctx(wp_MlDsaSigCtx* srcCtx) +{ + wp_MlDsaSigCtx* dstCtx = NULL; + + if (!wolfssl_prov_is_running()) { + return NULL; + } + + dstCtx = wp_mldsa_newctx(srcCtx->provCtx, NULL); + if (dstCtx == NULL) { + return NULL; + } + if (srcCtx->mldsa != NULL) { + if (!wp_mldsa_up_ref(srcCtx->mldsa)) { + wp_mldsa_freectx(dstCtx); + return NULL; + } + dstCtx->mldsa = srcCtx->mldsa; + } + return dstCtx; +} + +/** + * Common init: take a reference on the key, reset state. + * + * @param [in, out] ctx Signature context. + * @param [in] mldsa ML-DSA key (reference taken). + * @param [in] params Parameters. Unused. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, + const OSSL_PARAM params[]) +{ + int ok = 1; + + (void)params; + + if ((ctx == NULL) || (mldsa == NULL)) { + ok = 0; + } + if (ok && !wp_mldsa_up_ref(mldsa)) { + ok = 0; + } + if (ok) { + wp_mldsa_free(ctx->mldsa); + ctx->mldsa = mldsa; + wp_mldsa_buf_reset(ctx); + } + return ok; +} + +static int wp_mldsa_sign_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, + const OSSL_PARAM params[]) +{ + return wp_mldsa_init(ctx, mldsa, params); +} + +static int wp_mldsa_verify_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, + const OSSL_PARAM params[]) +{ + return wp_mldsa_init(ctx, mldsa, params); +} + +/** + * One-shot sign of a message. + * + * If sig is NULL, just report the signature size in sigLen. + * + * @param [in] ctx Signature context. + * @param [out] sig Signature buffer. + * @param [in, out] sigLen On in, buffer size; on out, signature length. + * @param [in] sigSize Allocated size of sig (unused). + * @param [in] msg Message to sign. + * @param [in] msgLen Message length. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, + size_t* sigLen, size_t sigSize, const unsigned char* msg, size_t msgLen) +{ + int ok = 1; + int rc; + word32 sigSz; + + (void)sigSize; + + if ((ctx == NULL) || (ctx->mldsa == NULL) || (sigLen == NULL)) { + return 0; + } + + sigSz = (word32)wp_mldsa_get_sig_size(ctx->mldsa); + + if (sig == NULL) { + *sigLen = sigSz; + return 1; + } + if (*sigLen < sigSz) { + ok = 0; + } + if (ok) { + word32 outLen = sigSz; + rc = wc_dilithium_sign_msg(msg, (word32)msgLen, sig, &outLen, + (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), &ctx->rng); + if (rc != 0) { + ok = 0; + } + if (ok) { + *sigLen = outLen; + } + } + return ok; +} + +/** + * One-shot verify of a signature on a message. + * + * @param [in] ctx Signature context. + * @param [in] sig Signature. + * @param [in] sigLen Signature length. + * @param [in] msg Message. + * @param [in] msgLen Message length. + * @return 1 if signature valid, 0 otherwise. + */ +static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, + size_t sigLen, const unsigned char* msg, size_t msgLen) +{ + int ok = 1; + int rc; + int res = 0; + + if ((ctx == NULL) || (ctx->mldsa == NULL)) { + return 0; + } + + rc = wc_dilithium_verify_msg(sig, (word32)sigLen, msg, (word32)msgLen, + &res, (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa)); + if ((rc != 0) || (res != 1)) { + ok = 0; + } + return ok; +} + +/** + * Digest-sign init: ML-DSA is pure (no pre-hash), so the buffer captures the + * message and the one-shot signer is invoked at _final time. + * + * @param [in, out] ctx Signature context. + * @param [in] mdName Message digest name (must be NULL or empty). + * @param [in] mldsa ML-DSA key (reference taken). + * @param [in] params Parameters. Unused. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_digest_sign_init(wp_MlDsaSigCtx* ctx, const char* mdName, + wp_MlDsa* mldsa, const OSSL_PARAM params[]) +{ + if ((mdName != NULL) && (mdName[0] != '\0')) { + return 0; + } + return wp_mldsa_init(ctx, mldsa, params); +} + +static int wp_mldsa_digest_verify_init(wp_MlDsaSigCtx* ctx, const char* mdName, + wp_MlDsa* mldsa, const OSSL_PARAM params[]) +{ + if ((mdName != NULL) && (mdName[0] != '\0')) { + return 0; + } + return wp_mldsa_init(ctx, mldsa, params); +} + +/** + * Append data to the accumulated message buffer. + * + * @param [in, out] ctx Signature context. + * @param [in] data Data to append. + * @param [in] dataLen Length of data. + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_digest_signverify_update(wp_MlDsaSigCtx* ctx, + const unsigned char* data, size_t dataLen) +{ + if ((ctx == NULL) || (ctx->mldsa == NULL)) { + return 0; + } + return wp_mldsa_buf_append(ctx, data, dataLen); +} + +/** + * Finalize a digest-style sign: produce signature over the buffered message. + * + * If sig is NULL, just report the signature size. + * + * @param [in] ctx Signature context. + * @param [out] sig Signature buffer. + * @param [in, out] sigLen On in, buffer size; on out, signature length. + * @param [in] sigSize Allocated size of sig (unused). + * @return 1 on success, 0 on failure. + */ +static int wp_mldsa_digest_sign_final(wp_MlDsaSigCtx* ctx, unsigned char* sig, + size_t* sigLen, size_t sigSize) +{ + return wp_mldsa_sign(ctx, sig, sigLen, sigSize, ctx->mdBuf, ctx->mdLen); +} + +/** + * Finalize a digest-style verify on the buffered message. + * + * @param [in] ctx Signature context. + * @param [in] sig Signature. + * @param [in] sigLen Signature length. + * @return 1 if valid, 0 otherwise. + */ +static int wp_mldsa_digest_verify_final(wp_MlDsaSigCtx* ctx, + const unsigned char* sig, size_t sigLen) +{ + return wp_mldsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); +} + +/** + * Get ctx params. None supported. + */ +static int wp_mldsa_get_ctx_params(wp_MlDsaSigCtx* ctx, OSSL_PARAM* params) +{ + (void)ctx; + (void)params; + return 1; +} + +static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mldsa_gettable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mldsa_gettable; +} + +/** + * Set ctx params. None supported. + */ +static int wp_mldsa_set_ctx_params(wp_MlDsaSigCtx* ctx, + const OSSL_PARAM params[]) +{ + (void)ctx; + (void)params; + return 1; +} + +static const OSSL_PARAM* wp_mldsa_settable_ctx_params(wp_MlDsaSigCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mldsa_settable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mldsa_settable; +} + +/** Dispatch table for ML-DSA signatures (shared across all three levels). */ +const OSSL_DISPATCH wp_mldsa_signature_functions[] = { + { OSSL_FUNC_SIGNATURE_NEWCTX, + (DFUNC)wp_mldsa_newctx }, + { OSSL_FUNC_SIGNATURE_FREECTX, + (DFUNC)wp_mldsa_freectx }, + { OSSL_FUNC_SIGNATURE_DUPCTX, + (DFUNC)wp_mldsa_dupctx }, + { OSSL_FUNC_SIGNATURE_SIGN_INIT, + (DFUNC)wp_mldsa_sign_init }, + { OSSL_FUNC_SIGNATURE_SIGN, + (DFUNC)wp_mldsa_sign }, + { OSSL_FUNC_SIGNATURE_VERIFY_INIT, + (DFUNC)wp_mldsa_verify_init }, + { OSSL_FUNC_SIGNATURE_VERIFY, + (DFUNC)wp_mldsa_verify }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_INIT, + (DFUNC)wp_mldsa_digest_sign_init }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_UPDATE, + (DFUNC)wp_mldsa_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_SIGN_FINAL, + (DFUNC)wp_mldsa_digest_sign_final }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_INIT, + (DFUNC)wp_mldsa_digest_verify_init }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_UPDATE, + (DFUNC)wp_mldsa_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_FINAL, + (DFUNC)wp_mldsa_digest_verify_final }, + { OSSL_FUNC_SIGNATURE_GET_CTX_PARAMS, + (DFUNC)wp_mldsa_get_ctx_params }, + { OSSL_FUNC_SIGNATURE_GETTABLE_CTX_PARAMS, + (DFUNC)wp_mldsa_gettable_ctx_params }, + { OSSL_FUNC_SIGNATURE_SET_CTX_PARAMS, + (DFUNC)wp_mldsa_set_ctx_params }, + { OSSL_FUNC_SIGNATURE_SETTABLE_CTX_PARAMS, + (DFUNC)wp_mldsa_settable_ctx_params }, + { 0, NULL } +}; + +#endif /* WP_HAVE_MLDSA */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index b901c4e8..60a57f45 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -671,6 +671,14 @@ static const OSSL_ALGORITHM wolfprov_keymgmt[] = { { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, wp_mlkem1024_keymgmt_functions, "ML-KEM-1024" }, #endif +#ifdef WP_HAVE_MLDSA + { WP_NAMES_ML_DSA_44, WOLFPROV_PROPERTIES, + wp_mldsa44_keymgmt_functions, "ML-DSA-44" }, + { WP_NAMES_ML_DSA_65, WOLFPROV_PROPERTIES, + wp_mldsa65_keymgmt_functions, "ML-DSA-65" }, + { WP_NAMES_ML_DSA_87, WOLFPROV_PROPERTIES, + wp_mldsa87_keymgmt_functions, "ML-DSA-87" }, +#endif { NULL, NULL, NULL, NULL } }; @@ -729,6 +737,14 @@ static const OSSL_ALGORITHM wolfprov_signature[] = { { WP_NAMES_CMAC, WOLFPROV_PROPERTIES, wp_cmac_signature_functions, "" }, #endif +#ifdef WP_HAVE_MLDSA + { WP_NAMES_ML_DSA_44, WOLFPROV_PROPERTIES, + wp_mldsa_signature_functions, "" }, + { WP_NAMES_ML_DSA_65, WOLFPROV_PROPERTIES, + wp_mldsa_signature_functions, "" }, + { WP_NAMES_ML_DSA_87, WOLFPROV_PROPERTIES, + wp_mldsa_signature_functions, "" }, +#endif { NULL, NULL, NULL, NULL } }; From 3df1a6f9f1d6025324f2d8ec000cd06e35e1f92c Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:33:50 -0700 Subject: [PATCH 04/16] Add ML-KEM and ML-DSA unit tests + dupctx buffer copy fix --- src/wp_mldsa_kmgmt.c | 32 ++-- src/wp_mldsa_sig.c | 6 + test/include.am | 2 + test/test_mldsa.c | 370 ++++++++++++++++++++++++++++++++++++++++++ test/test_mlkem.c | 379 +++++++++++++++++++++++++++++++++++++++++++ test/unit.c | 15 ++ test/unit.h | 15 ++ 7 files changed, 806 insertions(+), 13 deletions(-) create mode 100644 test/test_mldsa.c create mode 100644 test/test_mlkem.c diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 578c0527..3498d816 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -874,18 +874,6 @@ static void wp_mldsa_gen_cleanup(wp_MlDsaGenCtx* ctx) } } -/** - * Return the algorithm name for OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME. - * - * @param [in] op Operation type. Unused. - * @return NULL (default). - */ -static const char* wp_mldsa_query_operation_name(int op) -{ - (void)op; - return NULL; -} - /* Per-level new() and gen_init() trampolines. */ static wp_MlDsa* wp_mldsa44_new(WOLFPROV_CTX* provCtx) @@ -903,6 +891,24 @@ static wp_MlDsa* wp_mldsa87_new(WOLFPROV_CTX* provCtx) return wp_mldsa_new(provCtx, &mldsa87Data); } +static const char* wp_mldsa44_query_operation_name(int op) +{ + (void)op; + return "ML-DSA-44"; +} + +static const char* wp_mldsa65_query_operation_name(int op) +{ + (void)op; + return "ML-DSA-65"; +} + +static const char* wp_mldsa87_query_operation_name(int op) +{ + (void)op; + return "ML-DSA-87"; +} + static wp_MlDsaGenCtx* wp_mldsa44_gen_init(WOLFPROV_CTX* provCtx, int selection, const OSSL_PARAM params[]) { @@ -958,7 +964,7 @@ const OSSL_DISPATCH wp_##alg##_keymgmt_functions[] = { \ { OSSL_FUNC_KEYMGMT_EXPORT_TYPES, \ (DFUNC)wp_mldsa_export_types }, \ { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ - (DFUNC)wp_mldsa_query_operation_name }, \ + (DFUNC)wp_##alg##_query_operation_name }, \ { 0, NULL } \ }; diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 1cbe51db..c4da9b25 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -180,6 +180,12 @@ static wp_MlDsaSigCtx* wp_mldsa_dupctx(wp_MlDsaSigCtx* srcCtx) } dstCtx->mldsa = srcCtx->mldsa; } + if (srcCtx->mdLen > 0) { + if (!wp_mldsa_buf_append(dstCtx, srcCtx->mdBuf, srcCtx->mdLen)) { + wp_mldsa_freectx(dstCtx); + return NULL; + } + } return dstCtx; } diff --git a/test/include.am b/test/include.am index d32e2f94..e404ad56 100644 --- a/test/include.am +++ b/test/include.am @@ -31,6 +31,8 @@ test_unit_test_SOURCES = \ test/test_pbe.c \ test/test_pkey.c \ test/test_pkcs7_x509.c \ + test/test_mlkem.c \ + test/test_mldsa.c \ test/test_rand.c \ test/test_rsa.c \ test/test_seccomp_sandbox.c \ diff --git a/test/test_mldsa.c b/test/test_mldsa.c new file mode 100644 index 00000000..70fcdb77 --- /dev/null +++ b/test/test_mldsa.c @@ -0,0 +1,370 @@ +/* test_mldsa.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include "unit.h" + +#include + +#ifdef WP_HAVE_MLDSA + +#include + +/* Per-level metadata. */ +typedef struct mldsa_test_level { + const char* name; + size_t pubKeySize; + size_t sigSize; +} mldsa_test_level; + +static const mldsa_test_level mldsa_levels[] = { + { "ML-DSA-44", ML_DSA_LEVEL2_PUB_KEY_SIZE, ML_DSA_LEVEL2_SIG_SIZE }, + { "ML-DSA-65", ML_DSA_LEVEL3_PUB_KEY_SIZE, ML_DSA_LEVEL3_SIG_SIZE }, + { "ML-DSA-87", ML_DSA_LEVEL5_PUB_KEY_SIZE, ML_DSA_LEVEL5_SIG_SIZE }, +}; +#define MLDSA_LEVEL_COUNT (sizeof(mldsa_levels) / sizeof(mldsa_levels[0])) + + +static const unsigned char mldsa_test_msg[] = + "wolfProvider ML-DSA test message bytes for FIPS 204 sign/verify"; +#define MLDSA_TEST_MSG_LEN (sizeof(mldsa_test_msg) - 1) + + +/** + * Generate an ML-DSA key pair via wolfProvider. + * + * @param [in] name Algorithm name (e.g. "ML-DSA-44"). + * @param [out] pkey Generated EVP_PKEY (caller frees). + * @return 0 on success, non-zero on failure. + */ +static int mldsa_keygen(const char* name, EVP_PKEY** pkey) +{ + int err = 0; + EVP_PKEY_CTX* ctx = NULL; + + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, name, NULL); + err = (ctx == NULL); + if (err == 0) { + err = EVP_PKEY_keygen_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_keygen(ctx, pkey) != 1; + } + EVP_PKEY_CTX_free(ctx); + return err; +} + +/** + * Extract the raw public key bytes from an ML-DSA EVP_PKEY. + */ +static int mldsa_get_pub(EVP_PKEY* pkey, unsigned char** out, size_t* len) +{ + int err = 0; + size_t need = 0; + + err = EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, + NULL, 0, &need) != 1; + if (err == 0) { + *out = (unsigned char*)OPENSSL_malloc(need); + err = (*out == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, + *out, need, len) != 1; + } + if (err && (*out != NULL)) { + OPENSSL_free(*out); + *out = NULL; + } + return err; +} + +/** + * Sign a message with the given ML-DSA EVP_PKEY using the digest-sign API + * (which for ML-DSA passes the whole message to the one-shot signer). + */ +static int mldsa_sign_msg(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, unsigned char** sigOut, size_t* sigLenOut) +{ + int err = 0; + EVP_MD_CTX* mdctx = NULL; + size_t sigLen = 0; + unsigned char* sig = NULL; + + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, pkey, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, &sigLen, msg, msgLen) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, sig, &sigLen, msg, msgLen) != 1; + } + if (err == 0) { + *sigOut = sig; + *sigLenOut = sigLen; + } + else { + OPENSSL_free(sig); + } + EVP_MD_CTX_free(mdctx); + return err; +} + +/** + * Verify a signature on a message with the given ML-DSA EVP_PKEY. + * + * @return 1 if verified, 0 if not (does not set err on bad sig). + */ +static int mldsa_verify_msg(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, const unsigned char* sig, size_t sigLen) +{ + int ok = 0; + int rc; + EVP_MD_CTX* mdctx = NULL; + + mdctx = EVP_MD_CTX_new(); + if (mdctx == NULL) { + return 0; + } + rc = EVP_DigestVerifyInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, pkey, NULL); + if (rc == 1) { + rc = EVP_DigestVerify(mdctx, sig, sigLen, msg, msgLen); + if (rc == 1) { + ok = 1; + } + } + EVP_MD_CTX_free(mdctx); + return ok; +} + +/** + * Test ML-DSA key generation; verify pub-key size and that two keys differ. + */ +int test_mldsa_keygen(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + unsigned char* p1 = NULL; + unsigned char* p2 = NULL; + size_t p1Len = 0; + size_t p2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Keygen %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k1); + if (err == 0) { + err = mldsa_keygen(lvl->name, &k2); + } + if (err == 0) { + err = mldsa_get_pub(k1, &p1, &p1Len); + } + if (err == 0) { + err = mldsa_get_pub(k2, &p2, &p2Len); + } + if (err == 0) { + err = (p1Len != lvl->pubKeySize); + if (err) { + PRINT_ERR_MSG("Unexpected pub key size %zu vs %zu", + p1Len, lvl->pubKeySize); + } + } + if (err == 0) { + err = (memcmp(p1, p2, p1Len) == 0); + } + + OPENSSL_free(p1); p1 = NULL; + OPENSSL_free(p2); p2 = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/** + * Test ML-DSA sign / verify round-trip via the digest-sign EVP API. + */ +int test_mldsa_sign_verify(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* pkey = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Sign/verify %s", lvl->name); + + err = mldsa_keygen(lvl->name, &pkey); + if (err == 0) { + err = mldsa_sign_msg(pkey, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + err = (sigLen > lvl->sigSize); + if (err) { + PRINT_ERR_MSG("Sig len %zu exceeds expected max %zu", + sigLen, lvl->sigSize); + } + } + if (err == 0) { + err = mldsa_verify_msg(pkey, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + sig, sigLen) != 1; + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(pkey); pkey = NULL; + } + return err; +} + +/** + * Test ML-DSA verify with a single-bit-flipped signature: must fail. + */ +int test_mldsa_verify_tampered_sig(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* pkey = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Tampered sig %s", lvl->name); + + err = mldsa_keygen(lvl->name, &pkey); + if (err == 0) { + err = mldsa_sign_msg(pkey, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + sig[0] ^= 0x01; + err = mldsa_verify_msg(pkey, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + sig, sigLen) == 1; + if (err) { + PRINT_ERR_MSG("Tampered signature verified"); + } + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(pkey); pkey = NULL; + } + return err; +} + +/** + * Test ML-DSA verify with a single-bit-flipped message: must fail. + */ +int test_mldsa_verify_tampered_msg(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* pkey = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + unsigned char tampered[MLDSA_TEST_MSG_LEN]; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Tampered msg %s", lvl->name); + + err = mldsa_keygen(lvl->name, &pkey); + if (err == 0) { + err = mldsa_sign_msg(pkey, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + memcpy(tampered, mldsa_test_msg, MLDSA_TEST_MSG_LEN); + tampered[0] ^= 0x01; + err = mldsa_verify_msg(pkey, tampered, MLDSA_TEST_MSG_LEN, + sig, sigLen) == 1; + if (err) { + PRINT_ERR_MSG("Tampered message verified"); + } + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(pkey); pkey = NULL; + } + return err; +} + +/** + * Test ML-DSA verify with a different key: must fail. + */ +int test_mldsa_verify_wrong_key(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* keyA = NULL; + EVP_PKEY* keyB = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Wrong key %s", lvl->name); + + err = mldsa_keygen(lvl->name, &keyA); + if (err == 0) { + err = mldsa_keygen(lvl->name, &keyB); + } + if (err == 0) { + err = mldsa_sign_msg(keyA, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + &sig, &sigLen); + } + if (err == 0) { + err = mldsa_verify_msg(keyB, mldsa_test_msg, MLDSA_TEST_MSG_LEN, + sig, sigLen) == 1; + if (err) { + PRINT_ERR_MSG("Wrong key verified"); + } + } + + OPENSSL_free(sig); sig = NULL; + EVP_PKEY_free(keyA); keyA = NULL; + EVP_PKEY_free(keyB); keyB = NULL; + } + return err; +} + +#endif /* WP_HAVE_MLDSA */ diff --git a/test/test_mlkem.c b/test/test_mlkem.c new file mode 100644 index 00000000..50c855ad --- /dev/null +++ b/test/test_mlkem.c @@ -0,0 +1,379 @@ +/* test_mlkem.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include "unit.h" + +#include +#include + +#ifdef WP_HAVE_MLKEM + +#include + +/* Per-level metadata. */ +typedef struct mlkem_test_level { + const char* name; + size_t pubKeySize; + size_t privKeySize; + size_t ctSize; +} mlkem_test_level; + +static const mlkem_test_level mlkem_levels[] = { + { "ML-KEM-512", WC_ML_KEM_512_PUBLIC_KEY_SIZE, + WC_ML_KEM_512_PRIVATE_KEY_SIZE, WC_ML_KEM_512_CIPHER_TEXT_SIZE }, + { "ML-KEM-768", WC_ML_KEM_768_PUBLIC_KEY_SIZE, + WC_ML_KEM_768_PRIVATE_KEY_SIZE, WC_ML_KEM_768_CIPHER_TEXT_SIZE }, + { "ML-KEM-1024", WC_ML_KEM_1024_PUBLIC_KEY_SIZE, + WC_ML_KEM_1024_PRIVATE_KEY_SIZE, WC_ML_KEM_1024_CIPHER_TEXT_SIZE }, +}; +#define MLKEM_LEVEL_COUNT (sizeof(mlkem_levels) / sizeof(mlkem_levels[0])) + + +/** + * Generate an ML-KEM key pair via wolfProvider. + * + * @param [in] name Algorithm name (e.g. "ML-KEM-512"). + * @param [out] pkey Generated EVP_PKEY (caller frees). + * @return 0 on success, non-zero on failure. + */ +static int mlkem_keygen(const char* name, EVP_PKEY** pkey) +{ + int err = 0; + EVP_PKEY_CTX* ctx = NULL; + + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, name, NULL); + err = (ctx == NULL); + if (err == 0) { + err = EVP_PKEY_keygen_init(ctx) != 1; + } + if (err == 0) { + err = EVP_PKEY_keygen(ctx, pkey) != 1; + } + EVP_PKEY_CTX_free(ctx); + return err; +} + +/** + * Extract the raw public key bytes from an ML-KEM EVP_PKEY. + * + * @param [in] pkey ML-KEM EVP_PKEY. + * @param [out] out Buffer for public key bytes (caller frees with OPENSSL_free). + * @param [out] len Length of returned key in bytes. + * @return 0 on success, non-zero on failure. + */ +static int mlkem_get_pub(EVP_PKEY* pkey, unsigned char** out, size_t* len) +{ + int err = 0; + size_t need = 0; + + err = EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, + NULL, 0, &need) != 1; + if (err == 0) { + *out = (unsigned char*)OPENSSL_malloc(need); + err = (*out == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, + *out, need, len) != 1; + } + if (err && (*out != NULL)) { + OPENSSL_free(*out); + *out = NULL; + } + return err; +} + +/** + * Test ML-KEM key generation and that public key size matches expected. + */ +int test_mlkem_keygen(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* pkey1 = NULL; + EVP_PKEY* pkey2 = NULL; + unsigned char* pub1 = NULL; + unsigned char* pub2 = NULL; + size_t pub1Len = 0; + size_t pub2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Keygen %s", lvl->name); + + err = mlkem_keygen(lvl->name, &pkey1); + if (err == 0) { + err = mlkem_keygen(lvl->name, &pkey2); + } + if (err == 0) { + err = mlkem_get_pub(pkey1, &pub1, &pub1Len); + } + if (err == 0) { + err = mlkem_get_pub(pkey2, &pub2, &pub2Len); + } + if (err == 0) { + err = (pub1Len != lvl->pubKeySize); + if (err) { + PRINT_ERR_MSG("Unexpected pub key size: %zu vs %zu", + pub1Len, lvl->pubKeySize); + } + } + if (err == 0) { + err = (memcmp(pub1, pub2, pub1Len) == 0); + if (err) { + PRINT_ERR_MSG("Two keygens produced identical public keys"); + } + } + + OPENSSL_free(pub1); pub1 = NULL; + OPENSSL_free(pub2); pub2 = NULL; + EVP_PKEY_free(pkey1); pkey1 = NULL; + EVP_PKEY_free(pkey2); pkey2 = NULL; + } + + return err; +} + +/** + * Test ML-KEM encapsulate / decapsulate round trip via EVP_PKEY API. + */ +int test_mlkem_encap_decap(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* pkey = NULL; + EVP_PKEY_CTX* ectx = NULL; + EVP_PKEY_CTX* dctx = NULL; + unsigned char* ct = NULL; + unsigned char* ss1 = NULL; + unsigned char* ss2 = NULL; + size_t ctLen = 0; + size_t ss1Len = 0; + size_t ss2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Encap/Decap %s", lvl->name); + + err = mlkem_keygen(lvl->name, &pkey); + + if (err == 0) { + ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, pkey, NULL); + err = (ectx == NULL); + } + if (err == 0) { + err = EVP_PKEY_encapsulate_init(ectx, NULL) != 1; + } + if (err == 0) { + err = EVP_PKEY_encapsulate(ectx, NULL, &ctLen, NULL, &ss1Len) != 1; + } + if (err == 0) { + err = (ctLen != lvl->ctSize) || (ss1Len != 32); + } + if (err == 0) { + ct = (unsigned char*)OPENSSL_malloc(ctLen); + ss1 = (unsigned char*)OPENSSL_malloc(ss1Len); + ss2 = (unsigned char*)OPENSSL_malloc(ss1Len); + err = (ct == NULL) || (ss1 == NULL) || (ss2 == NULL); + } + if (err == 0) { + err = EVP_PKEY_encapsulate(ectx, ct, &ctLen, ss1, &ss1Len) != 1; + } + + if (err == 0) { + dctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, pkey, NULL); + err = (dctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_decapsulate_init(dctx, NULL) != 1; + } + if (err == 0) { + ss2Len = ss1Len; + err = EVP_PKEY_decapsulate(dctx, ss2, &ss2Len, ct, ctLen) != 1; + } + if (err == 0) { + err = (ss1Len != ss2Len) || (memcmp(ss1, ss2, ss1Len) != 0); + if (err) { + PRINT_ERR_MSG("Shared secrets do not match"); + } + } + + OPENSSL_free(ct); ct = NULL; + OPENSSL_free(ss1); ss1 = NULL; + OPENSSL_free(ss2); ss2 = NULL; + EVP_PKEY_CTX_free(ectx); ectx = NULL; + EVP_PKEY_CTX_free(dctx); dctx = NULL; + EVP_PKEY_free(pkey); pkey = NULL; + } + + return err; +} + +/** + * Test ML-KEM decapsulate of a tampered ciphertext: must still succeed and + * yield a different shared secret (implicit rejection). + */ +int test_mlkem_decap_tampered_ct(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* pkey = NULL; + EVP_PKEY_CTX* ectx = NULL; + EVP_PKEY_CTX* dctx = NULL; + unsigned char* ct = NULL; + unsigned char ss1[32]; + unsigned char ss2[32]; + size_t ctLen = 0; + size_t ss1Len = sizeof(ss1); + size_t ss2Len = sizeof(ss2); + + (void)data; + + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Decap tampered ct %s", lvl->name); + + err = mlkem_keygen(lvl->name, &pkey); + if (err == 0) { + ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, pkey, NULL); + err = (ectx == NULL); + } + if (err == 0) { + err = EVP_PKEY_encapsulate_init(ectx, NULL) != 1; + } + if (err == 0) { + ctLen = lvl->ctSize; + ct = (unsigned char*)OPENSSL_malloc(ctLen); + err = (ct == NULL); + } + if (err == 0) { + ss1Len = sizeof(ss1); + err = EVP_PKEY_encapsulate(ectx, ct, &ctLen, ss1, &ss1Len) != 1; + } + if (err == 0) { + ct[0] ^= 0x01; + dctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, pkey, NULL); + err = (dctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_decapsulate_init(dctx, NULL) != 1; + } + if (err == 0) { + ss2Len = sizeof(ss2); + err = EVP_PKEY_decapsulate(dctx, ss2, &ss2Len, ct, ctLen) != 1; + if (err) { + PRINT_ERR_MSG("Decap of tampered ct should return implicit " + "secret, not fail"); + } + } + if (err == 0) { + err = (ss1Len == ss2Len) && + (memcmp(ss1, ss2, ss1Len) == 0); + if (err) { + PRINT_ERR_MSG("Tampered ct produced original shared secret"); + } + } + + OPENSSL_free(ct); ct = NULL; + EVP_PKEY_CTX_free(ectx); ectx = NULL; + EVP_PKEY_CTX_free(dctx); dctx = NULL; + EVP_PKEY_free(pkey); pkey = NULL; + } + + return err; +} + +/** + * Test ML-KEM decapsulate with a different key: produces a different secret. + */ +int test_mlkem_decap_wrong_key(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* keyA = NULL; + EVP_PKEY* keyB = NULL; + EVP_PKEY_CTX* ectx = NULL; + EVP_PKEY_CTX* dctx = NULL; + unsigned char* ct = NULL; + unsigned char ss1[32]; + unsigned char ss2[32]; + size_t ctLen = 0; + size_t ss1Len = sizeof(ss1); + size_t ss2Len = sizeof(ss2); + + (void)data; + + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Decap wrong key %s", lvl->name); + + err = mlkem_keygen(lvl->name, &keyA); + if (err == 0) { + err = mlkem_keygen(lvl->name, &keyB); + } + if (err == 0) { + ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, keyA, NULL); + err = (ectx == NULL); + } + if (err == 0) { + err = EVP_PKEY_encapsulate_init(ectx, NULL) != 1; + } + if (err == 0) { + ctLen = lvl->ctSize; + ct = (unsigned char*)OPENSSL_malloc(ctLen); + err = (ct == NULL); + } + if (err == 0) { + ss1Len = sizeof(ss1); + err = EVP_PKEY_encapsulate(ectx, ct, &ctLen, ss1, &ss1Len) != 1; + } + if (err == 0) { + dctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, keyB, NULL); + err = (dctx == NULL); + } + if (err == 0) { + err = EVP_PKEY_decapsulate_init(dctx, NULL) != 1; + } + if (err == 0) { + ss2Len = sizeof(ss2); + err = EVP_PKEY_decapsulate(dctx, ss2, &ss2Len, ct, ctLen) != 1; + } + if (err == 0) { + err = (memcmp(ss1, ss2, ss1Len) == 0); + if (err) { + PRINT_ERR_MSG("Wrong-key decap produced matching secret"); + } + } + + OPENSSL_free(ct); ct = NULL; + EVP_PKEY_CTX_free(ectx); ectx = NULL; + EVP_PKEY_CTX_free(dctx); dctx = NULL; + EVP_PKEY_free(keyA); keyA = NULL; + EVP_PKEY_free(keyB); keyB = NULL; + } + + return err; +} + +#endif /* WP_HAVE_MLKEM */ diff --git a/test/unit.c b/test/unit.c index 81b7064c..37a49b9f 100644 --- a/test/unit.c +++ b/test/unit.c @@ -478,6 +478,21 @@ TEST_CASE test_case[] = { TEST_DECL(test_des3_tls_cbc_bad_pad, NULL), #endif #endif + +#ifdef WP_HAVE_MLKEM + TEST_DECL(test_mlkem_keygen, NULL), + TEST_DECL(test_mlkem_encap_decap, NULL), + TEST_DECL(test_mlkem_decap_tampered_ct, NULL), + TEST_DECL(test_mlkem_decap_wrong_key, NULL), +#endif + +#ifdef WP_HAVE_MLDSA + TEST_DECL(test_mldsa_keygen, NULL), + TEST_DECL(test_mldsa_sign_verify, NULL), + TEST_DECL(test_mldsa_verify_tampered_sig, NULL), + TEST_DECL(test_mldsa_verify_tampered_msg, NULL), + TEST_DECL(test_mldsa_verify_wrong_key, NULL), +#endif }; #define TEST_CASE_CNT (int)(sizeof(test_case) / sizeof(*test_case)) diff --git a/test/unit.h b/test/unit.h index ef7bed6f..eda647ba 100644 --- a/test/unit.h +++ b/test/unit.h @@ -477,4 +477,19 @@ int test_des3_tls_cbc_bad_pad(void *data); #endif #endif +#ifdef WP_HAVE_MLKEM +int test_mlkem_keygen(void *data); +int test_mlkem_encap_decap(void *data); +int test_mlkem_decap_tampered_ct(void *data); +int test_mlkem_decap_wrong_key(void *data); +#endif + +#ifdef WP_HAVE_MLDSA +int test_mldsa_keygen(void *data); +int test_mldsa_sign_verify(void *data); +int test_mldsa_verify_tampered_sig(void *data); +int test_mldsa_verify_tampered_msg(void *data); +int test_mldsa_verify_wrong_key(void *data); +#endif + #endif /* UNIT_H */ From f78fdb880721a59cd81156e21760d222a591d448 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:34:31 -0700 Subject: [PATCH 05/16] Add PQC version-compat CI: pre-PQC, latest stable, master --- .github/workflows/wolfssl-versions-pqc.yml | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/wolfssl-versions-pqc.yml diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml new file mode 100644 index 00000000..17b8a76f --- /dev/null +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -0,0 +1,82 @@ +name: wolfSSL Versions (PQC) + +# Backward-compatibility matrix for ML-KEM and ML-DSA. +# +# Three rows: +# - pre-PQC wolfSSL (e.g. v5.7.0-stable): +# wolfSSL is built without --enable-mlkem/--enable-dilithium. wolfProvider +# auto-detects via settings.h that PQC macros are undefined; the PQC +# source files compile to no-ops; the ML-KEM/ML-DSA tests are skipped. +# Proves the no-symbol path still builds and runs cleanly. +# - latest stable wolfSSL with PQC enabled: +# --enable-pqc is passed to scripts/build-wolfprovider.sh, which adds +# --enable-mlkem --enable-dilithium --enable-experimental to wolfSSL. +# wolfProvider's settings.h picks up WP_HAVE_MLKEM and WP_HAVE_MLDSA; +# the PQC tests run and must pass. +# - master wolfSSL with PQC enabled: +# Same as above against the development tip. + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pqc_version_test: + name: ${{ matrix.name }} + runs-on: ubuntu-22.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: pre-PQC (v5.7.0-stable, PQC disabled) + wolfssl_ref: v5.7.0-stable + pqc: false + - name: latest stable (v5.8.4-stable, PQC enabled) + wolfssl_ref: v5.8.4-stable + pqc: true + - name: master (PQC enabled) + wolfssl_ref: master + pqc: true + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build and test wolfProvider (PQC=${{ matrix.pqc }}) + run: | + if [ "${{ matrix.pqc }}" = "true" ]; then + WOLFSSL_TAG=${{ matrix.wolfssl_ref }} \ + ./scripts/build-wolfprovider.sh --enable-pqc + else + WOLFSSL_TAG=${{ matrix.wolfssl_ref }} \ + ./scripts/build-wolfprovider.sh + fi + + - name: Confirm PQC tests present (or absent) as expected + run: | + if [ "${{ matrix.pqc }}" = "true" ]; then + ./test/unit.test --list | grep -q 'test_mlkem_keygen' \ + || { echo 'ERROR: PQC tests missing in PQC-enabled build'; exit 1; } + ./test/unit.test --list | grep -q 'test_mldsa_sign_verify' \ + || { echo 'ERROR: ML-DSA tests missing in PQC-enabled build'; exit 1; } + else + if ./test/unit.test --list | grep -qE 'test_mlkem|test_mldsa'; then + echo 'ERROR: PQC tests present in pre-PQC build (should be skipped)' + exit 1 + fi + fi + + - name: Print errors on failure + if: ${{ failure() }} + run: | + if [ -f test-suite.log ]; then + cat test-suite.log + fi From c1b7c109c055317fede6ee02e30b13fcac6099ea Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:48:30 -0700 Subject: [PATCH 06/16] Add FIPS 204 ctx mode for ML-DSA + three-way interop validator in CI --- .github/workflows/wolfssl-versions-pqc.yml | 10 + src/wp_mldsa_sig.c | 12 +- test/standalone/include.am | 8 +- .../tests/pqc_interop/test_pqc_interop.c | 592 ++++++++++++++++++ 4 files changed, 616 insertions(+), 6 deletions(-) create mode 100644 test/standalone/tests/pqc_interop/test_pqc_interop.c diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 17b8a76f..5c7df999 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -74,6 +74,16 @@ jobs: fi fi + # Three-way interop: wolfProvider <-> OpenSSL default <-> wolfSSL direct. + # Proves wolfProvider's raw-key, ciphertext, and signature bytes are + # FIPS 203/204 standards-compliant by cross-checking against two + # independent reference implementations. + - name: Three-way PQC interop validation + if: matrix.pqc == true + run: | + LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib" \ + ./test/pqc_interop.test + - name: Print errors on failure if: ${{ failure() }} run: | diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index c4da9b25..33c03639 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -267,8 +267,11 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, } if (ok) { word32 outLen = sigSz; - rc = wc_dilithium_sign_msg(msg, (word32)msgLen, sig, &outLen, - (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), &ctx->rng); + /* FIPS 204 sec 5.2 (Algorithm 22): pure ML-DSA prepends 0x00, ctxLen, + * and ctx before the message. OpenSSL uses an empty context by + * default; use the ctx variant with empty ctx to interop. */ + rc = wc_dilithium_sign_ctx_msg(NULL, 0, msg, (word32)msgLen, sig, + &outLen, (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), &ctx->rng); if (rc != 0) { ok = 0; } @@ -300,8 +303,9 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, return 0; } - rc = wc_dilithium_verify_msg(sig, (word32)sigLen, msg, (word32)msgLen, - &res, (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa)); + /* Match the sign path: FIPS 204 pure ML-DSA with empty context. */ + rc = wc_dilithium_verify_ctx_msg(sig, (word32)sigLen, NULL, 0, msg, + (word32)msgLen, &res, (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa)); if ((rc != 0) || (res != 1)) { ok = 0; } diff --git a/test/standalone/include.am b/test/standalone/include.am index da1a7564..ef673165 100644 --- a/test/standalone/include.am +++ b/test/standalone/include.am @@ -11,8 +11,8 @@ noinst_HEADERS += test/standalone/test_common.h \ # Standalone test programs # Each test compiles to its own binary for isolated execution # Note: These are NOT in check_PROGRAMS because they must be run through scripts, not directly -noinst_PROGRAMS += test/sha256_simple.test test/hardload.test test/fips_baseline.test -DISTCLEANFILES += test/.libs/sha256_simple.test test/.libs/hardload.test test/.libs/fips_baseline.test +noinst_PROGRAMS += test/sha256_simple.test test/hardload.test test/fips_baseline.test test/pqc_interop.test +DISTCLEANFILES += test/.libs/sha256_simple.test test/.libs/hardload.test test/.libs/fips_baseline.test test/.libs/pqc_interop.test # Common flags for all standalone tests STANDALONE_COMMON_CPPFLAGS = -DCERTS_DIR='"$(abs_top_srcdir)/certs"' \ @@ -41,6 +41,10 @@ test_fips_baseline_test_SOURCES = test/standalone/tests/fips_baseline/test_fips_ test/standalone/tests/fips_baseline/test_fips_baseline_pbkdf2.c test_fips_baseline_test_LDADD = $(STANDALONE_COMMON_LDADD) +test_pqc_interop_test_CPPFLAGS = $(STANDALONE_COMMON_CPPFLAGS) +test_pqc_interop_test_SOURCES = test/standalone/tests/pqc_interop/test_pqc_interop.c +test_pqc_interop_test_LDADD = $(STANDALONE_COMMON_LDADD) + # Common test utilities are built automatically by automake # Standalone tests are available for manual execution but not part of make check diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c new file mode 100644 index 00000000..437131eb --- /dev/null +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -0,0 +1,592 @@ +/* test_pqc_interop.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +/* PQC three-way interop validator. + * + * Three independent code paths exercised against each other: + * 1. wolfProvider (via EVP_PKEY API) + * 2. OpenSSL default provider (native ML-KEM / ML-DSA in OpenSSL 3.5+) + * 3. wolfSSL direct (wc_MlKemKey_* / wc_dilithium_* APIs, no provider) + * + * For each algorithm at each NIST level, every cross-pair is tested: + * wolfProv enc/sign -> default dec/verify + * default enc/sign -> wolfProv dec/verify + * wolfProv enc/sign -> wolfssl-dir dec/verify + * wolfssl-dir enc/sign -> wolfProv dec/verify + * + * Passing all three pairings proves the raw-key, ciphertext, and signature + * byte encodings are standards-compliant end-to-end -- not just internally + * round-trippable. + * + * Usage: test_pqc_interop [provider_path] + * provider_path defaults to ".libs" (relative to current dir). + * Set WOLFPROV_PATH env var to override. + */ +#include +#include +#include + +#ifdef WOLFPROV_USER_SETTINGS +#include +#endif +#include + +#include +#include +#include +#include +#include + +#include + +#if defined(WP_HAVE_MLKEM) && defined(WP_HAVE_MLDSA) + +#include +#include +#include +#include + +#define WP_NAME "libwolfprov" + +static OSSL_LIB_CTX* wp_ctx; +static OSSL_LIB_CTX* oss_ctx; +static OSSL_PROVIDER* wp_prov; +static OSSL_PROVIDER* def_prov; +static WC_RNG g_rng; + +static int load_all(const char* wp_path) +{ + wp_ctx = OSSL_LIB_CTX_new(); + oss_ctx = OSSL_LIB_CTX_new(); + if (wp_ctx == NULL || oss_ctx == NULL) return 0; + + OSSL_PROVIDER_set_default_search_path(wp_ctx, wp_path); + wp_prov = OSSL_PROVIDER_load(wp_ctx, WP_NAME); + if (wp_prov == NULL) { + fprintf(stderr, "Failed to load wolfProvider\n"); + ERR_print_errors_fp(stderr); + return 0; + } + def_prov = OSSL_PROVIDER_load(oss_ctx, "default"); + if (def_prov == NULL) { + fprintf(stderr, "Failed to load OpenSSL default provider\n"); + return 0; + } + if (wc_InitRng(&g_rng) != 0) { + fprintf(stderr, "wc_InitRng failed\n"); + return 0; + } + return 1; +} + +static void unload_all(void) +{ + wc_FreeRng(&g_rng); + if (wp_prov) OSSL_PROVIDER_unload(wp_prov); + if (def_prov) OSSL_PROVIDER_unload(def_prov); + if (wp_ctx) OSSL_LIB_CTX_free(wp_ctx); + if (oss_ctx) OSSL_LIB_CTX_free(oss_ctx); +} + +/* Map "ML-KEM-512/768/1024" to wolfSSL type enum. */ +static int mlkem_name_to_type(const char* alg) +{ + if (strcmp(alg, "ML-KEM-512") == 0) return WC_ML_KEM_512; + if (strcmp(alg, "ML-KEM-768") == 0) return WC_ML_KEM_768; + if (strcmp(alg, "ML-KEM-1024") == 0) return WC_ML_KEM_1024; + return -1; +} + +/* Map "ML-DSA-44/65/87" to wolfSSL level byte. */ +static byte mldsa_name_to_level(const char* alg) +{ + if (strcmp(alg, "ML-DSA-44") == 0) return WC_ML_DSA_44; + if (strcmp(alg, "ML-DSA-65") == 0) return WC_ML_DSA_65; + if (strcmp(alg, "ML-DSA-87") == 0) return WC_ML_DSA_87; + return 0; +} + +/* Pull raw pub/priv bytes out of an EVP_PKEY. priv is optional. */ +static int evp_pkey_export_raw(EVP_PKEY* src, unsigned char** pub, + size_t* pubLen, unsigned char** priv, size_t* privLen) +{ + *pub = NULL; *pubLen = 0; + if (priv != NULL) { *priv = NULL; *privLen = 0; } + + if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PUB_KEY, NULL, 0, + pubLen) != 1) return 0; + *pub = OPENSSL_malloc(*pubLen); + if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PUB_KEY, *pub, + *pubLen, pubLen) != 1) return 0; + if (priv != NULL) { + if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, privLen) == 1) { + *priv = OPENSSL_malloc(*privLen); + if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PRIV_KEY, + *priv, *privLen, privLen) != 1) { + OPENSSL_free(*priv); *priv = NULL; *privLen = 0; + } + } + } + return 1; +} + +/* Build an EVP_PKEY from raw pub (and optional priv) on the given lib ctx. */ +static EVP_PKEY* evp_pkey_import_raw(OSSL_LIB_CTX* lib, const char* alg, + const unsigned char* pub, size_t pubLen, + const unsigned char* priv, size_t privLen) +{ + EVP_PKEY* dst = NULL; + EVP_PKEY_CTX* dctx = NULL; + OSSL_PARAM params[3]; + int n = 0; + + dctx = EVP_PKEY_CTX_new_from_name(lib, alg, NULL); + if (dctx == NULL) return NULL; + if (EVP_PKEY_fromdata_init(dctx) != 1) goto end; + + if (pub != NULL) { + params[n++] = OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_PUB_KEY, (void*)pub, pubLen); + } + if (priv != NULL) { + params[n++] = OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_PRIV_KEY, (void*)priv, privLen); + } + params[n] = OSSL_PARAM_construct_end(); + + if (EVP_PKEY_fromdata(dctx, &dst, EVP_PKEY_KEYPAIR, params) != 1) { + dst = NULL; + } + +end: + if (dst == NULL) ERR_print_errors_fp(stderr); + EVP_PKEY_CTX_free(dctx); + return dst; +} + +/* + * ML-KEM helpers + */ + +/* wolfProvider keygen for ML-KEM, returns EVP_PKEY in wp_ctx. */ +static EVP_PKEY* mlkem_wp_keygen(const char* alg) +{ + EVP_PKEY* k = NULL; + EVP_PKEY_CTX* g = EVP_PKEY_CTX_new_from_name(wp_ctx, alg, NULL); + if (g && EVP_PKEY_keygen_init(g) == 1) EVP_PKEY_keygen(g, &k); + EVP_PKEY_CTX_free(g); + return k; +} + +/* EVP encapsulate (lib determines which provider runs). */ +static int evp_encap(OSSL_LIB_CTX* lib, EVP_PKEY* k, unsigned char** ct, + size_t* ctLen, unsigned char* ss, size_t* ssLen) +{ + int ok = 0; + EVP_PKEY_CTX* e = EVP_PKEY_CTX_new_from_pkey(lib, k, NULL); + if (!e || EVP_PKEY_encapsulate_init(e, NULL) != 1) goto end; + if (EVP_PKEY_encapsulate(e, NULL, ctLen, NULL, ssLen) != 1) goto end; + *ct = OPENSSL_malloc(*ctLen); + ok = (EVP_PKEY_encapsulate(e, *ct, ctLen, ss, ssLen) == 1); +end: + EVP_PKEY_CTX_free(e); + return ok; +} + +/* EVP decapsulate. */ +static int evp_decap(OSSL_LIB_CTX* lib, EVP_PKEY* k, unsigned char* ss, + size_t* ssLen, const unsigned char* ct, size_t ctLen) +{ + int ok = 0; + EVP_PKEY_CTX* d = EVP_PKEY_CTX_new_from_pkey(lib, k, NULL); + if (!d || EVP_PKEY_decapsulate_init(d, NULL) != 1) goto end; + ok = (EVP_PKEY_decapsulate(d, ss, ssLen, ct, ctLen) == 1); +end: + EVP_PKEY_CTX_free(d); + return ok; +} + +/* wolfSSL-direct encapsulate using wc_* APIs (no provider involved). + * Pub bytes loaded from raw, ct + ss returned. */ +static int wc_mlkem_encap_direct(const char* alg, const unsigned char* pub, + size_t pubLen, unsigned char** ct, size_t* ctLen, + unsigned char* ss, size_t ssCap) +{ + MlKemKey key; + int rc; + word32 ctSize = 0; + int type = mlkem_name_to_type(alg); + + if (wc_MlKemKey_Init(&key, type, NULL, INVALID_DEVID) != 0) return 0; + rc = wc_MlKemKey_DecodePublicKey(&key, pub, (word32)pubLen); + if (rc != 0) { wc_MlKemKey_Free(&key); return 0; } + rc = wc_MlKemKey_CipherTextSize(&key, &ctSize); + if (rc != 0) { wc_MlKemKey_Free(&key); return 0; } + *ct = OPENSSL_malloc(ctSize); + *ctLen = ctSize; + if (ssCap < WC_ML_KEM_SS_SZ) { wc_MlKemKey_Free(&key); return 0; } + rc = wc_MlKemKey_Encapsulate(&key, *ct, ss, &g_rng); + wc_MlKemKey_Free(&key); + return rc == 0; +} + +/* wolfSSL-direct decapsulate. */ +static int wc_mlkem_decap_direct(const char* alg, const unsigned char* priv, + size_t privLen, const unsigned char* ct, size_t ctLen, + unsigned char* ss, size_t ssCap) +{ + MlKemKey key; + int rc; + int type = mlkem_name_to_type(alg); + + if (wc_MlKemKey_Init(&key, type, NULL, INVALID_DEVID) != 0) return 0; + rc = wc_MlKemKey_DecodePrivateKey(&key, priv, (word32)privLen); + if (rc != 0) { wc_MlKemKey_Free(&key); return 0; } + if (ssCap < WC_ML_KEM_SS_SZ) { wc_MlKemKey_Free(&key); return 0; } + rc = wc_MlKemKey_Decapsulate(&key, ss, ct, (word32)ctLen); + wc_MlKemKey_Free(&key); + return rc == 0; +} + +/* + * ML-DSA helpers + */ + +static EVP_PKEY* mldsa_wp_keygen(const char* alg) +{ + EVP_PKEY* k = NULL; + EVP_PKEY_CTX* g = EVP_PKEY_CTX_new_from_name(wp_ctx, alg, NULL); + if (g && EVP_PKEY_keygen_init(g) == 1) EVP_PKEY_keygen(g, &k); + EVP_PKEY_CTX_free(g); + return k; +} + +static int evp_sign(OSSL_LIB_CTX* lib, EVP_PKEY* k, const unsigned char* msg, + size_t msgLen, unsigned char** sig, size_t* sigLen) +{ + int ok = 0; + EVP_MD_CTX* s = EVP_MD_CTX_new(); + if (EVP_DigestSignInit_ex(s, NULL, NULL, lib, NULL, k, NULL) != 1) goto end; + if (EVP_DigestSign(s, NULL, sigLen, msg, msgLen) != 1) goto end; + *sig = OPENSSL_malloc(*sigLen); + ok = (EVP_DigestSign(s, *sig, sigLen, msg, msgLen) == 1); +end: + EVP_MD_CTX_free(s); + return ok; +} + +static int evp_verify(OSSL_LIB_CTX* lib, EVP_PKEY* k, const unsigned char* msg, + size_t msgLen, const unsigned char* sig, size_t sigLen) +{ + int ok = 0; + EVP_MD_CTX* v = EVP_MD_CTX_new(); + if (EVP_DigestVerifyInit_ex(v, NULL, NULL, lib, NULL, k, NULL) != 1) + goto end; + ok = (EVP_DigestVerify(v, sig, sigLen, msg, msgLen) == 1); +end: + EVP_MD_CTX_free(v); + return ok; +} + +/* wolfSSL-direct sign using wc_dilithium_sign_ctx_msg with empty context + * (FIPS 204 pure ML-DSA). */ +static int wc_mldsa_sign_direct(const char* alg, const unsigned char* priv, + size_t privLen, const unsigned char* msg, size_t msgLen, + unsigned char** sig, size_t* sigLen) +{ + dilithium_key key; + int rc; + word32 outLen; + int sigSz; + byte level = mldsa_name_to_level(alg); + + if (wc_dilithium_init_ex(&key, NULL, INVALID_DEVID) != 0) return 0; + if (wc_dilithium_set_level(&key, level) != 0) { + wc_dilithium_free(&key); return 0; + } + rc = wc_dilithium_import_private(priv, (word32)privLen, &key); + if (rc != 0) { wc_dilithium_free(&key); return 0; } + sigSz = wc_dilithium_sig_size(&key); + if (sigSz <= 0) { wc_dilithium_free(&key); return 0; } + *sig = OPENSSL_malloc(sigSz); + outLen = (word32)sigSz; + rc = wc_dilithium_sign_ctx_msg(NULL, 0, msg, (word32)msgLen, *sig, &outLen, + &key, &g_rng); + wc_dilithium_free(&key); + if (rc != 0) { OPENSSL_free(*sig); *sig = NULL; return 0; } + *sigLen = outLen; + return 1; +} + +/* wolfSSL-direct verify. */ +static int wc_mldsa_verify_direct(const char* alg, const unsigned char* pub, + size_t pubLen, const unsigned char* msg, size_t msgLen, + const unsigned char* sig, size_t sigLen) +{ + dilithium_key key; + int rc; + int res = 0; + byte level = mldsa_name_to_level(alg); + + if (wc_dilithium_init_ex(&key, NULL, INVALID_DEVID) != 0) return 0; + if (wc_dilithium_set_level(&key, level) != 0) { + wc_dilithium_free(&key); return 0; + } + rc = wc_dilithium_import_public(pub, (word32)pubLen, &key); + if (rc != 0) { wc_dilithium_free(&key); return 0; } + rc = wc_dilithium_verify_ctx_msg(sig, (word32)sigLen, NULL, 0, msg, + (word32)msgLen, &res, &key); + wc_dilithium_free(&key); + return rc == 0 && res == 1; +} + +/* + * Test cases - each is one cross-pair. + */ + +/* wolfProvider encap -> partner decap (partner=default OR direct). */ +static int test_mlkem_pair_wp_to(const char* alg, const char* partner) +{ + int ok = 0; + EVP_PKEY* wp_key = mlkem_wp_keygen(alg); + EVP_PKEY* part_key = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* ct = NULL; + unsigned char ss1[32], ss2[32]; + size_t pubLen = 0, privLen = 0, ctLen = 0; + size_t ss1Len = sizeof(ss1), ss2Len = sizeof(ss2); + + if (!wp_key) goto end; + if (!evp_pkey_export_raw(wp_key, &pub, &pubLen, &priv, &privLen)) goto end; + + /* wolfProvider encapsulates. */ + if (!evp_encap(wp_ctx, wp_key, &ct, &ctLen, ss1, &ss1Len)) goto end; + + if (strcmp(partner, "default") == 0) { + part_key = evp_pkey_import_raw(oss_ctx, alg, pub, pubLen, priv, + privLen); + if (!part_key) goto end; + if (!evp_decap(oss_ctx, part_key, ss2, &ss2Len, ct, ctLen)) goto end; + } + else { /* direct */ + if (!wc_mlkem_decap_direct(alg, priv, privLen, ct, ctLen, ss2, + sizeof(ss2))) goto end; + ss2Len = WC_ML_KEM_SS_SZ; + } + ok = (ss1Len == ss2Len) && memcmp(ss1, ss2, ss1Len) == 0; + +end: + if (!ok) ERR_print_errors_fp(stderr); + printf(" %-12s wolfProv enc -> %-7s dec : %s\n", alg, partner, + ok ? "PASS" : "FAIL"); + OPENSSL_free(pub); + OPENSSL_clear_free(priv, privLen); + OPENSSL_free(ct); + EVP_PKEY_free(wp_key); + EVP_PKEY_free(part_key); + return ok; +} + +/* partner encap -> wolfProvider decap. */ +static int test_mlkem_pair_to_wp(const char* alg, const char* partner) +{ + int ok = 0; + EVP_PKEY* wp_key = mlkem_wp_keygen(alg); + EVP_PKEY* part_key = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* ct = NULL; + unsigned char ss1[32], ss2[32]; + size_t pubLen = 0, privLen = 0, ctLen = 0; + size_t ss1Len = sizeof(ss1), ss2Len = sizeof(ss2); + + if (!wp_key) goto end; + if (!evp_pkey_export_raw(wp_key, &pub, &pubLen, &priv, &privLen)) goto end; + + if (strcmp(partner, "default") == 0) { + part_key = evp_pkey_import_raw(oss_ctx, alg, pub, pubLen, NULL, 0); + if (!part_key) goto end; + if (!evp_encap(oss_ctx, part_key, &ct, &ctLen, ss1, &ss1Len)) goto end; + } + else { /* direct */ + if (!wc_mlkem_encap_direct(alg, pub, pubLen, &ct, &ctLen, ss1, + sizeof(ss1))) goto end; + ss1Len = WC_ML_KEM_SS_SZ; + } + + if (!evp_decap(wp_ctx, wp_key, ss2, &ss2Len, ct, ctLen)) goto end; + ok = (ss1Len == ss2Len) && memcmp(ss1, ss2, ss1Len) == 0; + +end: + if (!ok) ERR_print_errors_fp(stderr); + printf(" %-12s %-7s enc -> wolfProv dec : %s\n", alg, partner, + ok ? "PASS" : "FAIL"); + OPENSSL_free(pub); + OPENSSL_clear_free(priv, privLen); + OPENSSL_free(ct); + EVP_PKEY_free(wp_key); + EVP_PKEY_free(part_key); + return ok; +} + +static const char* mldsa_msg = + "wolfProvider three-way ML-DSA interop validation message"; + +/* wolfProvider sign -> partner verify. */ +static int test_mldsa_pair_wp_to(const char* alg, const char* partner) +{ + int ok = 0; + EVP_PKEY* wp_key = mldsa_wp_keygen(alg); + EVP_PKEY* part_key = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* sig = NULL; + size_t pubLen = 0, privLen = 0, sigLen = 0; + size_t msgLen = strlen(mldsa_msg); + + if (!wp_key) goto end; + if (!evp_pkey_export_raw(wp_key, &pub, &pubLen, &priv, &privLen)) goto end; + + if (!evp_sign(wp_ctx, wp_key, (const unsigned char*)mldsa_msg, msgLen, + &sig, &sigLen)) goto end; + + if (strcmp(partner, "default") == 0) { + part_key = evp_pkey_import_raw(oss_ctx, alg, pub, pubLen, NULL, 0); + if (!part_key) goto end; + ok = evp_verify(oss_ctx, part_key, (const unsigned char*)mldsa_msg, + msgLen, sig, sigLen); + } + else { /* direct */ + ok = wc_mldsa_verify_direct(alg, pub, pubLen, + (const unsigned char*)mldsa_msg, msgLen, sig, sigLen); + } + +end: + if (!ok) ERR_print_errors_fp(stderr); + printf(" %-12s wolfProv sign -> %-7s vrfy: %s\n", alg, partner, + ok ? "PASS" : "FAIL"); + OPENSSL_free(pub); + OPENSSL_clear_free(priv, privLen); + OPENSSL_free(sig); + EVP_PKEY_free(wp_key); + EVP_PKEY_free(part_key); + return ok; +} + +/* partner sign -> wolfProvider verify. */ +static int test_mldsa_pair_to_wp(const char* alg, const char* partner) +{ + int ok = 0; + EVP_PKEY* wp_key = mldsa_wp_keygen(alg); + EVP_PKEY* part_key = NULL; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + unsigned char* sig = NULL; + size_t pubLen = 0, privLen = 0, sigLen = 0; + size_t msgLen = strlen(mldsa_msg); + + if (!wp_key) goto end; + if (!evp_pkey_export_raw(wp_key, &pub, &pubLen, &priv, &privLen)) goto end; + + if (strcmp(partner, "default") == 0) { + part_key = evp_pkey_import_raw(oss_ctx, alg, pub, pubLen, priv, + privLen); + if (!part_key) goto end; + if (!evp_sign(oss_ctx, part_key, (const unsigned char*)mldsa_msg, + msgLen, &sig, &sigLen)) goto end; + } + else { /* direct */ + if (!wc_mldsa_sign_direct(alg, priv, privLen, + (const unsigned char*)mldsa_msg, msgLen, &sig, &sigLen)) + goto end; + } + + ok = evp_verify(wp_ctx, wp_key, (const unsigned char*)mldsa_msg, msgLen, + sig, sigLen); + +end: + if (!ok) ERR_print_errors_fp(stderr); + printf(" %-12s %-7s sign -> wolfProv vrfy: %s\n", alg, partner, + ok ? "PASS" : "FAIL"); + OPENSSL_free(pub); + OPENSSL_clear_free(priv, privLen); + OPENSSL_free(sig); + EVP_PKEY_free(wp_key); + EVP_PKEY_free(part_key); + return ok; +} + + +int main(int argc, char* argv[]) +{ + int fail = 0; + const char* mlkem[] = { "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024" }; + const char* mldsa[] = { "ML-DSA-44", "ML-DSA-65", "ML-DSA-87" }; + const char* wp_path = ".libs"; + const char* env_path; + size_t i; + + if (argc > 1) { + wp_path = argv[1]; + } + else { + env_path = getenv("WOLFPROV_PATH"); + if (env_path != NULL) { + wp_path = env_path; + } + } + + if (!load_all(wp_path)) return 1; + + printf("ML-KEM three-way interop:\n"); + printf(" (wolfProvider) <-> (OpenSSL default) and <-> (wolfSSL direct)\n"); + for (i = 0; i < 3; i++) { + if (!test_mlkem_pair_wp_to(mlkem[i], "default")) fail++; + if (!test_mlkem_pair_to_wp(mlkem[i], "default")) fail++; + if (!test_mlkem_pair_wp_to(mlkem[i], "direct")) fail++; + if (!test_mlkem_pair_to_wp(mlkem[i], "direct")) fail++; + } + + printf("\nML-DSA three-way interop:\n"); + printf(" (wolfProvider) <-> (OpenSSL default) and <-> (wolfSSL direct)\n"); + for (i = 0; i < 3; i++) { + if (!test_mldsa_pair_wp_to(mldsa[i], "default")) fail++; + if (!test_mldsa_pair_to_wp(mldsa[i], "default")) fail++; + if (!test_mldsa_pair_wp_to(mldsa[i], "direct")) fail++; + if (!test_mldsa_pair_to_wp(mldsa[i], "direct")) fail++; + } + + unload_all(); + printf("\n%s: %d failure(s)\n", fail == 0 ? "ALL PASS" : "FAILED", fail); + return fail ? 1 : 0; +} + +#else /* !WP_HAVE_MLKEM || !WP_HAVE_MLDSA */ + +int main(void) +{ + printf("PQC interop test skipped: wolfProvider built without ML-KEM and " + "ML-DSA support.\n"); + return 0; +} + +#endif /* WP_HAVE_MLKEM && WP_HAVE_MLDSA */ From 60f2cd6d3b46a4c9bb6412281538a29d9c874579 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:54:33 -0700 Subject: [PATCH 07/16] Add ML-KEM and ML-DSA raw key import/export roundtrip tests --- test/test_mldsa.c | 102 +++++++++++++++++++++++++++++++++++++++++++++ test/test_mlkem.c | 103 ++++++++++++++++++++++++++++++++++++++++++++++ test/unit.c | 2 + test/unit.h | 2 + 4 files changed, 209 insertions(+) diff --git a/test/test_mldsa.c b/test/test_mldsa.c index 70fcdb77..d094d3f9 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -21,6 +21,7 @@ #include "unit.h" #include +#include #ifdef WP_HAVE_MLDSA @@ -210,6 +211,107 @@ int test_mldsa_keygen(void* data) return err; } +/** + * Test ML-DSA raw key import/export round-trip. + * + * For each level: keygen, export both pub and priv, import into a fresh + * EVP_PKEY, re-export, and verify the bytes match exactly. + */ +int test_mldsa_import_export_roundtrip(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + unsigned char* pub1 = NULL; + unsigned char* pub2 = NULL; + unsigned char* priv1 = NULL; + unsigned char* priv2 = NULL; + size_t pub1Len = 0, pub2Len = 0, priv1Len = 0, priv2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Import/export roundtrip %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k1); + if (err == 0) { + err = mldsa_get_pub(k1, &pub1, &pub1Len); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k1, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, &priv1Len) != 1; + } + if (err == 0) { + priv1 = (unsigned char*)OPENSSL_malloc(priv1Len); + err = (priv1 == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k1, OSSL_PKEY_PARAM_PRIV_KEY, + priv1, priv1Len, &priv1Len) != 1; + } + + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, lvl->name, NULL); + err = (ctx == NULL) || EVP_PKEY_fromdata_init(ctx) != 1; + } + if (err == 0) { + OSSL_PARAM_BLD* bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL) + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PUB_KEY, pub1, pub1Len) != 1 + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv1, priv1Len) != 1; + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + OSSL_PARAM_BLD_free(bld); + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &k2, EVP_PKEY_KEYPAIR, params) != 1; + } + if (err == 0) { + err = mldsa_get_pub(k2, &pub2, &pub2Len); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k2, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, &priv2Len) != 1; + } + if (err == 0) { + priv2 = (unsigned char*)OPENSSL_malloc(priv2Len); + err = (priv2 == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k2, OSSL_PKEY_PARAM_PRIV_KEY, + priv2, priv2Len, &priv2Len) != 1; + } + if (err == 0) { + err = (pub1Len != pub2Len) || + (memcmp(pub1, pub2, pub1Len) != 0); + if (err) PRINT_ERR_MSG("Public key roundtrip mismatch"); + } + if (err == 0) { + err = (priv1Len != priv2Len) || + (memcmp(priv1, priv2, priv1Len) != 0); + if (err) PRINT_ERR_MSG("Private key roundtrip mismatch"); + } + + OPENSSL_free(pub1); pub1 = NULL; + OPENSSL_free(pub2); pub2 = NULL; + OPENSSL_clear_free(priv1, priv1Len); priv1 = NULL; priv1Len = 0; + OPENSSL_clear_free(priv2, priv2Len); priv2 = NULL; priv2Len = 0; + OSSL_PARAM_free(params); params = NULL; + EVP_PKEY_CTX_free(ctx); ctx = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + /** * Test ML-DSA sign / verify round-trip via the digest-sign EVP API. */ diff --git a/test/test_mlkem.c b/test/test_mlkem.c index 50c855ad..091e70c9 100644 --- a/test/test_mlkem.c +++ b/test/test_mlkem.c @@ -153,6 +153,109 @@ int test_mlkem_keygen(void* data) return err; } +/** + * Test ML-KEM raw key import/export round-trip. + * + * For each level: keygen, export both pub and priv via EVP_PKEY_todata, + * import into a fresh EVP_PKEY via EVP_PKEY_fromdata, re-export, and verify + * the bytes match exactly. Proves the OSSL_PARAM marshaling for raw keys is + * lossless in both directions. + */ +int test_mlkem_import_export_roundtrip(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + unsigned char* pub1 = NULL; + unsigned char* pub2 = NULL; + unsigned char* priv1 = NULL; + unsigned char* priv2 = NULL; + size_t pub1Len = 0, pub2Len = 0, priv1Len = 0, priv2Len = 0; + + (void)data; + + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Import/export roundtrip %s", lvl->name); + + err = mlkem_keygen(lvl->name, &k1); + if (err == 0) { + err = mlkem_get_pub(k1, &pub1, &pub1Len); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k1, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, &priv1Len) != 1; + } + if (err == 0) { + priv1 = (unsigned char*)OPENSSL_malloc(priv1Len); + err = (priv1 == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k1, OSSL_PKEY_PARAM_PRIV_KEY, + priv1, priv1Len, &priv1Len) != 1; + } + + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, lvl->name, NULL); + err = (ctx == NULL) || EVP_PKEY_fromdata_init(ctx) != 1; + } + if (err == 0) { + OSSL_PARAM_BLD* bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL) + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PUB_KEY, pub1, pub1Len) != 1 + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv1, priv1Len) != 1; + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + OSSL_PARAM_BLD_free(bld); + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &k2, EVP_PKEY_KEYPAIR, params) != 1; + } + if (err == 0) { + err = mlkem_get_pub(k2, &pub2, &pub2Len); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k2, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, &priv2Len) != 1; + } + if (err == 0) { + priv2 = (unsigned char*)OPENSSL_malloc(priv2Len); + err = (priv2 == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k2, OSSL_PKEY_PARAM_PRIV_KEY, + priv2, priv2Len, &priv2Len) != 1; + } + if (err == 0) { + err = (pub1Len != pub2Len) || + (memcmp(pub1, pub2, pub1Len) != 0); + if (err) PRINT_ERR_MSG("Public key roundtrip mismatch"); + } + if (err == 0) { + err = (priv1Len != priv2Len) || + (memcmp(priv1, priv2, priv1Len) != 0); + if (err) PRINT_ERR_MSG("Private key roundtrip mismatch"); + } + + OPENSSL_free(pub1); pub1 = NULL; + OPENSSL_free(pub2); pub2 = NULL; + OPENSSL_clear_free(priv1, priv1Len); priv1 = NULL; priv1Len = 0; + OPENSSL_clear_free(priv2, priv2Len); priv2 = NULL; priv2Len = 0; + OSSL_PARAM_free(params); params = NULL; + EVP_PKEY_CTX_free(ctx); ctx = NULL; + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + /** * Test ML-KEM encapsulate / decapsulate round trip via EVP_PKEY API. */ diff --git a/test/unit.c b/test/unit.c index 37a49b9f..1656962b 100644 --- a/test/unit.c +++ b/test/unit.c @@ -481,6 +481,7 @@ TEST_CASE test_case[] = { #ifdef WP_HAVE_MLKEM TEST_DECL(test_mlkem_keygen, NULL), + TEST_DECL(test_mlkem_import_export_roundtrip, NULL), TEST_DECL(test_mlkem_encap_decap, NULL), TEST_DECL(test_mlkem_decap_tampered_ct, NULL), TEST_DECL(test_mlkem_decap_wrong_key, NULL), @@ -488,6 +489,7 @@ TEST_CASE test_case[] = { #ifdef WP_HAVE_MLDSA TEST_DECL(test_mldsa_keygen, NULL), + TEST_DECL(test_mldsa_import_export_roundtrip, NULL), TEST_DECL(test_mldsa_sign_verify, NULL), TEST_DECL(test_mldsa_verify_tampered_sig, NULL), TEST_DECL(test_mldsa_verify_tampered_msg, NULL), diff --git a/test/unit.h b/test/unit.h index eda647ba..2616f9cb 100644 --- a/test/unit.h +++ b/test/unit.h @@ -479,6 +479,7 @@ int test_des3_tls_cbc_bad_pad(void *data); #ifdef WP_HAVE_MLKEM int test_mlkem_keygen(void *data); +int test_mlkem_import_export_roundtrip(void *data); int test_mlkem_encap_decap(void *data); int test_mlkem_decap_tampered_ct(void *data); int test_mlkem_decap_wrong_key(void *data); @@ -486,6 +487,7 @@ int test_mlkem_decap_wrong_key(void *data); #ifdef WP_HAVE_MLDSA int test_mldsa_keygen(void *data); +int test_mldsa_import_export_roundtrip(void *data); int test_mldsa_sign_verify(void *data); int test_mldsa_verify_tampered_sig(void *data); int test_mldsa_verify_tampered_msg(void *data); From dae5cd63a9bfc5fa0d258e52e241c2e9334a3c71 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 23:11:37 -0700 Subject: [PATCH 08/16] Gate PQC macros on header availability via __has_include --- include/wolfprovider/settings.h | 39 ++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index 895fef1c..a1dc4b37 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -169,17 +169,40 @@ #ifdef HAVE_ED448 #define WP_HAVE_ED448 #endif +/* PQC: gate on both wolfSSL feature macro AND header availability. On wolfSSL + * master with --enable-all-crypto (no --enable-experimental), the feature + * macros can be defined in options.h while the mlkem.h / dilithium.h headers + * are not installed, so probe the headers too. */ #ifdef WOLFSSL_HAVE_MLKEM - #define WP_HAVE_MLKEM - #define WP_HAVE_ML_KEM_512 - #define WP_HAVE_ML_KEM_768 - #define WP_HAVE_ML_KEM_1024 + #if defined(__has_include) + #if __has_include() && \ + __has_include() + #define WP_HAVE_MLKEM + #define WP_HAVE_ML_KEM_512 + #define WP_HAVE_ML_KEM_768 + #define WP_HAVE_ML_KEM_1024 + #endif + #else + #define WP_HAVE_MLKEM + #define WP_HAVE_ML_KEM_512 + #define WP_HAVE_ML_KEM_768 + #define WP_HAVE_ML_KEM_1024 + #endif #endif #ifdef HAVE_DILITHIUM - #define WP_HAVE_MLDSA - #define WP_HAVE_ML_DSA_44 - #define WP_HAVE_ML_DSA_65 - #define WP_HAVE_ML_DSA_87 + #if defined(__has_include) + #if __has_include() + #define WP_HAVE_MLDSA + #define WP_HAVE_ML_DSA_44 + #define WP_HAVE_ML_DSA_65 + #define WP_HAVE_ML_DSA_87 + #endif + #else + #define WP_HAVE_MLDSA + #define WP_HAVE_ML_DSA_44 + #define WP_HAVE_ML_DSA_65 + #define WP_HAVE_ML_DSA_87 + #endif #endif #if !defined(NO_AES_CBC) && (defined(WP_HAVE_HMAC) || defined(WP_HAVE_CMAC)) #define WP_HAVE_KBKDF From 0aec54f7afb96bbc9f647e44a293d748b4011884 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 23:36:06 -0700 Subject: [PATCH 09/16] Address Copilot review + dynamic wolfSSL version matrix with PQC floor --- .github/workflows/wolfssl-versions-pqc.yml | 121 ++++++++++++------ src/wp_mldsa_kmgmt.c | 28 ++-- src/wp_mldsa_sig.c | 18 ++- src/wp_mlkem_kem.c | 12 +- src/wp_mlkem_kmgmt.c | 31 +++-- .../tests/pqc_interop/test_pqc_interop.c | 40 +++++- 6 files changed, 181 insertions(+), 69 deletions(-) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 5c7df999..a8e864f9 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -1,62 +1,108 @@ name: wolfSSL Versions (PQC) -# Backward-compatibility matrix for ML-KEM and ML-DSA. +# Backward-compatibility matrix for ML-KEM and ML-DSA. Mirrors wolfTPM's +# wolfssl-versions-pqc.yml pattern: a discover-versions job dynamically +# resolves the latest -stable wolfSSL tag and decides if it is past the PQC +# floor, then the build job runs three rows -- pre-PQC floor, dynamically +# resolved latest -stable, and master. # -# Three rows: -# - pre-PQC wolfSSL (e.g. v5.7.0-stable): -# wolfSSL is built without --enable-mlkem/--enable-dilithium. wolfProvider -# auto-detects via settings.h that PQC macros are undefined; the PQC -# source files compile to no-ops; the ML-KEM/ML-DSA tests are skipped. -# Proves the no-symbol path still builds and runs cleanly. -# - latest stable wolfSSL with PQC enabled: -# --enable-pqc is passed to scripts/build-wolfprovider.sh, which adds -# --enable-mlkem --enable-dilithium --enable-experimental to wolfSSL. -# wolfProvider's settings.h picks up WP_HAVE_MLKEM and WP_HAVE_MLDSA; -# the PQC tests run and must pass. -# - master wolfSSL with PQC enabled: -# Same as above against the development tip. +# PQC_FLOOR is v5.9.1-stable: the wc_MlDsaKey_* / wc_dilithium_sign_ctx_msg +# API that wolfProvider's PQC code depends on lands post-v5.9.1-stable +# (wolfSSL PR #10436), so v5.9.2-stable+ is the first PQC-eligible release. +# Older wolfSSL versions skip the PQC code paths via settings.h gating and +# only verify the no-symbol path still builds. on: push: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + types: [opened, synchronize, reopened, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: - pqc_version_test: + discover-versions: + name: Resolve wolfSSL version matrix + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + latest-stable: ${{ steps.set-matrix.outputs.latest-stable }} + steps: + - name: Resolve latest -stable wolfSSL tag + id: set-matrix + run: | + set -euo pipefail + LATEST=$(git ls-remote --tags --refs \ + https://github.com/wolfSSL/wolfssl.git 'v*-stable' \ + | awk -F/ '{print $NF}' | sort -V | tail -n 1) + if [ -z "${LATEST:-}" ]; then + echo "::error::Could not resolve latest wolfSSL -stable tag" + exit 1 + fi + echo "Latest stable wolfSSL: $LATEST" + echo "latest-stable=$LATEST" >> "$GITHUB_OUTPUT" + # Enable PQC when $LATEST is strictly newer than v5.9.1-stable + # (i.e. v5.9.2-stable, v5.10+, v6+, ...). Anything at or before + # the floor lacks the wc_MlDsaKey_* / wc_dilithium_sign_ctx_msg + # API and stays on the no-symbol path. + PQC_FLOOR="v5.9.1-stable" + if [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$LATEST" \ + | sort -V | tail -n1)" != "$PQC_FLOOR" ]; then + LATEST_PQC_ELIGIBLE=true + else + LATEST_PQC_ELIGIBLE=false + fi + echo "latest-stable PQC eligible: $LATEST_PQC_ELIGIBLE" + MATRIX=$(jq -nc \ + --arg latest "$LATEST" \ + --argjson latest_pqc "$LATEST_PQC_ELIGIBLE" '{ + include: [ + {"name":"pre-PQC (v5.8.0-stable, PQC disabled)", + "wolfssl-ref":"v5.8.0-stable","pqc":false}, + {"name":("latest stable (" + $latest + ", PQC " + + (if $latest_pqc then "enabled" else "disabled" end) + ")"), + "wolfssl-ref":$latest,"pqc":$latest_pqc}, + {"name":"master (PQC enabled)", + "wolfssl-ref":"master","pqc":true} + ] + }') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + + pqc-build-test: name: ${{ matrix.name }} + needs: discover-versions + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-22.04 timeout-minutes: 30 strategy: fail-fast: false - matrix: - include: - - name: pre-PQC (v5.7.0-stable, PQC disabled) - wolfssl_ref: v5.7.0-stable - pqc: false - - name: latest stable (v5.8.4-stable, PQC enabled) - wolfssl_ref: v5.8.4-stable - pqc: true - - name: master (PQC enabled) - wolfssl_ref: master - pqc: true + matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} steps: - name: Checkout wolfProvider uses: actions/checkout@v4 with: fetch-depth: 1 - - name: Build and test wolfProvider (PQC=${{ matrix.pqc }}) + # OpenSSL is pinned to 3.5.4 on every row so the cross-provider interop + # test can verify against the default provider's native ML-KEM/ML-DSA. + # OpenSSL 3.5 is the first release with native PQC support; older 3.x + # versions can build wolfProvider but the interop step would have + # nothing to compare against on the default-provider side. + - name: Build wolfProvider (PQC=${{ matrix.pqc }}) run: | if [ "${{ matrix.pqc }}" = "true" ]; then - WOLFSSL_TAG=${{ matrix.wolfssl_ref }} \ + OPENSSL_TAG=openssl-3.5.4 \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ ./scripts/build-wolfprovider.sh --enable-pqc else - WOLFSSL_TAG=${{ matrix.wolfssl_ref }} \ + OPENSSL_TAG=openssl-3.5.4 \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ ./scripts/build-wolfprovider.sh fi @@ -64,20 +110,23 @@ jobs: run: | if [ "${{ matrix.pqc }}" = "true" ]; then ./test/unit.test --list | grep -q 'test_mlkem_keygen' \ - || { echo 'ERROR: PQC tests missing in PQC-enabled build'; exit 1; } + || { echo 'ERROR: PQC tests missing in PQC-enabled build'; \ + exit 1; } ./test/unit.test --list | grep -q 'test_mldsa_sign_verify' \ - || { echo 'ERROR: ML-DSA tests missing in PQC-enabled build'; exit 1; } + || { echo 'ERROR: ML-DSA tests missing in PQC-enabled build'; \ + exit 1; } else - if ./test/unit.test --list | grep -qE 'test_mlkem|test_mldsa'; then - echo 'ERROR: PQC tests present in pre-PQC build (should be skipped)' + if ./test/unit.test --list | grep -qE 'test_mlkem|test_mldsa'; \ + then + echo 'ERROR: PQC tests present in pre-PQC build (should skip)' exit 1 fi fi # Three-way interop: wolfProvider <-> OpenSSL default <-> wolfSSL direct. - # Proves wolfProvider's raw-key, ciphertext, and signature bytes are - # FIPS 203/204 standards-compliant by cross-checking against two - # independent reference implementations. + # Only runs on PQC-enabled rows; OpenSSL 3.5+ has native ML-KEM/ML-DSA + # in the default provider, so this proves wolfProvider's bytes are + # FIPS 203/204 standards-compliant against two reference implementations. - name: Three-way PQC interop validation if: matrix.pqc == true run: | diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 3498d816..0c80a648 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -689,13 +689,19 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) p->return_size = outLen; } else if (mldsa->hasPub) { - rc = wc_dilithium_export_public(&mldsa->key, - (unsigned char*)p->data, &outLen); - if (rc != 0) { + if (p->data_size < outLen) { ok = 0; } else { - p->return_size = outLen; + outLen = (word32)p->data_size; + rc = wc_dilithium_export_public(&mldsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } } } } @@ -708,13 +714,19 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) p->return_size = outLen; } else if (mldsa->hasPriv) { - rc = wc_dilithium_export_private(&mldsa->key, - (unsigned char*)p->data, &outLen); - if (rc != 0) { + if (p->data_size < outLen) { ok = 0; } else { - p->return_size = outLen; + outLen = (word32)p->data_size; + rc = wc_dilithium_export_private(&mldsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } } } } diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 33c03639..10e3773b 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -371,6 +371,9 @@ static int wp_mldsa_digest_signverify_update(wp_MlDsaSigCtx* ctx, static int wp_mldsa_digest_sign_final(wp_MlDsaSigCtx* ctx, unsigned char* sig, size_t* sigLen, size_t sigSize) { + if (ctx == NULL) { + return 0; + } return wp_mldsa_sign(ctx, sig, sigLen, sigSize, ctx->mdBuf, ctx->mdLen); } @@ -385,17 +388,20 @@ static int wp_mldsa_digest_sign_final(wp_MlDsaSigCtx* ctx, unsigned char* sig, static int wp_mldsa_digest_verify_final(wp_MlDsaSigCtx* ctx, const unsigned char* sig, size_t sigLen) { + if (ctx == NULL) { + return 0; + } return wp_mldsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); } /** - * Get ctx params. None supported. + * Get ctx params. None supported; checks ctx is non-NULL to match other + * provider implementations. */ static int wp_mldsa_get_ctx_params(wp_MlDsaSigCtx* ctx, OSSL_PARAM* params) { - (void)ctx; (void)params; - return 1; + return ctx != NULL; } static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, @@ -410,14 +416,14 @@ static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, } /** - * Set ctx params. None supported. + * Set ctx params. None supported; checks ctx is non-NULL to match other + * provider implementations. */ static int wp_mldsa_set_ctx_params(wp_MlDsaSigCtx* ctx, const OSSL_PARAM params[]) { - (void)ctx; (void)params; - return 1; + return ctx != NULL; } static const OSSL_PARAM* wp_mldsa_settable_ctx_params(wp_MlDsaSigCtx* ctx, diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index 71c98f80..fca165eb 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -266,13 +266,13 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, } /** - * Get ctx params. None supported. + * Get ctx params. None supported; checks ctx is non-NULL to match other + * provider implementations. */ static int wp_mlkem_kem_get_ctx_params(wp_MlKemCtx* ctx, OSSL_PARAM* params) { - (void)ctx; (void)params; - return 1; + return ctx != NULL; } static const OSSL_PARAM* wp_mlkem_kem_gettable_ctx_params(wp_MlKemCtx* ctx, @@ -287,14 +287,14 @@ static const OSSL_PARAM* wp_mlkem_kem_gettable_ctx_params(wp_MlKemCtx* ctx, } /** - * Set ctx params. None supported. + * Set ctx params. None supported; checks ctx is non-NULL to match other + * provider implementations. */ static int wp_mlkem_kem_set_ctx_params(wp_MlKemCtx* ctx, const OSSL_PARAM params[]) { - (void)ctx; (void)params; - return 1; + return ctx != NULL; } static const OSSL_PARAM* wp_mlkem_kem_settable_ctx_params(wp_MlKemCtx* ctx, diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index ea6061c5..753bda8d 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -698,13 +698,18 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) p->return_size = outLen; } else if (mlkem->hasPub) { - rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, - (unsigned char*)p->data, outLen); - if (rc != 0) { + if (p->data_size < outLen) { ok = 0; } else { - p->return_size = outLen; + rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } } } } @@ -717,13 +722,18 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) p->return_size = outLen; } else if (mlkem->hasPriv) { - rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, - (unsigned char*)p->data, outLen); - if (rc != 0) { + if (p->data_size < outLen) { ok = 0; } else { - p->return_size = outLen; + rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } } } } @@ -886,8 +896,11 @@ static void wp_mlkem_gen_cleanup(wp_MlKemGenCtx* ctx) /** * Return the algorithm name for OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME. * + * ML-KEM has no associated operation name lookup; return NULL so OpenSSL + * falls back to the algorithm name from the dispatch table. + * * @param [in] op Operation type. Unused. - * @return Empty string (default). + * @return NULL. */ static const char* wp_mlkem_query_operation_name(int op) { diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index 437131eb..24e39886 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -131,14 +131,27 @@ static int evp_pkey_export_raw(EVP_PKEY* src, unsigned char** pub, if (priv != NULL) { *priv = NULL; *privLen = 0; } if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PUB_KEY, NULL, 0, - pubLen) != 1) return 0; + pubLen) != 1) { + return 0; + } *pub = OPENSSL_malloc(*pubLen); + if (*pub == NULL) { + return 0; + } if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PUB_KEY, *pub, - *pubLen, pubLen) != 1) return 0; + *pubLen, pubLen) != 1) { + OPENSSL_free(*pub); *pub = NULL; *pubLen = 0; + return 0; + } if (priv != NULL) { if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PRIV_KEY, NULL, 0, privLen) == 1) { *priv = OPENSSL_malloc(*privLen); + if (*priv == NULL) { + OPENSSL_free(*pub); *pub = NULL; *pubLen = 0; + *privLen = 0; + return 0; + } if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PRIV_KEY, *priv, *privLen, privLen) != 1) { OPENSSL_free(*priv); *priv = NULL; *privLen = 0; @@ -205,7 +218,12 @@ static int evp_encap(OSSL_LIB_CTX* lib, EVP_PKEY* k, unsigned char** ct, if (!e || EVP_PKEY_encapsulate_init(e, NULL) != 1) goto end; if (EVP_PKEY_encapsulate(e, NULL, ctLen, NULL, ssLen) != 1) goto end; *ct = OPENSSL_malloc(*ctLen); + if (*ct == NULL) goto end; ok = (EVP_PKEY_encapsulate(e, *ct, ctLen, ss, ssLen) == 1); + if (!ok) { + OPENSSL_free(*ct); + *ct = NULL; + } end: EVP_PKEY_CTX_free(e); return ok; @@ -240,12 +258,18 @@ static int wc_mlkem_encap_direct(const char* alg, const unsigned char* pub, if (rc != 0) { wc_MlKemKey_Free(&key); return 0; } rc = wc_MlKemKey_CipherTextSize(&key, &ctSize); if (rc != 0) { wc_MlKemKey_Free(&key); return 0; } + if (ssCap < WC_ML_KEM_SS_SZ) { wc_MlKemKey_Free(&key); return 0; } *ct = OPENSSL_malloc(ctSize); + if (*ct == NULL) { wc_MlKemKey_Free(&key); return 0; } *ctLen = ctSize; - if (ssCap < WC_ML_KEM_SS_SZ) { wc_MlKemKey_Free(&key); return 0; } rc = wc_MlKemKey_Encapsulate(&key, *ct, ss, &g_rng); wc_MlKemKey_Free(&key); - return rc == 0; + if (rc != 0) { + OPENSSL_free(*ct); + *ct = NULL; + return 0; + } + return 1; } /* wolfSSL-direct decapsulate. */ @@ -284,10 +308,16 @@ static int evp_sign(OSSL_LIB_CTX* lib, EVP_PKEY* k, const unsigned char* msg, { int ok = 0; EVP_MD_CTX* s = EVP_MD_CTX_new(); + if (s == NULL) return 0; if (EVP_DigestSignInit_ex(s, NULL, NULL, lib, NULL, k, NULL) != 1) goto end; if (EVP_DigestSign(s, NULL, sigLen, msg, msgLen) != 1) goto end; *sig = OPENSSL_malloc(*sigLen); + if (*sig == NULL) goto end; ok = (EVP_DigestSign(s, *sig, sigLen, msg, msgLen) == 1); + if (!ok) { + OPENSSL_free(*sig); + *sig = NULL; + } end: EVP_MD_CTX_free(s); return ok; @@ -298,6 +328,7 @@ static int evp_verify(OSSL_LIB_CTX* lib, EVP_PKEY* k, const unsigned char* msg, { int ok = 0; EVP_MD_CTX* v = EVP_MD_CTX_new(); + if (v == NULL) return 0; if (EVP_DigestVerifyInit_ex(v, NULL, NULL, lib, NULL, k, NULL) != 1) goto end; ok = (EVP_DigestVerify(v, sig, sigLen, msg, msgLen) == 1); @@ -327,6 +358,7 @@ static int wc_mldsa_sign_direct(const char* alg, const unsigned char* priv, sigSz = wc_dilithium_sig_size(&key); if (sigSz <= 0) { wc_dilithium_free(&key); return 0; } *sig = OPENSSL_malloc(sigSz); + if (*sig == NULL) { wc_dilithium_free(&key); return 0; } outLen = (word32)sigSz; rc = wc_dilithium_sign_ctx_msg(NULL, 0, msg, (word32)msgLen, *sig, &outLen, &key, &g_rng); From 618ad0ab9bb33085637e396eb09e62e4b4d492cf Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 23:47:09 -0700 Subject: [PATCH 10/16] Document ML-KEM and ML-DSA support in README and integration guide --- ChangeLog.md | 1 + README.md | 7 +++++ docs/INTEGRATION_GUIDE.md | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index d1d3d527..6e1ecfa2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,7 @@ PR stands for Pull Request, and PR references a GitHub pull request number where the code change was added. ## New Feature Additions +* Add ML-KEM (FIPS 203) and ML-DSA (FIPS 204) post-quantum algorithm support via `--enable-pqc` (PR 399) * Add OpenSSL FIPS baseline process implementation (PR 357) * Add seed-src handling for wolfProvider (PR 350) * Add EC public key auto derivation from private key (PR 338) diff --git a/README.md b/README.md index 38849669..10ccfbfd 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,13 @@ Information on how to configure, build, and test wolfProvider can be found here: * X25519, X448 (key exchange) * Ed25519, Ed448 (signatures) +### Post-Quantum (NIST FIPS 203 / 204) +Requires wolfSSL master (post-v5.9.1-stable) and OpenSSL 3.5+ for native +default-provider interop. Opt in with `./scripts/build-wolfprovider.sh --enable-pqc`. + +* ML-KEM (FIPS 203) — ML-KEM-512, ML-KEM-768, ML-KEM-1024 (key encapsulation) +* ML-DSA (FIPS 204) — ML-DSA-44, ML-DSA-65, ML-DSA-87 (signatures, pure mode with empty context per FIPS 204 sec 5.2) + ## Support diff --git a/docs/INTEGRATION_GUIDE.md b/docs/INTEGRATION_GUIDE.md index 3cc58c7d..713cdb46 100644 --- a/docs/INTEGRATION_GUIDE.md +++ b/docs/INTEGRATION_GUIDE.md @@ -30,6 +30,7 @@ This retrieves dependencies (OpenSSL and wolfSSL) and compiles them as necessary | `--openssl-dir=/path` | Use existing OpenSSL installation | | `--replace-default` | Make wolfProvider the default provider | | `--enable-replace-default-testing` | Enable unit testing with replace-default | +| `--enable-pqc` | Enable ML-KEM and ML-DSA post-quantum algorithms (adds `--enable-mlkem --enable-dilithium --enable-experimental` to wolfSSL). Requires wolfSSL post-v5.9.1-stable. | **Examples:** @@ -82,6 +83,7 @@ sudo make install | `--enable-pwdbased` | PKCS#12 support | | `--enable-hmac-copy` | Faster repeated HMAC with same key (wolfSSL 5.7.8+) | | `--enable-sp=yes,asm --enable-sp-math-all` | SP Integer maths | +| `--enable-mlkem --enable-dilithium --enable-experimental` | ML-KEM and ML-DSA post-quantum algorithms (wolfSSL post-v5.9.1-stable). The `build-wolfprovider.sh --enable-pqc` flag sets these automatically. | **Optional CPPFLAGS:** @@ -151,6 +153,58 @@ This makes replace default mode useful for testing scenarios where you want to e --- +## Post-Quantum Cryptography (ML-KEM and ML-DSA) + +wolfProvider supports NIST's post-quantum algorithms via the wolfSSL backend: + +| Algorithm | Standard | Parameter Sets | +|-----------|----------|----------------| +| ML-KEM (key encapsulation) | FIPS 203 | ML-KEM-512, ML-KEM-768, ML-KEM-1024 | +| ML-DSA (digital signature) | FIPS 204 | ML-DSA-44, ML-DSA-65, ML-DSA-87 | + +ML-DSA uses pure mode with an empty context string (FIPS 204 sec 5.2, Algorithm 22) — interoperable with OpenSSL 3.5+'s native ML-DSA. + +### Requirements + +- **wolfSSL**: post-v5.9.1-stable (i.e. v5.9.2-stable or master). Older releases lack the `wc_MlDsaKey_*` and `wc_dilithium_sign_ctx_msg` API surface that wolfProvider's PQC code uses. +- **OpenSSL**: any 3.x. OpenSSL 3.5+ is required only for cross-provider interop against its native ML-KEM/ML-DSA implementations. + +### Building with PQC + +```bash +./scripts/build-wolfprovider.sh --enable-pqc +``` + +This adds `--enable-mlkem --enable-dilithium --enable-experimental` to the wolfSSL configure step. wolfProvider auto-detects the resulting `WOLFSSL_HAVE_MLKEM` / `HAVE_DILITHIUM` macros via `include/wolfprovider/settings.h` (gated on `__has_include` of the corresponding wolfSSL headers) and registers the six PQC algorithms. + +### Usage Example + +```bash +# Generate an ML-DSA-65 key with wolfProvider +OPENSSL_CONF=provider.conf openssl genpkey -algorithm ML-DSA-65 -out key.pem + +# Sign and verify with ML-DSA-65 +OPENSSL_CONF=provider.conf openssl pkeyutl -sign -inkey key.pem -in msg.bin -out sig.bin +OPENSSL_CONF=provider.conf openssl pkeyutl -verify -pubin -inkey pub.pem -sigfile sig.bin -in msg.bin +``` + +The OpenSSL CLI can also enumerate available algorithms: + +```bash +OPENSSL_CONF=provider.conf openssl list -kem-algorithms -provider libwolfprov +OPENSSL_CONF=provider.conf openssl list -signature-algorithms -provider libwolfprov +``` + +### Validation + +A standalone three-way interop validator (`test/pqc_interop.test`) cross-checks every ML-KEM / ML-DSA combination against: +- OpenSSL 3.5+'s native default provider +- wolfSSL's `wc_*` APIs directly (no provider abstraction) + +This proves wolfProvider's raw-key, ciphertext, and signature bytes are FIPS 203 / 204 standards-compliant. The CI workflow `.github/workflows/wolfssl-versions-pqc.yml` runs this validator on every PR, plus a backward-compatibility build against pre-PQC wolfSSL to verify the no-symbol path still builds cleanly. + +--- + ## Testing ### Unit Tests From 39e677cf5169e709ad49fe880a3e453307129d42 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 23 May 2026 00:12:03 -0700 Subject: [PATCH 11/16] Address Skoll review: input validation, consistency checks, dup selection honoring --- include/wolfprovider/alg_funcs.h | 4 +- src/wp_mldsa_kmgmt.c | 76 +++++++++++++++++++-- src/wp_mldsa_sig.c | 20 +++++- src/wp_mlkem_kem.c | 14 ++-- src/wp_mlkem_kmgmt.c | 112 +++++++++++++++++++++---------- src/wp_wolfprov.c | 12 ++-- 6 files changed, 188 insertions(+), 50 deletions(-) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 74c7fcfd..264d6ac8 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -241,7 +241,9 @@ void wp_mlkem_free(wp_MlKem* mlkem); void* wp_mlkem_get_key(wp_MlKem* mlkem); const wp_MlKemData* wp_mlkem_get_data(const wp_MlKem* mlkem); word32 wp_mlkem_data_ct_size(const wp_MlKemData* data); -word32 wp_mlkem_data_ss_size(const wp_MlKemData* data); +/* ML-KEM shared secret size is a FIPS 203 constant (32 bytes) independent + * of the parameter set. */ +#define WP_MLKEM_SS_SIZE 32 /* Internal ML-DSA types and functions. */ typedef struct wp_MlDsa wp_MlDsa; diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 0c80a648..21c13a92 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -290,19 +290,22 @@ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) word32 privLen; int rc; int ok = 1; - - (void)selection; + int dupPub; + int dupPriv; if (!wolfssl_prov_is_running() || (src == NULL)) { return NULL; } + dupPub = ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) && src->hasPub; + dupPriv = ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) + && src->hasPriv; dst = wp_mldsa_new(src->provCtx, src->data); if (dst == NULL) { return NULL; } - if (src->hasPub) { + if (dupPub) { pubLen = src->data->pubKeySize; pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); if (pubBuf == NULL) { @@ -328,7 +331,7 @@ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) pubBuf = NULL; } - if (ok && src->hasPriv) { + if (ok && dupPriv) { privLen = src->data->privKeySize; privBuf = (unsigned char*)OPENSSL_malloc(privLen); if (privBuf == NULL) { @@ -449,6 +452,34 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) } OPENSSL_free(bufA); OPENSSL_free(bufB); + bufA = NULL; + bufB = NULL; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + lenA = a->data->privKeySize; + lenB = b->data->privKeySize; + bufA = (unsigned char*)OPENSSL_malloc(lenA); + bufB = (unsigned char*)OPENSSL_malloc(lenB); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_dilithium_export_private((MlDsaKey*)&a->key, bufA, &lenA); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_dilithium_export_private((MlDsaKey*)&b->key, bufB, &lenB); + if (rc != 0) { + ok = 0; + } + } + if (ok && ((lenA != lenB) || (XMEMCMP(bufA, bufB, lenA) != 0))) { + ok = 0; + } + OPENSSL_clear_free(bufA, lenA); + OPENSSL_clear_free(bufB, lenB); } return ok; } @@ -470,6 +501,8 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, unsigned char* pubData = NULL; size_t privLen = 0; size_t pubLen = 0; + unsigned char* derivedPub = NULL; + word32 derivedPubLen = 0; if (!wolfssl_prov_is_running() || (mldsa == NULL)) { ok = 0; @@ -490,6 +523,18 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, } if (ok) { mldsa->hasPriv = 1; + /* FIPS 204 raw private key embeds the public seed; probe + * whether wolfSSL populated the public portion as a side + * effect of import_private. If so, set hasPub so downstream + * tools (e.g. openssl pkey -in priv.der) can emit SPKI. */ + derivedPubLen = mldsa->data->pubKeySize; + derivedPub = (unsigned char*)OPENSSL_malloc(derivedPubLen); + if (derivedPub != NULL) { + if (wc_dilithium_export_public(&mldsa->key, derivedPub, + &derivedPubLen) == 0) { + mldsa->hasPub = 1; + } + } } } } @@ -498,6 +543,16 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, &pubData, &pubLen)) { ok = 0; } + /* Consistency check: if both priv and pub were supplied AND priv + * import gave us a derived pub, the supplied pub must match. + * Rejects attacker-supplied or corrupted mismatched keypairs. */ + if (ok && (pubData != NULL) && (privData != NULL) + && (derivedPub != NULL) && mldsa->hasPub) { + if ((derivedPubLen != pubLen) || + (XMEMCMP(derivedPub, pubData, pubLen) != 0)) { + ok = 0; + } + } if (ok && (pubData != NULL)) { rc = wc_dilithium_import_public(pubData, (word32)pubLen, &mldsa->key); @@ -512,6 +567,7 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, if (ok && (privData == NULL) && (pubData == NULL)) { ok = 0; } + OPENSSL_free(derivedPub); return ok; } @@ -662,6 +718,10 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) int rc; OSSL_PARAM* p; + if (mldsa == NULL) { + return 0; + } + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); if ((p != NULL) && !OSSL_PARAM_set_int(p, (int)mldsa->data->pubKeySize * 8)) { @@ -704,6 +764,10 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) } } } + else { + /* Buffer supplied but no public key available. */ + p->return_size = 0; + } } } if (ok) { @@ -729,6 +793,10 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) } } } + else { + /* Buffer supplied but no private key available. */ + p->return_size = 0; + } } } return ok; diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 10e3773b..9cd4f4f7 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -62,6 +62,11 @@ typedef struct wp_MlDsaSigCtx { * @param [in] dataLen Length of data in bytes. * @return 1 on success, 0 on failure. */ +/* Upper bound on the accumulated message buffer (64 MiB). ML-DSA messages + * are typically small (handshake transcripts, certificates); a cap prevents + * a hostile caller from driving OOM via unbounded digest_sign_update. */ +#define WP_MLDSA_BUF_MAX (64UL * 1024UL * 1024UL) + static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, size_t dataLen) { @@ -73,6 +78,9 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, if (needed < ctx->mdLen) { ok = 0; } + if (ok && (needed > WP_MLDSA_BUF_MAX)) { + ok = 0; + } if (ok && (needed > ctx->mdCap)) { size_t newCap = ctx->mdCap == 0 ? 256 : ctx->mdCap; while (newCap < needed) { @@ -165,7 +173,7 @@ static wp_MlDsaSigCtx* wp_mldsa_dupctx(wp_MlDsaSigCtx* srcCtx) { wp_MlDsaSigCtx* dstCtx = NULL; - if (!wolfssl_prov_is_running()) { + if ((!wolfssl_prov_is_running()) || (srcCtx == NULL)) { return NULL; } @@ -265,6 +273,11 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, if (*sigLen < sigSz) { ok = 0; } + /* wolfSSL's dilithium API takes a 32-bit message length. Reject >4 GiB + * messages explicitly rather than silently truncating. */ + if (ok && (msgLen > 0xFFFFFFFFU)) { + ok = 0; + } if (ok) { word32 outLen = sigSz; /* FIPS 204 sec 5.2 (Algorithm 22): pure ML-DSA prepends 0x00, ctxLen, @@ -302,6 +315,11 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, if ((ctx == NULL) || (ctx->mldsa == NULL)) { return 0; } + /* wolfSSL's dilithium API takes 32-bit lengths. Reject oversize inputs + * explicitly rather than silently truncating. */ + if ((sigLen > 0xFFFFFFFFU) || (msgLen > 0xFFFFFFFFU)) { + return 0; + } /* Match the sign path: FIPS 204 pure ML-DSA with empty context. */ rc = wc_dilithium_verify_ctx_msg(sig, (word32)sigLen, NULL, 0, msg, diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index fca165eb..07b5b60e 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -95,7 +95,7 @@ static wp_MlKemCtx* wp_mlkem_kem_dupctx(wp_MlKemCtx* srcCtx) { wp_MlKemCtx* dstCtx = NULL; - if (!wolfssl_prov_is_running()) { + if ((!wolfssl_prov_is_running()) || (srcCtx == NULL)) { return NULL; } @@ -179,9 +179,12 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, data = wp_mlkem_get_data(ctx->mlkem); ctSize = wp_mlkem_data_ct_size(data); - ssSize = wp_mlkem_data_ss_size(data); + ssSize = WP_MLKEM_SS_SIZE; - if ((out == NULL) || (secret == NULL)) { + /* Size-only query: out == NULL with outLen/secretLen set per OpenSSL + * KEM encapsulate contract. Mixed-NULL is a caller bug, not a size + * query, so reject it explicitly. */ + if (out == NULL) { if (outLen != NULL) { *outLen = ctSize; } @@ -190,6 +193,9 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, } return 1; } + if (secret == NULL) { + return 0; + } if (ok && (*outLen < ctSize)) { ok = 0; @@ -236,7 +242,7 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, } data = wp_mlkem_get_data(ctx->mlkem); - ssSize = wp_mlkem_data_ss_size(data); + ssSize = WP_MLKEM_SS_SIZE; ctSize = wp_mlkem_data_ct_size(data); if (out == NULL) { diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index 753bda8d..ef298746 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -187,18 +187,6 @@ word32 wp_mlkem_data_ct_size(const wp_MlKemData* data) return data->ctSize; } -/** - * Get the shared secret size for ML-KEM (constant 32 bytes). - * - * @param [in] data Parameter set data. Unused. - * @return Shared secret size in bytes. - */ -word32 wp_mlkem_data_ss_size(const wp_MlKemData* data) -{ - (void)data; - return WC_ML_KEM_SS_SZ; -} - /** * Create a new ML-KEM key object. * @@ -287,8 +275,14 @@ static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) wp_MlKem* dst = NULL; unsigned char* pubBuf = NULL; unsigned char* privBuf = NULL; - - (void)selection; + word32 pubLen; + word32 privLen; + int rc; + int ok = 1; + int dupPub = ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) + && src != NULL && src->hasPub; + int dupPriv = ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) + && src != NULL && src->hasPriv; if (!wolfssl_prov_is_running() || (src == NULL)) { return NULL; @@ -299,11 +293,8 @@ static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) return NULL; } - if (src->hasPub) { - int ok = 1; - word32 pubLen = src->data->pubKeySize; - int rc; - + if (dupPub) { + pubLen = src->data->pubKeySize; pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); if (pubBuf == NULL) { ok = 0; @@ -324,19 +315,12 @@ static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) if (ok) { dst->hasPub = 1; } - if (!ok) { - OPENSSL_free(pubBuf); - wp_mlkem_free(dst); - return NULL; - } OPENSSL_free(pubBuf); + pubBuf = NULL; } - if (src->hasPriv) { - int ok = 1; - word32 privLen = src->data->privKeySize; - int rc; - + if (ok && dupPriv) { + privLen = src->data->privKeySize; privBuf = (unsigned char*)OPENSSL_malloc(privLen); if (privBuf == NULL) { ok = 0; @@ -358,12 +342,12 @@ static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) dst->hasPriv = 1; } OPENSSL_clear_free(privBuf, privLen); - if (!ok) { - wp_mlkem_free(dst); - return NULL; - } } + if (!ok) { + wp_mlkem_free(dst); + return NULL; + } return dst; } @@ -459,6 +443,32 @@ static int wp_mlkem_match(const wp_MlKem* a, const wp_MlKem* b, int selection) bufA = NULL; bufB = NULL; } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + lenA = a->data->privKeySize; + lenB = b->data->privKeySize; + bufA = (unsigned char*)OPENSSL_malloc(lenA); + bufB = (unsigned char*)OPENSSL_malloc(lenB); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePrivateKey((MlKemKey*)&a->key, bufA, lenA); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_MlKemKey_EncodePrivateKey((MlKemKey*)&b->key, bufB, lenB); + if (rc != 0) { + ok = 0; + } + } + if (ok && ((lenA != lenB) || (XMEMCMP(bufA, bufB, lenA) != 0))) { + ok = 0; + } + OPENSSL_clear_free(bufA, lenA); + OPENSSL_clear_free(bufB, lenB); + } return ok; } @@ -479,6 +489,8 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, unsigned char* pubData = NULL; size_t privLen = 0; size_t pubLen = 0; + unsigned char* derivedPub = NULL; + word32 derivedPubLen = 0; if (!wolfssl_prov_is_running() || (mlkem == NULL)) { ok = 0; @@ -499,7 +511,16 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, } if (ok) { mlkem->hasPriv = 1; - mlkem->hasPub = 1; + /* Probe whether private-key import gave us the public part + * (FIPS 203 private keys embed the public component). */ + derivedPubLen = mlkem->data->pubKeySize; + derivedPub = (unsigned char*)OPENSSL_malloc(derivedPubLen); + if (derivedPub != NULL) { + if (wc_MlKemKey_EncodePublicKey(&mlkem->key, derivedPub, + derivedPubLen) == 0) { + mlkem->hasPub = 1; + } + } } } } @@ -508,6 +529,16 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, &pubData, &pubLen)) { ok = 0; } + /* Consistency check: if both priv and pub were supplied AND priv + * import gave us a derived pub, the supplied pub must match. + * Rejects attacker-supplied or corrupted mismatched keypairs. */ + if (ok && (pubData != NULL) && (privData != NULL) + && (derivedPub != NULL) && mlkem->hasPub) { + if ((derivedPubLen != pubLen) || + (XMEMCMP(derivedPub, pubData, pubLen) != 0)) { + ok = 0; + } + } if (ok && (pubData != NULL)) { rc = wc_MlKemKey_DecodePublicKey(&mlkem->key, pubData, (word32)pubLen); @@ -522,6 +553,7 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, if (ok && (privData == NULL) && (pubData == NULL)) { ok = 0; } + OPENSSL_free(derivedPub); return ok; } @@ -672,6 +704,10 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) int rc; OSSL_PARAM* p; + if (mlkem == NULL) { + return 0; + } + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); if ((p != NULL) && !OSSL_PARAM_set_int(p, (int)mlkem->data->pubKeySize * 8)) { ok = 0; @@ -712,6 +748,10 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) } } } + else { + /* Buffer supplied but no public key available. */ + p->return_size = 0; + } } } if (ok) { @@ -736,6 +776,10 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) } } } + else { + /* Buffer supplied but no private key available. */ + p->return_size = 0; + } } } return ok; diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index 60a57f45..f9f4a708 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -665,19 +665,19 @@ static const OSSL_ALGORITHM wolfprov_keymgmt[] = { #ifdef WP_HAVE_MLKEM { WP_NAMES_ML_KEM_512, WOLFPROV_PROPERTIES, - wp_mlkem512_keymgmt_functions, "ML-KEM-512" }, + wp_mlkem512_keymgmt_functions, "" }, { WP_NAMES_ML_KEM_768, WOLFPROV_PROPERTIES, - wp_mlkem768_keymgmt_functions, "ML-KEM-768" }, + wp_mlkem768_keymgmt_functions, "" }, { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, - wp_mlkem1024_keymgmt_functions, "ML-KEM-1024" }, + wp_mlkem1024_keymgmt_functions, "" }, #endif #ifdef WP_HAVE_MLDSA { WP_NAMES_ML_DSA_44, WOLFPROV_PROPERTIES, - wp_mldsa44_keymgmt_functions, "ML-DSA-44" }, + wp_mldsa44_keymgmt_functions, "" }, { WP_NAMES_ML_DSA_65, WOLFPROV_PROPERTIES, - wp_mldsa65_keymgmt_functions, "ML-DSA-65" }, + wp_mldsa65_keymgmt_functions, "" }, { WP_NAMES_ML_DSA_87, WOLFPROV_PROPERTIES, - wp_mldsa87_keymgmt_functions, "ML-DSA-87" }, + wp_mldsa87_keymgmt_functions, "" }, #endif { NULL, NULL, NULL, NULL } From ef9ac48ab8fa2fc0617dd6a5548b0001274e5ae6 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 23 May 2026 00:13:21 -0700 Subject: [PATCH 12/16] Run PQC version matrix on draft PRs too (match wolfTPM behavior) --- .github/workflows/wolfssl-versions-pqc.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index a8e864f9..7c7c651f 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -17,7 +17,6 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] - types: [opened, synchronize, reopened, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -77,7 +76,6 @@ jobs: pqc-build-test: name: ${{ matrix.name }} needs: discover-versions - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-22.04 timeout-minutes: 30 strategy: From ed5814202d5764f2abe6239c173c313b8054d1ca Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 23 May 2026 00:27:49 -0700 Subject: [PATCH 13/16] Use wc_mlkem.h (mlkem.h removed on wolfssl master); drop absence check --- .github/workflows/wolfssl-versions-pqc.yml | 28 +++++++++---------- include/wolfprovider/settings.h | 5 ++-- src/wp_mlkem_kem.c | 2 +- src/wp_mlkem_kmgmt.c | 1 - .../tests/pqc_interop/test_pqc_interop.c | 1 - test/test_mlkem.c | 18 ++++++------ 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 7c7c651f..92156e58 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -104,22 +104,20 @@ jobs: ./scripts/build-wolfprovider.sh fi - - name: Confirm PQC tests present (or absent) as expected + # On PQC-enabled rows the PQC tests must be present. We do NOT assert + # absence on the no-PQC rows because v5.9.x's --enable-all-crypto now + # auto-enables MLKEM/DILITHIUM, so the "latest stable" row will pick up + # PQC at the wolfSSL level even without --enable-pqc. wolfProvider + # auto-detects and compiles in the PQC code in that case, which is fine. + - name: Confirm PQC tests present on PQC-enabled rows + if: matrix.pqc == true run: | - if [ "${{ matrix.pqc }}" = "true" ]; then - ./test/unit.test --list | grep -q 'test_mlkem_keygen' \ - || { echo 'ERROR: PQC tests missing in PQC-enabled build'; \ - exit 1; } - ./test/unit.test --list | grep -q 'test_mldsa_sign_verify' \ - || { echo 'ERROR: ML-DSA tests missing in PQC-enabled build'; \ - exit 1; } - else - if ./test/unit.test --list | grep -qE 'test_mlkem|test_mldsa'; \ - then - echo 'ERROR: PQC tests present in pre-PQC build (should skip)' - exit 1 - fi - fi + ./test/unit.test --list | grep -q 'test_mlkem_keygen' \ + || { echo 'ERROR: PQC tests missing in PQC-enabled build'; \ + exit 1; } + ./test/unit.test --list | grep -q 'test_mldsa_sign_verify' \ + || { echo 'ERROR: ML-DSA tests missing in PQC-enabled build'; \ + exit 1; } # Three-way interop: wolfProvider <-> OpenSSL default <-> wolfSSL direct. # Only runs on PQC-enabled rows; OpenSSL 3.5+ has native ML-KEM/ML-DSA diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index a1dc4b37..c1b7d8b6 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -175,8 +175,9 @@ * are not installed, so probe the headers too. */ #ifdef WOLFSSL_HAVE_MLKEM #if defined(__has_include) - #if __has_include() && \ - __has_include() + /* wc_mlkem.h is present in both v5.9.1-stable (alongside mlkem.h) + * and on master (where mlkem.h was removed). Probe wc_mlkem.h only. */ + #if __has_include() #define WP_HAVE_MLKEM #define WP_HAVE_ML_KEM_512 #define WP_HAVE_ML_KEM_768 diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index 07b5b60e..07e3288a 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -30,7 +30,7 @@ #ifdef WP_HAVE_MLKEM -#include +#include /** * ML-KEM KEM context. diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index ef298746..6892450f 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -31,7 +31,6 @@ #ifdef WP_HAVE_MLKEM -#include #include /** Supported selections (key parts) in this key manager for ML-KEM. */ diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index 24e39886..6ff1e337 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -58,7 +58,6 @@ #if defined(WP_HAVE_MLKEM) && defined(WP_HAVE_MLDSA) -#include #include #include #include diff --git a/test/test_mlkem.c b/test/test_mlkem.c index 091e70c9..d336f2f0 100644 --- a/test/test_mlkem.c +++ b/test/test_mlkem.c @@ -25,7 +25,7 @@ #ifdef WP_HAVE_MLKEM -#include +#include /* Per-level metadata. */ typedef struct mlkem_test_level { @@ -53,7 +53,7 @@ static const mlkem_test_level mlkem_levels[] = { * @param [out] pkey Generated EVP_PKEY (caller frees). * @return 0 on success, non-zero on failure. */ -static int mlkem_keygen(const char* name, EVP_PKEY** pkey) +static int wp_test_mlkem_keygen(const char* name, EVP_PKEY** pkey) { int err = 0; EVP_PKEY_CTX* ctx = NULL; @@ -120,9 +120,9 @@ int test_mlkem_keygen(void* data) const mlkem_test_level* lvl = &mlkem_levels[i]; PRINT_MSG("Keygen %s", lvl->name); - err = mlkem_keygen(lvl->name, &pkey1); + err = wp_test_mlkem_keygen(lvl->name, &pkey1); if (err == 0) { - err = mlkem_keygen(lvl->name, &pkey2); + err = wp_test_mlkem_keygen(lvl->name, &pkey2); } if (err == 0) { err = mlkem_get_pub(pkey1, &pub1, &pub1Len); @@ -181,7 +181,7 @@ int test_mlkem_import_export_roundtrip(void* data) const mlkem_test_level* lvl = &mlkem_levels[i]; PRINT_MSG("Import/export roundtrip %s", lvl->name); - err = mlkem_keygen(lvl->name, &k1); + err = wp_test_mlkem_keygen(lvl->name, &k1); if (err == 0) { err = mlkem_get_pub(k1, &pub1, &pub1Len); } @@ -279,7 +279,7 @@ int test_mlkem_encap_decap(void* data) const mlkem_test_level* lvl = &mlkem_levels[i]; PRINT_MSG("Encap/Decap %s", lvl->name); - err = mlkem_keygen(lvl->name, &pkey); + err = wp_test_mlkem_keygen(lvl->name, &pkey); if (err == 0) { ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, pkey, NULL); @@ -357,7 +357,7 @@ int test_mlkem_decap_tampered_ct(void* data) const mlkem_test_level* lvl = &mlkem_levels[i]; PRINT_MSG("Decap tampered ct %s", lvl->name); - err = mlkem_keygen(lvl->name, &pkey); + err = wp_test_mlkem_keygen(lvl->name, &pkey); if (err == 0) { ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, pkey, NULL); err = (ectx == NULL); @@ -431,9 +431,9 @@ int test_mlkem_decap_wrong_key(void* data) const mlkem_test_level* lvl = &mlkem_levels[i]; PRINT_MSG("Decap wrong key %s", lvl->name); - err = mlkem_keygen(lvl->name, &keyA); + err = wp_test_mlkem_keygen(lvl->name, &keyA); if (err == 0) { - err = mlkem_keygen(lvl->name, &keyB); + err = wp_test_mlkem_keygen(lvl->name, &keyB); } if (err == 0) { ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, keyA, NULL); From 0b04e5a12456b26f232131993b6350a8d16174d0 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 23 May 2026 00:42:55 -0700 Subject: [PATCH 14/16] CI: diagnose OpenSSL default provider PQC support --- .github/workflows/wolfssl-versions-pqc.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 92156e58..35168188 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -123,6 +123,21 @@ jobs: # Only runs on PQC-enabled rows; OpenSSL 3.5+ has native ML-KEM/ML-DSA # in the default provider, so this proves wolfProvider's bytes are # FIPS 203/204 standards-compliant against two reference implementations. + - name: Diagnose OpenSSL default provider PQC support + if: matrix.pqc == true + run: | + export LD_LIBRARY_PATH="$(pwd)/openssl-install/lib" + echo "--- OpenSSL version ---" + ./openssl-install/bin/openssl version -a + echo "--- libraries linked into pqc_interop.test ---" + ldd ./test/pqc_interop.test | grep -E "libcrypto|libssl" || true + echo "--- default provider KEM algorithms ---" + ./openssl-install/bin/openssl list -kem-algorithms -provider default \ + | grep -i ml-kem || echo "NO ML-KEM in default provider" + echo "--- default provider signature algorithms ---" + ./openssl-install/bin/openssl list -signature-algorithms -provider default \ + | grep -i ml-dsa || echo "NO ML-DSA in default provider" + - name: Three-way PQC interop validation if: matrix.pqc == true run: | From 371c4e6a27d2aef5032cd699b9f5f2f04f3f25c8 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 23 May 2026 00:47:21 -0700 Subject: [PATCH 15/16] interop: use global lib ctx for default provider side (CI lib ctx fix) --- .../tests/pqc_interop/test_pqc_interop.c | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index 6ff1e337..b4e0df95 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -65,16 +65,19 @@ #define WP_NAME "libwolfprov" static OSSL_LIB_CTX* wp_ctx; -static OSSL_LIB_CTX* oss_ctx; +/* oss_ctx is NULL = use OpenSSL's global default library context. The global + * ctx auto-loads the default provider on first use, so we don't have to + * explicitly load it (which can run into per-ctx algorithm registration + * quirks across OpenSSL builds). wolfProvider stays in its own isolated + * wp_ctx with an explicit search path. */ +#define oss_ctx ((OSSL_LIB_CTX*)NULL) static OSSL_PROVIDER* wp_prov; -static OSSL_PROVIDER* def_prov; static WC_RNG g_rng; static int load_all(const char* wp_path) { wp_ctx = OSSL_LIB_CTX_new(); - oss_ctx = OSSL_LIB_CTX_new(); - if (wp_ctx == NULL || oss_ctx == NULL) return 0; + if (wp_ctx == NULL) return 0; OSSL_PROVIDER_set_default_search_path(wp_ctx, wp_path); wp_prov = OSSL_PROVIDER_load(wp_ctx, WP_NAME); @@ -83,9 +86,12 @@ static int load_all(const char* wp_path) ERR_print_errors_fp(stderr); return 0; } - def_prov = OSSL_PROVIDER_load(oss_ctx, "default"); - if (def_prov == NULL) { - fprintf(stderr, "Failed to load OpenSSL default provider\n"); + /* Sanity check: the global default provider should advertise ML-KEM-512 + * when running against OpenSSL 3.5+. Fail fast with a clear message if + * not (e.g. when the wrong libcrypto is loaded at runtime). */ + if (!OSSL_PROVIDER_available(NULL, "default")) { + fprintf(stderr, "OpenSSL default provider unavailable in global " + "context\n"); return 0; } if (wc_InitRng(&g_rng) != 0) { @@ -99,9 +105,7 @@ static void unload_all(void) { wc_FreeRng(&g_rng); if (wp_prov) OSSL_PROVIDER_unload(wp_prov); - if (def_prov) OSSL_PROVIDER_unload(def_prov); if (wp_ctx) OSSL_LIB_CTX_free(wp_ctx); - if (oss_ctx) OSSL_LIB_CTX_free(oss_ctx); } /* Map "ML-KEM-512/768/1024" to wolfSSL type enum. */ From f69c064a24b6f37f56109174315814a813a088ce Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 23 May 2026 00:56:21 -0700 Subject: [PATCH 16/16] CI: include lib64 in LD_LIBRARY_PATH so Linux finds the local libcrypto --- .github/workflows/wolfssl-versions-pqc.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 35168188..1fb6d5cc 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -123,25 +123,14 @@ jobs: # Only runs on PQC-enabled rows; OpenSSL 3.5+ has native ML-KEM/ML-DSA # in the default provider, so this proves wolfProvider's bytes are # FIPS 203/204 standards-compliant against two reference implementations. - - name: Diagnose OpenSSL default provider PQC support - if: matrix.pqc == true - run: | - export LD_LIBRARY_PATH="$(pwd)/openssl-install/lib" - echo "--- OpenSSL version ---" - ./openssl-install/bin/openssl version -a - echo "--- libraries linked into pqc_interop.test ---" - ldd ./test/pqc_interop.test | grep -E "libcrypto|libssl" || true - echo "--- default provider KEM algorithms ---" - ./openssl-install/bin/openssl list -kem-algorithms -provider default \ - | grep -i ml-kem || echo "NO ML-KEM in default provider" - echo "--- default provider signature algorithms ---" - ./openssl-install/bin/openssl list -signature-algorithms -provider default \ - | grep -i ml-dsa || echo "NO ML-DSA in default provider" - + # Linux x86_64 OpenSSL installs to lib64 by default; LD_LIBRARY_PATH + # must include both lib and lib64 or the dynamic linker falls through + # to the system libcrypto/libssl (Ubuntu 22.04 ships 3.0.2, which has + # no ML-KEM/ML-DSA in the default provider). - name: Three-way PQC interop validation if: matrix.pqc == true run: | - LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib" \ + LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" \ ./test/pqc_interop.test - name: Print errors on failure