From aad005da85c72c9b4bafd403ab6c0ea7f4c5b030 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 22 May 2026 22:08:36 -0700 Subject: [PATCH 01/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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 From c2bd794e1d97e18afda5ebbd325e6386ec6fdf2b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 29 May 2026 16:38:21 -0700 Subject: [PATCH 17/43] PQC: canonical ML-KEM/ML-DSA APIs + address review feedback --- .github/workflows/wolfssl-versions-pqc.yml | 2 +- README.md | 4 +- docs/INTEGRATION_GUIDE.md | 10 +- include/wolfprovider/alg_funcs.h | 1 - include/wolfprovider/settings.h | 16 +- scripts/build-wolfprovider.sh | 2 +- scripts/utils-wolfssl.sh | 6 +- src/wp_mldsa_kmgmt.c | 205 +++++----- src/wp_mldsa_sig.c | 57 +-- src/wp_mlkem_kem.c | 23 +- src/wp_mlkem_kmgmt.c | 101 ++--- .../tests/pqc_interop/test_pqc_interop.c | 83 ++-- test/test_mldsa.c | 373 +++++++++++++++++- test/test_mlkem.c | 300 +++++++++++++- test/unit.c | 12 + test/unit.h | 12 + 16 files changed, 974 insertions(+), 233 deletions(-) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 1fb6d5cc..212b4597 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -3,7 +3,7 @@ name: wolfSSL Versions (PQC) # 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 +# floor, then the build job runs three rows: pre-PQC floor, dynamically # resolved latest -stable, and master. # # PQC_FLOOR is v5.9.1-stable: the wc_MlDsaKey_* / wc_dilithium_sign_ctx_msg diff --git a/README.md b/README.md index 10ccfbfd..49f677d7 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ Information on how to configure, build, and test wolfProvider can be found here: 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) +* 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 713cdb46..ee90c561 100644 --- a/docs/INTEGRATION_GUIDE.md +++ b/docs/INTEGRATION_GUIDE.md @@ -30,7 +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. | +| `--enable-pqc` | Enable ML-KEM and ML-DSA post-quantum algorithms (adds `--enable-mlkem --enable-mldsa` to wolfSSL). Requires wolfSSL post-v5.9.1-stable. | **Examples:** @@ -83,7 +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. | +| `--enable-mlkem --enable-mldsa` | ML-KEM and ML-DSA post-quantum algorithms (wolfSSL post-v5.9.1-stable). The `build-wolfprovider.sh --enable-pqc` flag sets these automatically. Neither algorithm requires `--enable-experimental`. | **Optional CPPFLAGS:** @@ -162,11 +162,11 @@ wolfProvider supports NIST's post-quantum algorithms via the wolfSSL backend: | 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. +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. +- **wolfSSL**: post-v5.9.1-stable (i.e. v5.9.2-stable or master). v5.9.1-stable defines `HAVE_DILITHIUM` and exposes `wc_dilithium_sign_ctx_msg` (the older name for the FIPS 204 pure-mode signer) but does not yet ship the canonical `WOLFSSL_HAVE_MLDSA` macro, `` header, or `wc_MlDsaKey_SignCtx` alias that wolfProvider gates on. - **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 @@ -175,7 +175,7 @@ ML-DSA uses pure mode with an empty context string (FIPS 204 sec 5.2, Algorithm ./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. +This adds `--enable-mlkem --enable-mldsa` to the wolfSSL configure step (neither flag requires `--enable-experimental`). wolfProvider auto-detects the resulting `WOLFSSL_HAVE_MLKEM` / `WOLFSSL_HAVE_MLDSA` macros via `include/wolfprovider/settings.h` (gated on `__has_include` of `` / ``) and registers the six PQC algorithms. ### Usage Example diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 264d6ac8..7133595d 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -251,7 +251,6 @@ 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. */ diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index c1b7d8b6..c21dd817 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -169,14 +169,14 @@ #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. */ +/* PQC: gate on both the wolfSSL feature macro AND header availability. The + * canonical post-rename names (WOLFSSL_HAVE_MLKEM / WOLFSSL_HAVE_MLDSA and + * wc_mlkem.h / wc_mldsa.h) are required. Older wolfSSL releases that only + * exposed the pre-standardization names (HAVE_DILITHIUM, dilithium.h) are + * intentionally treated as PQC-absent here so that wolfProvider only ever + * builds against the canonical FIPS 203 / FIPS 204 surface. */ #ifdef WOLFSSL_HAVE_MLKEM #if defined(__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 @@ -190,9 +190,9 @@ #define WP_HAVE_ML_KEM_1024 #endif #endif -#ifdef HAVE_DILITHIUM +#ifdef WOLFSSL_HAVE_MLDSA #if defined(__has_include) - #if __has_include() + #if __has_include() #define WP_HAVE_MLDSA #define WP_HAVE_ML_DSA_44 #define WP_HAVE_ML_DSA_65 diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index b4448c9f..3a1611b1 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -33,7 +33,7 @@ show_help() { 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 " Adds --enable-mlkem --enable-mldsa to wolfSSL configure." echo "" echo "Environment Variables:" echo " OPENSSL_TAG OpenSSL tag to use (e.g., openssl-3.5.0)" diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 16c7c813..c1be940c 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -38,9 +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 +# Enable ML-KEM and ML-DSA in wolfSSL when --enable-pqc is requested. +# Use the canonical FIPS 203 / FIPS 204 flag names. Neither algorithm +# requires --enable-experimental anymore. if [ "$WOLFPROV_PQC" = "1" ]; then - WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mlkem --enable-dilithium --enable-experimental" + WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mlkem --enable-mldsa" fi WOLFSSL_DEBUG_ASN_TEMPLATE=${DWOLFSSL_DEBUG_ASN_TEMPLATE:-0} diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 21c13a92..1d0babf8 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -1,6 +1,6 @@ /* wp_mldsa_kmgmt.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -31,7 +31,7 @@ #ifdef WP_HAVE_MLDSA -#include +#include /** Supported selections (key parts) in this key manager for ML-DSA. */ #define WP_MLDSA_POSSIBLE_SELECTIONS \ @@ -60,7 +60,7 @@ typedef struct wp_MlDsaData { */ struct wp_MlDsa { /** wolfSSL ML-DSA key. */ - MlDsaKey key; + wc_MlDsaKey key; /** Parameter set data. */ const wp_MlDsaData* data; @@ -157,27 +157,13 @@ int wp_mldsa_up_ref(wp_MlDsa* mldsa) * 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*. + * @return Pointer to wolfSSL wc_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. * @@ -210,14 +196,14 @@ static wp_MlDsa* wp_mldsa_new(WOLFPROV_CTX* provCtx, const wp_MlDsaData* data) int ok = 1; int rc; - rc = wc_dilithium_init_ex(&mldsa->key, NULL, INVALID_DEVID); + rc = wc_MlDsaKey_Init(&mldsa->key, NULL, INVALID_DEVID); if (rc != 0) { ok = 0; } if (ok) { - rc = wc_dilithium_set_level(&mldsa->key, data->level); + rc = wc_MlDsaKey_SetParams(&mldsa->key, data->level); if (rc != 0) { - wc_dilithium_free(&mldsa->key); + wc_MlDsaKey_Free(&mldsa->key); ok = 0; } } @@ -225,7 +211,7 @@ static wp_MlDsa* wp_mldsa_new(WOLFPROV_CTX* provCtx, const wp_MlDsaData* data) if (ok) { rc = wc_InitMutex(&mldsa->mutex); if (rc != 0) { - wc_dilithium_free(&mldsa->key); + wc_MlDsaKey_Free(&mldsa->key); ok = 0; } } @@ -268,7 +254,7 @@ void wp_mldsa_free(wp_MlDsa* mldsa) #ifndef WP_SINGLE_THREADED wc_FreeMutex(&mldsa->mutex); #endif - wc_dilithium_free(&mldsa->key); + wc_MlDsaKey_Free(&mldsa->key); OPENSSL_free(mldsa); } } @@ -288,6 +274,7 @@ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) unsigned char* privBuf = NULL; word32 pubLen; word32 privLen; + word32 privAllocLen = 0; int rc; int ok = 1; int dupPub; @@ -312,14 +299,14 @@ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) ok = 0; } if (ok) { - rc = wc_dilithium_export_public((MlDsaKey*)&src->key, pubBuf, + rc = wc_MlDsaKey_ExportPubRaw((wc_MlDsaKey*)&src->key, pubBuf, &pubLen); if (rc != 0) { ok = 0; } } if (ok) { - rc = wc_dilithium_import_public(pubBuf, pubLen, &dst->key); + rc = wc_MlDsaKey_ImportPubRaw(&dst->key, pubBuf, pubLen); if (rc != 0) { ok = 0; } @@ -332,20 +319,21 @@ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) } if (ok && dupPriv) { - privLen = src->data->privKeySize; - privBuf = (unsigned char*)OPENSSL_malloc(privLen); + privAllocLen = src->data->privKeySize; + privLen = privAllocLen; + privBuf = (unsigned char*)OPENSSL_malloc(privAllocLen); if (privBuf == NULL) { ok = 0; } if (ok) { - rc = wc_dilithium_export_private((MlDsaKey*)&src->key, privBuf, + rc = wc_MlDsaKey_ExportPrivRaw((wc_MlDsaKey*)&src->key, privBuf, &privLen); if (rc != 0) { ok = 0; } } if (ok) { - rc = wc_dilithium_import_private(privBuf, privLen, &dst->key); + rc = wc_MlDsaKey_ImportPrivRaw(&dst->key, privBuf, privLen); if (rc != 0) { ok = 0; } @@ -353,7 +341,8 @@ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) if (ok) { dst->hasPriv = 1; } - OPENSSL_clear_free(privBuf, privLen); + /* Zero the full allocation, not just the (possibly-truncated) out len. */ + OPENSSL_clear_free(privBuf, privAllocLen); } if (!ok) { @@ -420,6 +409,8 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) unsigned char* bufB = NULL; word32 lenA; word32 lenB; + word32 allocA = 0; + word32 allocB = 0; if (!wolfssl_prov_is_running() || (a == NULL) || (b == NULL)) { return 0; @@ -436,13 +427,13 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) ok = 0; } if (ok) { - rc = wc_dilithium_export_public((MlDsaKey*)&a->key, bufA, &lenA); + rc = wc_MlDsaKey_ExportPubRaw((wc_MlDsaKey*)&a->key, bufA, &lenA); if (rc != 0) { ok = 0; } } if (ok) { - rc = wc_dilithium_export_public((MlDsaKey*)&b->key, bufB, &lenB); + rc = wc_MlDsaKey_ExportPubRaw((wc_MlDsaKey*)&b->key, bufB, &lenB); if (rc != 0) { ok = 0; } @@ -456,21 +447,23 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) 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); + allocA = a->data->privKeySize; + allocB = b->data->privKeySize; + lenA = allocA; + lenB = allocB; + bufA = (unsigned char*)OPENSSL_malloc(allocA); + bufB = (unsigned char*)OPENSSL_malloc(allocB); if ((bufA == NULL) || (bufB == NULL)) { ok = 0; } if (ok) { - rc = wc_dilithium_export_private((MlDsaKey*)&a->key, bufA, &lenA); + rc = wc_MlDsaKey_ExportPrivRaw((wc_MlDsaKey*)&a->key, bufA, &lenA); if (rc != 0) { ok = 0; } } if (ok) { - rc = wc_dilithium_export_private((MlDsaKey*)&b->key, bufB, &lenB); + rc = wc_MlDsaKey_ExportPrivRaw((wc_MlDsaKey*)&b->key, bufB, &lenB); if (rc != 0) { ok = 0; } @@ -478,8 +471,9 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) if (ok && ((lenA != lenB) || (XMEMCMP(bufA, bufB, lenA) != 0))) { ok = 0; } - OPENSSL_clear_free(bufA, lenA); - OPENSSL_clear_free(bufB, lenB); + /* Zero full allocations even if export truncated the out lengths. */ + OPENSSL_clear_free(bufA, allocA); + OPENSSL_clear_free(bufB, allocB); } return ok; } @@ -515,22 +509,25 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, &privData, &privLen)) { ok = 0; } + /* FIPS 204 priv keys are fixed-size; equality check before word32 cast + * also catches truncation on 64-bit platforms. */ + if (ok && (privData != NULL) && (privLen != mldsa->data->privKeySize)) { + ok = 0; + } if (ok && (privData != NULL)) { - rc = wc_dilithium_import_private(privData, (word32)privLen, - &mldsa->key); + rc = wc_MlDsaKey_ImportPrivRaw(&mldsa->key, privData, + (word32)privLen); if (rc != 0) { ok = 0; } 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. */ + /* FIPS 204 priv-key import may populate pub as a side effect + * (impl-defined); probe so we can advertise it if available. */ derivedPubLen = mldsa->data->pubKeySize; derivedPub = (unsigned char*)OPENSSL_malloc(derivedPubLen); if (derivedPub != NULL) { - if (wc_dilithium_export_public(&mldsa->key, derivedPub, + if (wc_MlDsaKey_ExportPubRaw(&mldsa->key, derivedPub, &derivedPubLen) == 0) { mldsa->hasPub = 1; } @@ -543,19 +540,27 @@ 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)) { + if (ok && (pubData != NULL) && (pubLen != mldsa->data->pubKeySize)) { + ok = 0; + } + /* Both supplied: if we derived a pub from priv, supplied pub must + * match. OOM during malloc is fatal so the hardening isn't fail-open + * under memory pressure. If wolfSSL did not auto-derive pub (impl + * choice, not attacker-influenced), the check is skipped. */ + if (ok && (pubData != NULL) && (privData != NULL)) { + if (derivedPub == NULL) { ok = 0; } + else if (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); + rc = wc_MlDsaKey_ImportPubRaw(&mldsa->key, pubData, + (word32)pubLen); if (rc != 0) { ok = 0; } @@ -567,6 +572,19 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, if (ok && (privData == NULL) && (pubData == NULL)) { ok = 0; } + /* When both parts imported, ask wolfSSL to verify they are consistent. + * Catches mismatched pub/priv that the earlier derived-pub probe could + * not (wolfSSL master's ImportPrivRaw does not auto-populate pub). */ + if (ok && (privData != NULL) && (pubData != NULL)) { + if (wc_MlDsaKey_CheckKey(&mldsa->key) != 0) { + ok = 0; + } + } + if (!ok) { + /* Clear flags on failure so partial-init state is not advertised. */ + mldsa->hasPriv = 0; + mldsa->hasPub = 0; + } OPENSSL_free(derivedPub); return ok; } @@ -635,6 +653,7 @@ static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, unsigned char* privBuf = NULL; word32 pubLen = 0; word32 privLen = 0; + word32 privAllocLen = 0; int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; @@ -650,7 +669,7 @@ static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, ok = 0; } if (ok) { - rc = wc_dilithium_export_public(&mldsa->key, pubBuf, &pubLen); + rc = wc_MlDsaKey_ExportPubRaw(&mldsa->key, pubBuf, &pubLen); if (rc != 0) { ok = 0; } @@ -661,13 +680,14 @@ static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, } } if (ok && expPriv && mldsa->hasPriv) { - privLen = mldsa->data->privKeySize; - privBuf = (unsigned char*)OPENSSL_malloc(privLen); + privAllocLen = mldsa->data->privKeySize; + privLen = privAllocLen; + privBuf = (unsigned char*)OPENSSL_malloc(privAllocLen); if (privBuf == NULL) { ok = 0; } if (ok) { - rc = wc_dilithium_export_private(&mldsa->key, privBuf, &privLen); + rc = wc_MlDsaKey_ExportPrivRaw(&mldsa->key, privBuf, &privLen); if (rc != 0) { ok = 0; } @@ -681,7 +701,8 @@ static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, ok = paramCb(params, cbArg); } OPENSSL_free(pubBuf); - OPENSSL_clear_free(privBuf, privLen); + /* Zero full allocation in case ExportPrivRaw truncated privLen. */ + OPENSSL_clear_free(privBuf, privAllocLen); return ok; } @@ -745,58 +766,54 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PUB_KEY); if (p != NULL) { word32 outLen = mldsa->data->pubKeySize; - if (p->data == NULL) { + if (!mldsa->hasPub) { + ok = 0; + } + else if (p->data == NULL) { + /* Size query. */ p->return_size = outLen; } - else if (mldsa->hasPub) { - if (p->data_size < outLen) { + else if (p->data_size < outLen) { + /* Buffer too small: report required size, let caller retry. */ + p->return_size = outLen; + } + else { + outLen = (word32)p->data_size; + rc = wc_MlDsaKey_ExportPubRaw(&mldsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { ok = 0; } else { - 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; - } + p->return_size = outLen; } } - else { - /* Buffer supplied but no public key available. */ - p->return_size = 0; - } } } if (ok) { p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PRIV_KEY); if (p != NULL) { word32 outLen = mldsa->data->privKeySize; - if (p->data == NULL) { + if (!mldsa->hasPriv) { + ok = 0; + } + else if (p->data == NULL) { + p->return_size = outLen; + } + else if (p->data_size < outLen) { p->return_size = outLen; } - else if (mldsa->hasPriv) { - if (p->data_size < outLen) { + else { + outLen = (word32)p->data_size; + rc = wc_MlDsaKey_ExportPrivRaw(&mldsa->key, + (unsigned char*)p->data, &outLen); + if (rc != 0) { ok = 0; } else { - 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; - } + p->return_size = outLen; } } - else { - /* Buffer supplied but no private key available. */ - p->return_size = 0; - } } } return ok; @@ -895,7 +912,7 @@ static wp_MlDsa* wp_mldsa_gen(wp_MlDsaGenCtx* ctx, OSSL_CALLBACK* osslcb, mldsa = wp_mldsa_new(ctx->provCtx, ctx->data); if ((mldsa != NULL) && keyPair) { - int rc = wc_dilithium_make_key(&mldsa->key, &ctx->rng); + int rc = wc_MlDsaKey_MakeKey(&mldsa->key, &ctx->rng); if (rc != 0) { wp_mldsa_free(mldsa); mldsa = NULL; diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 9cd4f4f7..e8705714 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -1,6 +1,6 @@ /* wp_mldsa_sig.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -30,7 +30,7 @@ #ifdef WP_HAVE_MLDSA -#include +#include /** * ML-DSA signature context. @@ -72,6 +72,8 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, { int ok = 1; size_t needed; + size_t newCap; + size_t doubled; unsigned char* tmp; needed = ctx->mdLen + dataLen; @@ -82,9 +84,9 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, ok = 0; } if (ok && (needed > ctx->mdCap)) { - size_t newCap = ctx->mdCap == 0 ? 256 : ctx->mdCap; + newCap = ctx->mdCap == 0 ? 256 : ctx->mdCap; while (newCap < needed) { - size_t doubled = newCap * 2; + doubled = newCap * 2; if (doubled < newCap) { ok = 0; break; @@ -92,11 +94,17 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, newCap = doubled; } if (ok) { - tmp = (unsigned char*)OPENSSL_realloc(ctx->mdBuf, newCap); + /* Grow by alloc+copy+zero rather than realloc so we always wipe + * the previous block (message can be signer-confidential). */ + tmp = (unsigned char*)OPENSSL_malloc(newCap); if (tmp == NULL) { ok = 0; } - else { + if (ok) { + if (ctx->mdLen > 0) { + XMEMCPY(tmp, ctx->mdBuf, ctx->mdLen); + } + OPENSSL_clear_free(ctx->mdBuf, ctx->mdCap); ctx->mdBuf = tmp; ctx->mdCap = newCap; } @@ -116,6 +124,10 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, */ static void wp_mldsa_buf_reset(wp_MlDsaSigCtx* ctx) { + /* Wipe stale bytes; ctx reuse across operations must not leak prior msg. */ + if ((ctx->mdBuf != NULL) && (ctx->mdLen > 0)) { + wc_ForceZero(ctx->mdBuf, ctx->mdLen); + } ctx->mdLen = 0; } @@ -273,7 +285,7 @@ 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 + /* wolfSSL's ML-DSA API takes a 32-bit message length. Reject >4 GiB * messages explicitly rather than silently truncating. */ if (ok && (msgLen > 0xFFFFFFFFU)) { ok = 0; @@ -283,8 +295,9 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, /* 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); + rc = wc_MlDsaKey_SignCtx( + (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), NULL, 0, sig, &outLen, + msg, (word32)msgLen, &ctx->rng); if (rc != 0) { ok = 0; } @@ -312,18 +325,20 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, int rc; int res = 0; - if ((ctx == NULL) || (ctx->mldsa == NULL)) { + if ((ctx == NULL) || (ctx->mldsa == NULL) || (sig == NULL) || + (msg == NULL)) { return 0; } - /* wolfSSL's dilithium API takes 32-bit lengths. Reject oversize inputs + /* wolfSSL's ML-DSA 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, - (word32)msgLen, &res, (MlDsaKey*)wp_mldsa_get_key(ctx->mldsa)); + rc = wc_MlDsaKey_VerifyCtx( + (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), sig, (word32)sigLen, + NULL, 0, msg, (word32)msgLen, &res); if ((rc != 0) || (res != 1)) { ok = 0; } @@ -412,14 +427,12 @@ static int wp_mldsa_digest_verify_final(wp_MlDsaSigCtx* ctx, return wp_mldsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); } -/** - * Get ctx params. None supported; checks ctx is non-NULL to match other - * provider implementations. - */ +/* No supported params; OSSL contract is unconditional success. */ static int wp_mldsa_get_ctx_params(wp_MlDsaSigCtx* ctx, OSSL_PARAM* params) { + (void)ctx; (void)params; - return ctx != NULL; + return 1; } static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, @@ -433,15 +446,13 @@ static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, return wp_mldsa_gettable; } -/** - * Set ctx params. None supported; checks ctx is non-NULL to match other - * provider implementations. - */ +/* No supported params; OSSL contract is unconditional success. */ static int wp_mldsa_set_ctx_params(wp_MlDsaSigCtx* ctx, const OSSL_PARAM params[]) { + (void)ctx; (void)params; - return ctx != NULL; + return 1; } 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 07e3288a..ea84f5b9 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -1,6 +1,6 @@ /* wp_mlkem_kem.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -193,7 +193,7 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, } return 1; } - if (secret == NULL) { + if ((secret == NULL) || (outLen == NULL) || (secretLen == NULL)) { return 0; } @@ -251,6 +251,9 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, } return 1; } + if ((outLen == NULL) || (in == NULL)) { + return 0; + } if (ok && (*outLen < ssSize)) { ok = 0; @@ -271,14 +274,12 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, return ok; } -/** - * Get ctx params. None supported; checks ctx is non-NULL to match other - * provider implementations. - */ +/* No supported params; OSSL contract is unconditional success. */ static int wp_mlkem_kem_get_ctx_params(wp_MlKemCtx* ctx, OSSL_PARAM* params) { + (void)ctx; (void)params; - return ctx != NULL; + return 1; } static const OSSL_PARAM* wp_mlkem_kem_gettable_ctx_params(wp_MlKemCtx* ctx, @@ -292,15 +293,13 @@ static const OSSL_PARAM* wp_mlkem_kem_gettable_ctx_params(wp_MlKemCtx* ctx, return wp_mlkem_kem_gettable; } -/** - * Set ctx params. None supported; checks ctx is non-NULL to match other - * provider implementations. - */ +/* No supported params; OSSL contract is unconditional success. */ static int wp_mlkem_kem_set_ctx_params(wp_MlKemCtx* ctx, const OSSL_PARAM params[]) { + (void)ctx; (void)params; - return ctx != NULL; + return 1; } 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 6892450f..eddb75ff 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -1,6 +1,6 @@ /* wp_mlkem_kmgmt.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -278,14 +278,15 @@ static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) 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; + 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_mlkem_new(src->provCtx, src->data); if (dst == NULL) { @@ -502,6 +503,11 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, &privData, &privLen)) { ok = 0; } + /* FIPS 203 priv keys are fixed-size; equality check before word32 cast + * also catches truncation on 64-bit platforms. */ + if (ok && (privData != NULL) && (privLen != mlkem->data->privKeySize)) { + ok = 0; + } if (ok && (privData != NULL)) { rc = wc_MlKemKey_DecodePrivateKey(&mlkem->key, privData, (word32)privLen); @@ -510,8 +516,8 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, } if (ok) { mlkem->hasPriv = 1; - /* Probe whether private-key import gave us the public part - * (FIPS 203 private keys embed the public component). */ + /* FIPS 203 private keys embed the public component; probe it + * so we can consistency-check against any supplied pub. */ derivedPubLen = mlkem->data->pubKeySize; derivedPub = (unsigned char*)OPENSSL_malloc(derivedPubLen); if (derivedPub != NULL) { @@ -528,15 +534,22 @@ 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)) { + if (ok && (pubData != NULL) && (pubLen != mlkem->data->pubKeySize)) { + ok = 0; + } + /* Both supplied: if we derived a pub from priv, supplied pub must + * match. OOM during malloc is fatal (no fail-open under memory + * pressure). If wolfSSL didn't auto-derive, the check is skipped. */ + if (ok && (pubData != NULL) && (privData != NULL)) { + if (derivedPub == NULL) { ok = 0; } + else if (mlkem->hasPub) { + if ((derivedPubLen != pubLen) || + (XMEMCMP(derivedPub, pubData, pubLen) != 0)) { + ok = 0; + } + } } if (ok && (pubData != NULL)) { rc = wc_MlKemKey_DecodePublicKey(&mlkem->key, pubData, @@ -552,6 +565,11 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, if (ok && (privData == NULL) && (pubData == NULL)) { ok = 0; } + if (!ok) { + /* Clear flags on failure so partial-init state is not advertised. */ + mlkem->hasPriv = 0; + mlkem->hasPub = 0; + } OPENSSL_free(derivedPub); return ok; } @@ -729,56 +747,51 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PUB_KEY); if (p != NULL) { word32 outLen = mlkem->data->pubKeySize; - if (p->data == NULL) { + if (!mlkem->hasPub) { + ok = 0; + } + else if (p->data == NULL) { p->return_size = outLen; } - else if (mlkem->hasPub) { - if (p->data_size < outLen) { + else if (p->data_size < outLen) { + /* Buffer too small: report required size, let caller retry. */ + p->return_size = outLen; + } + else { + rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { ok = 0; } else { - rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, - (unsigned char*)p->data, outLen); - if (rc != 0) { - ok = 0; - } - else { - p->return_size = outLen; - } + p->return_size = outLen; } } - else { - /* Buffer supplied but no public key available. */ - p->return_size = 0; - } } } if (ok) { p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PRIV_KEY); if (p != NULL) { word32 outLen = mlkem->data->privKeySize; - if (p->data == NULL) { + if (!mlkem->hasPriv) { + ok = 0; + } + else if (p->data == NULL) { p->return_size = outLen; } - else if (mlkem->hasPriv) { - if (p->data_size < outLen) { + else if (p->data_size < outLen) { + p->return_size = outLen; + } + else { + rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { ok = 0; } else { - rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, - (unsigned char*)p->data, outLen); - if (rc != 0) { - ok = 0; - } - else { - p->return_size = outLen; - } + p->return_size = outLen; } } - else { - /* Buffer supplied but no private key available. */ - p->return_size = 0; - } } } return ok; diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index b4e0df95..b17e692e 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -1,6 +1,6 @@ /* test_pqc_interop.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -23,7 +23,7 @@ * 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) + * 3. wolfSSL direct (wc_MlKemKey_* / wc_MlDsaKey_* APIs, no provider) * * For each algorithm at each NIST level, every cross-pair is tested: * wolfProv enc/sign -> default dec/verify @@ -32,7 +32,7 @@ * 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 + * byte encodings are standards-compliant end-to-end, not just internally * round-trippable. * * Usage: test_pqc_interop [provider_path] @@ -59,7 +59,7 @@ #if defined(WP_HAVE_MLKEM) && defined(WP_HAVE_MLDSA) #include -#include +#include #include #define WP_NAME "libwolfprov" @@ -76,29 +76,38 @@ static WC_RNG g_rng; static int load_all(const char* wp_path) { - wp_ctx = OSSL_LIB_CTX_new(); - if (wp_ctx == NULL) return 0; + int ok = 1; + wp_ctx = OSSL_LIB_CTX_new(); + 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); if (wp_prov == NULL) { fprintf(stderr, "Failed to load wolfProvider\n"); ERR_print_errors_fp(stderr); - return 0; + ok = 0; } - /* 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")) { + /* The global default provider must advertise PQC for cross-validation. */ + if (ok && !OSSL_PROVIDER_available(NULL, "default")) { fprintf(stderr, "OpenSSL default provider unavailable in global " "context\n"); - return 0; + ok = 0; } - if (wc_InitRng(&g_rng) != 0) { + if (ok && wc_InitRng(&g_rng) != 0) { fprintf(stderr, "wc_InitRng failed\n"); - return 0; + ok = 0; } - return 1; + if (!ok) { + if (wp_prov != NULL) { + OSSL_PROVIDER_unload(wp_prov); + wp_prov = NULL; + } + OSSL_LIB_CTX_free(wp_ctx); + wp_ctx = NULL; + } + return ok; } static void unload_all(void) @@ -340,32 +349,32 @@ static int evp_verify(OSSL_LIB_CTX* lib, EVP_PKEY* k, const unsigned char* msg, return ok; } -/* wolfSSL-direct sign using wc_dilithium_sign_ctx_msg with empty context +/* wolfSSL-direct sign using wc_MlDsaKey_SignCtx 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; + wc_MlDsaKey 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; + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) return 0; + if (wc_MlDsaKey_SetParams(&key, level) != 0) { + wc_MlDsaKey_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; } + rc = wc_MlDsaKey_ImportPrivRaw(&key, priv, (word32)privLen); + if (rc != 0) { wc_MlDsaKey_Free(&key); return 0; } + sigSz = wc_MlDsaKey_SigSize(&key); + if (sigSz <= 0) { wc_MlDsaKey_Free(&key); return 0; } *sig = OPENSSL_malloc(sigSz); - if (*sig == NULL) { wc_dilithium_free(&key); return 0; } + if (*sig == NULL) { wc_MlDsaKey_Free(&key); return 0; } outLen = (word32)sigSz; - rc = wc_dilithium_sign_ctx_msg(NULL, 0, msg, (word32)msgLen, *sig, &outLen, - &key, &g_rng); - wc_dilithium_free(&key); + rc = wc_MlDsaKey_SignCtx(&key, NULL, 0, *sig, &outLen, msg, (word32)msgLen, + &g_rng); + wc_MlDsaKey_Free(&key); if (rc != 0) { OPENSSL_free(*sig); *sig = NULL; return 0; } *sigLen = outLen; return 1; @@ -376,20 +385,20 @@ 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; + wc_MlDsaKey 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; + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) return 0; + if (wc_MlDsaKey_SetParams(&key, level) != 0) { + wc_MlDsaKey_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); + rc = wc_MlDsaKey_ImportPubRaw(&key, pub, (word32)pubLen); + if (rc != 0) { wc_MlDsaKey_Free(&key); return 0; } + rc = wc_MlDsaKey_VerifyCtx(&key, sig, (word32)sigLen, NULL, 0, msg, + (word32)msgLen, &res); + wc_MlDsaKey_Free(&key); return rc == 0 && res == 1; } diff --git a/test/test_mldsa.c b/test/test_mldsa.c index d094d3f9..1c094213 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -1,6 +1,6 @@ /* test_mldsa.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -25,7 +25,7 @@ #ifdef WP_HAVE_MLDSA -#include +#include /* Per-level metadata. */ typedef struct mldsa_test_level { @@ -469,4 +469,373 @@ int test_mldsa_verify_wrong_key(void* data) return err; } +/* Helper: digest_sign-only short message sign, returns sig (caller frees). */ +static int mldsa_dsign_short(EVP_PKEY* k, const unsigned char* msg, + size_t msgLen, unsigned char** sig, size_t* sigLen) +{ + EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); + int err = (mdctx == NULL); + + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, k, + 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; + } + EVP_MD_CTX_free(mdctx); + return err; +} + +/* EVP_PKEY_dup roundtrip: dup pub matches; sign with dup, verify with orig. */ +int test_mldsa_dup(void* data) +{ + static const unsigned char msg[32] = "ML-DSA dup test message vector!"; + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY* d = NULL; + unsigned char* pub1 = NULL; + unsigned char* pub2 = NULL; + size_t pub1Len = 0; + size_t pub2Len = 0; + 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("Dup %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k); + if (err == 0) { + d = EVP_PKEY_dup(k); + err = (d == NULL); + } + if (err == 0) { + err = mldsa_get_pub(k, &pub1, &pub1Len); + } + if (err == 0) { + err = mldsa_get_pub(d, &pub2, &pub2Len); + } + if (err == 0) { + err = (pub1Len != pub2Len) || (memcmp(pub1, pub2, pub1Len) != 0); + if (err) PRINT_ERR_MSG("Dup pub byte mismatch"); + } + if (err == 0) { + err = mldsa_dsign_short(d, msg, sizeof(msg), &sig, &sigLen); + } + if (err == 0) { + err = mldsa_verify_msg(k, msg, sizeof(msg), sig, sigLen) != 1; + if (err) PRINT_ERR_MSG("Verify-with-orig of dup-sig failed"); + } + + OPENSSL_free(pub1); pub1 = NULL; pub1Len = 0; + OPENSSL_free(pub2); pub2 = NULL; pub2Len = 0; + OPENSSL_free(sig); sig = NULL; sigLen = 0; + EVP_PKEY_free(d); d = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* EVP_PKEY_eq for ML-DSA. */ +int test_mldsa_match(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY* k3 = NULL; + + (void)data; + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + PRINT_MSG("Match %s", mldsa_levels[i].name); + + err = mldsa_keygen(mldsa_levels[i].name, &k1); + if (err == 0) { + err = mldsa_keygen(mldsa_levels[i].name, &k2); + } + if (err == 0) { + err = EVP_PKEY_eq(k1, k1) != 1; + if (err) PRINT_ERR_MSG("Self-eq failed"); + } + if (err == 0) { + err = EVP_PKEY_eq(k1, k2) == 1; + if (err) PRINT_ERR_MSG("Distinct keys reported equal"); + } + if ((err == 0) && (i + 1 < MLDSA_LEVEL_COUNT)) { + err = mldsa_keygen(mldsa_levels[i + 1].name, &k3); + if (err == 0) { + err = EVP_PKEY_eq(k1, k3) == 1; + if (err) PRINT_ERR_MSG("Cross-level keys reported equal"); + } + EVP_PKEY_free(k3); k3 = NULL; + } + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/* EVP_MD_CTX_copy_ex on a partial digest_sign accumulator. Both ctxs must + * produce signatures that verify under the original key. */ +int test_mldsa_dupctx(void* data) +{ + static const unsigned char part1[16] = "mldsa-dupctx-pt1"; + static const unsigned char part2[16] = "mldsa-dupctx-pt2"; + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_MD_CTX* a = NULL; + EVP_MD_CTX* b = NULL; + unsigned char* sigA = NULL; + unsigned char* sigB = NULL; + size_t sigALen = 0; + size_t sigBLen = 0; + unsigned char msg[32]; + + (void)data; + XMEMCPY(msg, part1, 16); + XMEMCPY(msg + 16, part2, 16); + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + PRINT_MSG("Dupctx %s", mldsa_levels[i].name); + + err = mldsa_keygen(mldsa_levels[i].name, &k); + if (err == 0) { + a = EVP_MD_CTX_new(); + err = (a == NULL); + } + if (err == 0) { + err = EVP_DigestSignInit_ex(a, NULL, NULL, wpLibCtx, NULL, k, + NULL) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(a, part1, sizeof(part1)) != 1; + } + if (err == 0) { + b = EVP_MD_CTX_new(); + err = (b == NULL); + } + if (err == 0) { + err = EVP_MD_CTX_copy_ex(b, a) != 1; + } + if (err == 0) { + err = EVP_DigestSignUpdate(a, part2, sizeof(part2)) != 1 + || EVP_DigestSignUpdate(b, part2, sizeof(part2)) != 1; + } + if (err == 0) { + err = EVP_DigestSignFinal(a, NULL, &sigALen) != 1 + || EVP_DigestSignFinal(b, NULL, &sigBLen) != 1; + } + if (err == 0) { + sigA = (unsigned char*)OPENSSL_malloc(sigALen); + sigB = (unsigned char*)OPENSSL_malloc(sigBLen); + err = (sigA == NULL) || (sigB == NULL); + } + if (err == 0) { + err = EVP_DigestSignFinal(a, sigA, &sigALen) != 1 + || EVP_DigestSignFinal(b, sigB, &sigBLen) != 1; + } + if (err == 0) { + err = mldsa_verify_msg(k, msg, sizeof(msg), sigA, sigALen) != 1 + || mldsa_verify_msg(k, msg, sizeof(msg), sigB, sigBLen) != 1; + if (err) PRINT_ERR_MSG("Dupctx sig verify failed"); + } + + EVP_MD_CTX_free(a); a = NULL; + EVP_MD_CTX_free(b); b = NULL; + OPENSSL_free(sigA); sigA = NULL; sigALen = 0; + OPENSSL_free(sigB); sigB = NULL; sigBLen = 0; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* One-shot EVP_PKEY_sign / EVP_PKEY_verify path (not digest_sign). */ +int test_mldsa_oneshot_sign_verify(void* data) +{ + static const unsigned char msg[16] = "mldsa-one-shot!!"; + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY_CTX* sctx = NULL; + EVP_PKEY_CTX* vctx = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + PRINT_MSG("One-shot sign/verify %s", mldsa_levels[i].name); + + err = mldsa_keygen(mldsa_levels[i].name, &k); + if (err == 0) { + sctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, k, NULL); + err = (sctx == NULL) || (EVP_PKEY_sign_init(sctx) != 1); + } + if (err == 0) { + sigLen = 0; + err = EVP_PKEY_sign(sctx, NULL, &sigLen, msg, sizeof(msg)) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_PKEY_sign(sctx, sig, &sigLen, msg, sizeof(msg)) != 1; + } + if (err == 0) { + vctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, k, NULL); + err = (vctx == NULL) || (EVP_PKEY_verify_init(vctx) != 1); + } + if (err == 0) { + err = EVP_PKEY_verify(vctx, sig, sigLen, msg, sizeof(msg)) != 1; + } + + OPENSSL_free(sig); sig = NULL; sigLen = 0; + EVP_PKEY_CTX_free(sctx); sctx = NULL; + EVP_PKEY_CTX_free(vctx); vctx = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* BITS / SECURITY_BITS / MAX_SIZE getters. */ +int test_mldsa_get_params(void* data) +{ + /* FIPS 204: ML-DSA-44 -> 128 sec bits, -65 -> 192, -87 -> 256 */ + static const int secBits[] = { 128, 192, 256 }; + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + + (void)data; + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Params %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k); + if (err == 0) { + err = EVP_PKEY_get_bits(k) != (int)(lvl->pubKeySize * 8); + if (err) PRINT_ERR_MSG("Wrong BITS"); + } + if (err == 0) { + err = EVP_PKEY_get_security_bits(k) != secBits[i]; + if (err) PRINT_ERR_MSG("Wrong SECURITY_BITS"); + } + if (err == 0) { + err = EVP_PKEY_get_size(k) != (int)lvl->sigSize; + if (err) PRINT_ERR_MSG("Wrong MAX_SIZE"); + } + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* DigestSignInit with non-empty mdName must fail (ML-DSA is pure). */ +int test_mldsa_digest_sign_init_rejects_md(void* data) +{ + int err = 0; + EVP_PKEY* k = NULL; + EVP_MD_CTX* mdctx = NULL; + int rc; + + (void)data; + PRINT_MSG("DigestSignInit rejects non-empty md"); + + err = mldsa_keygen("ML-DSA-44", &k); + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + /* Pass "SHA-256" as mdName; ML-DSA is pure-mode so this MUST fail. */ + rc = EVP_DigestSignInit_ex(mdctx, NULL, "SHA-256", wpLibCtx, NULL, k, + NULL); + err = (rc == 1); + if (err) PRINT_ERR_MSG("DigestSignInit with mdName unexpectedly OK"); + } + EVP_MD_CTX_free(mdctx); + EVP_PKEY_free(k); + return err; +} + +/* Negative: import priv + mutated pub. Expect fromdata to FAIL. */ +int test_mldsa_import_mismatched_pubpriv(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + OSSL_PARAM_BLD* bld; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + size_t pubLen = 0; + size_t privLen = 0; + int rc; + + (void)data; + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("Mismatched pub/priv %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k); + if (err == 0) { + err = mldsa_get_pub(k, &pub, &pubLen); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, &privLen) != 1; + } + if (err == 0) { + priv = (unsigned char*)OPENSSL_malloc(privLen); + err = (priv == NULL) || EVP_PKEY_get_octet_string_param(k, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen, &privLen) != 1; + } + if (err == 0) { + pub[0] ^= 0x01; + } + 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) { + bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL) + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PUB_KEY, pub, pubLen) != 1 + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen) != 1; + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + OSSL_PARAM_BLD_free(bld); + } + if (err == 0) { + rc = EVP_PKEY_fromdata(ctx, &k2, EVP_PKEY_KEYPAIR, params); + err = (rc == 1); + if (err) PRINT_ERR_MSG("Mismatched import succeeded"); + } + + OPENSSL_free(pub); pub = NULL; pubLen = 0; + OPENSSL_clear_free(priv, privLen); priv = NULL; privLen = 0; + OSSL_PARAM_free(params); params = NULL; + EVP_PKEY_CTX_free(ctx); ctx = NULL; + EVP_PKEY_free(k); k = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + #endif /* WP_HAVE_MLDSA */ diff --git a/test/test_mlkem.c b/test/test_mlkem.c index d336f2f0..0793f59f 100644 --- a/test/test_mlkem.c +++ b/test/test_mlkem.c @@ -1,6 +1,6 @@ /* test_mlkem.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -479,4 +479,302 @@ int test_mlkem_decap_wrong_key(void* data) return err; } +/* EVP_PKEY_dup roundtrip: dup pub must equal original pub, and an encap + * with the dup must decap correctly with the original. */ +int test_mlkem_dup(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY* d = NULL; + EVP_PKEY_CTX* ectx = NULL; + EVP_PKEY_CTX* dctx = NULL; + unsigned char* pub1 = NULL; + unsigned char* pub2 = NULL; + size_t pub1Len = 0; + size_t pub2Len = 0; + unsigned char* ct = NULL; + size_t ctLen = 0; + unsigned char ss1[32]; + unsigned char ss2[32]; + size_t ss1Len; + size_t ss2Len; + + (void)data; + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Dup %s", lvl->name); + + err = wp_test_mlkem_keygen(lvl->name, &k); + if (err == 0) { + d = EVP_PKEY_dup(k); + err = (d == NULL); + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k, OSSL_PKEY_PARAM_PUB_KEY, + NULL, 0, &pub1Len) != 1; + } + if (err == 0) { + pub1 = (unsigned char*)OPENSSL_malloc(pub1Len); + err = (pub1 == NULL) || EVP_PKEY_get_octet_string_param(k, + OSSL_PKEY_PARAM_PUB_KEY, pub1, pub1Len, &pub1Len) != 1; + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(d, OSSL_PKEY_PARAM_PUB_KEY, + NULL, 0, &pub2Len) != 1; + } + if (err == 0) { + pub2 = (unsigned char*)OPENSSL_malloc(pub2Len); + err = (pub2 == NULL) || EVP_PKEY_get_octet_string_param(d, + OSSL_PKEY_PARAM_PUB_KEY, pub2, pub2Len, &pub2Len) != 1; + } + if (err == 0) { + err = (pub1Len != pub2Len) || (memcmp(pub1, pub2, pub1Len) != 0); + if (err) PRINT_ERR_MSG("Dup pub byte mismatch"); + } + if (err == 0) { + ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, d, NULL); + err = (ectx == NULL) || (EVP_PKEY_encapsulate_init(ectx, NULL) != 1); + } + if (err == 0) { + ctLen = 0; + ss1Len = 0; + err = EVP_PKEY_encapsulate(ectx, NULL, &ctLen, NULL, &ss1Len) != 1; + } + if (err == 0) { + 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, k, NULL); + err = (dctx == NULL) || (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 = (ss1Len != ss2Len) || (memcmp(ss1, ss2, ss1Len) != 0); + if (err) PRINT_ERR_MSG("Dup secret mismatch"); + } + + OPENSSL_free(pub1); pub1 = NULL; pub1Len = 0; + OPENSSL_free(pub2); pub2 = NULL; pub2Len = 0; + OPENSSL_free(ct); ct = NULL; + EVP_PKEY_CTX_free(ectx); ectx = NULL; + EVP_PKEY_CTX_free(dctx); dctx = NULL; + EVP_PKEY_free(d); d = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* EVP_PKEY_eq: self == 1; distinct keys != 1; cross-level != 1. The non-self + * pairs can return 0 or -1 (type mismatch) so accept any "not equal". */ +int test_mlkem_match(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k1 = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY* k3 = NULL; + + (void)data; + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + PRINT_MSG("Match %s", mlkem_levels[i].name); + + err = wp_test_mlkem_keygen(mlkem_levels[i].name, &k1); + if (err == 0) { + err = wp_test_mlkem_keygen(mlkem_levels[i].name, &k2); + } + if (err == 0) { + err = EVP_PKEY_eq(k1, k1) != 1; + if (err) PRINT_ERR_MSG("Self-eq failed"); + } + if (err == 0) { + err = EVP_PKEY_eq(k1, k2) == 1; + if (err) PRINT_ERR_MSG("Distinct keys reported equal"); + } + if ((err == 0) && (i + 1 < MLKEM_LEVEL_COUNT)) { + err = wp_test_mlkem_keygen(mlkem_levels[i + 1].name, &k3); + if (err == 0) { + err = EVP_PKEY_eq(k1, k3) == 1; + if (err) PRINT_ERR_MSG("Cross-level keys reported equal"); + } + EVP_PKEY_free(k3); k3 = NULL; + } + EVP_PKEY_free(k1); k1 = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + +/* Decapsulate with out=NULL must return 1 and *outLen == 32. OpenSSL's wrapper + * requires a valid ciphertext even on the size-query path, so encap first. */ +int test_mlkem_decap_size_query(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY_CTX* ectx = NULL; + EVP_PKEY_CTX* dctx = NULL; + unsigned char* ct = NULL; + size_t ctLen = 0; + unsigned char ss[32]; + size_t ssLen; + + (void)data; + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + PRINT_MSG("Decap size-query %s", mlkem_levels[i].name); + + err = wp_test_mlkem_keygen(mlkem_levels[i].name, &k); + if (err == 0) { + ectx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, k, NULL); + err = (ectx == NULL) || (EVP_PKEY_encapsulate_init(ectx, NULL) != 1); + } + if (err == 0) { + ctLen = 0; + ssLen = 0; + err = EVP_PKEY_encapsulate(ectx, NULL, &ctLen, NULL, &ssLen) != 1; + } + if (err == 0) { + ct = (unsigned char*)OPENSSL_malloc(ctLen); + err = (ct == NULL); + } + if (err == 0) { + ssLen = sizeof(ss); + err = EVP_PKEY_encapsulate(ectx, ct, &ctLen, ss, &ssLen) != 1; + } + if (err == 0) { + dctx = EVP_PKEY_CTX_new_from_pkey(wpLibCtx, k, NULL); + err = (dctx == NULL) || (EVP_PKEY_decapsulate_init(dctx, NULL) != 1); + } + if (err == 0) { + ssLen = 0; + err = EVP_PKEY_decapsulate(dctx, NULL, &ssLen, ct, ctLen) != 1; + } + if (err == 0) { + err = (ssLen != 32); + if (err) PRINT_ERR_MSG("Decap size-query returned %zu", ssLen); + } + OPENSSL_free(ct); ct = NULL; + EVP_PKEY_CTX_free(ectx); ectx = NULL; + EVP_PKEY_CTX_free(dctx); dctx = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* BITS / SECURITY_BITS / MAX_SIZE getters. */ +int test_mlkem_get_params(void* data) +{ + /* FIPS 203: 512 -> 128 sec bits, 768 -> 192, 1024 -> 256 */ + static const int secBits[] = { 128, 192, 256 }; + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + + (void)data; + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Params %s", lvl->name); + + err = wp_test_mlkem_keygen(lvl->name, &k); + if (err == 0) { + err = EVP_PKEY_get_bits(k) != (int)(lvl->pubKeySize * 8); + if (err) PRINT_ERR_MSG("Wrong BITS"); + } + if (err == 0) { + err = EVP_PKEY_get_security_bits(k) != secBits[i]; + if (err) PRINT_ERR_MSG("Wrong SECURITY_BITS"); + } + if (err == 0) { + err = EVP_PKEY_get_size(k) != (int)lvl->ctSize; + if (err) PRINT_ERR_MSG("Wrong MAX_SIZE"); + } + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* Negative: import priv + mutated pub. Expect fromdata to FAIL. */ +int test_mlkem_import_mismatched_pubpriv(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY* k2 = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + OSSL_PARAM_BLD* bld; + unsigned char* pub = NULL; + unsigned char* priv = NULL; + size_t pubLen = 0; + size_t privLen = 0; + int rc; + + (void)data; + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + PRINT_MSG("Mismatched pub/priv %s", lvl->name); + + err = wp_test_mlkem_keygen(lvl->name, &k); + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k, OSSL_PKEY_PARAM_PUB_KEY, + NULL, 0, &pubLen) != 1; + } + if (err == 0) { + pub = (unsigned char*)OPENSSL_malloc(pubLen); + err = (pub == NULL) || EVP_PKEY_get_octet_string_param(k, + OSSL_PKEY_PARAM_PUB_KEY, pub, pubLen, &pubLen) != 1; + } + if (err == 0) { + err = EVP_PKEY_get_octet_string_param(k, OSSL_PKEY_PARAM_PRIV_KEY, + NULL, 0, &privLen) != 1; + } + if (err == 0) { + priv = (unsigned char*)OPENSSL_malloc(privLen); + err = (priv == NULL) || EVP_PKEY_get_octet_string_param(k, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen, &privLen) != 1; + } + if (err == 0) { + pub[0] ^= 0x01; + } + 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) { + bld = OSSL_PARAM_BLD_new(); + err = (bld == NULL) + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PUB_KEY, pub, pubLen) != 1 + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen) != 1; + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + OSSL_PARAM_BLD_free(bld); + } + if (err == 0) { + rc = EVP_PKEY_fromdata(ctx, &k2, EVP_PKEY_KEYPAIR, params); + err = (rc == 1); + if (err) PRINT_ERR_MSG("Mismatched import succeeded"); + } + + OPENSSL_free(pub); pub = NULL; pubLen = 0; + OPENSSL_clear_free(priv, privLen); priv = NULL; privLen = 0; + OSSL_PARAM_free(params); params = NULL; + EVP_PKEY_CTX_free(ctx); ctx = NULL; + EVP_PKEY_free(k); k = NULL; + EVP_PKEY_free(k2); k2 = NULL; + } + return err; +} + #endif /* WP_HAVE_MLKEM */ diff --git a/test/unit.c b/test/unit.c index 1656962b..0689fe59 100644 --- a/test/unit.c +++ b/test/unit.c @@ -485,6 +485,11 @@ TEST_CASE test_case[] = { TEST_DECL(test_mlkem_encap_decap, NULL), TEST_DECL(test_mlkem_decap_tampered_ct, NULL), TEST_DECL(test_mlkem_decap_wrong_key, NULL), + TEST_DECL(test_mlkem_dup, NULL), + TEST_DECL(test_mlkem_match, NULL), + TEST_DECL(test_mlkem_decap_size_query, NULL), + TEST_DECL(test_mlkem_get_params, NULL), + TEST_DECL(test_mlkem_import_mismatched_pubpriv, NULL), #endif #ifdef WP_HAVE_MLDSA @@ -494,6 +499,13 @@ TEST_CASE test_case[] = { TEST_DECL(test_mldsa_verify_tampered_sig, NULL), TEST_DECL(test_mldsa_verify_tampered_msg, NULL), TEST_DECL(test_mldsa_verify_wrong_key, NULL), + TEST_DECL(test_mldsa_dup, NULL), + TEST_DECL(test_mldsa_match, NULL), + TEST_DECL(test_mldsa_dupctx, NULL), + TEST_DECL(test_mldsa_oneshot_sign_verify, NULL), + TEST_DECL(test_mldsa_get_params, NULL), + TEST_DECL(test_mldsa_digest_sign_init_rejects_md, NULL), + TEST_DECL(test_mldsa_import_mismatched_pubpriv, NULL), #endif }; #define TEST_CASE_CNT (int)(sizeof(test_case) / sizeof(*test_case)) diff --git a/test/unit.h b/test/unit.h index 2616f9cb..f97ee785 100644 --- a/test/unit.h +++ b/test/unit.h @@ -483,6 +483,11 @@ 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); +int test_mlkem_dup(void *data); +int test_mlkem_match(void *data); +int test_mlkem_decap_size_query(void *data); +int test_mlkem_get_params(void *data); +int test_mlkem_import_mismatched_pubpriv(void *data); #endif #ifdef WP_HAVE_MLDSA @@ -492,6 +497,13 @@ 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); +int test_mldsa_dup(void *data); +int test_mldsa_match(void *data); +int test_mldsa_dupctx(void *data); +int test_mldsa_oneshot_sign_verify(void *data); +int test_mldsa_get_params(void *data); +int test_mldsa_digest_sign_init_rejects_md(void *data); +int test_mldsa_import_mismatched_pubpriv(void *data); #endif #endif /* UNIT_H */ From ec3e26d44da0c1a91c5fedc38188e6372d3685a1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Sat, 30 May 2026 13:20:49 -0700 Subject: [PATCH 18/43] PQC: address skoll review (empty msg, NULL-key reinit, KEM mixed-NULL, get_params, KEM op name, debian pqc) --- debian/install-wolfssl.sh | 14 ++++- scripts/build-wolfprovider.sh | 3 ++ src/wp_mldsa_kmgmt.c | 5 +- src/wp_mldsa_sig.c | 43 +++++++++++++--- src/wp_mlkem_kem.c | 10 ++-- src/wp_mlkem_kmgmt.c | 34 +++++++----- test/test_mldsa.c | 97 +++++++++++++++++++++++++++++++++++ test/unit.c | 2 + test/unit.h | 2 + 9 files changed, 182 insertions(+), 28 deletions(-) diff --git a/debian/install-wolfssl.sh b/debian/install-wolfssl.sh index 5c45bfde..30a1a33c 100755 --- a/debian/install-wolfssl.sh +++ b/debian/install-wolfssl.sh @@ -40,6 +40,7 @@ install_wolfssl_from_git() { local debug_mode="$3" local reinstall_mode="$4" local no_install="$5" + local pqc_mode="$6" local main_branch="master" # If no working directory specified, create one using mktemp @@ -173,6 +174,11 @@ AC_CONFIG_FILES([debian/rules],[chmod +x debian/rules])' configure.ac echo "Debug mode enabled" fi + if [ "$pqc_mode" = "true" ]; then + configure_opts="$configure_opts --enable-mlkem --enable-mldsa" + echo "PQC (ML-KEM/ML-DSA) enabled" + fi + ./configure $configure_opts \ CFLAGS="-DWOLFSSL_OLD_OID_SUM \ -DWOLFSSL_PUBLIC_ASN \ @@ -220,6 +226,7 @@ main() { local debug_mode="false" local reinstall_mode="false" local no_install="false" + local pqc_mode="false" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -233,6 +240,7 @@ main() { echo " -d, --debug Enable debug build mode (adds --enable-debug)" echo " -r, --reinstall Force reinstall even if packages are already installed" echo " -n, --no-install Build only, do not install packages" + echo " --enable-pqc Enable ML-KEM and ML-DSA (FIPS 203/204)" echo " -h, --help Show this help message" echo "" echo "Arguments:" @@ -264,6 +272,10 @@ main() { no_install="true" shift ;; + --enable-pqc) + pqc_mode="true" + shift + ;; -*) echo "Unknown option: $1" >&2 echo "Use --help for usage information" >&2 @@ -300,7 +312,7 @@ main() { echo "Building wolfSSL master branch" fi - install_wolfssl_from_git "$work_dir" "$git_tag" "$debug_mode" "$reinstall_mode" "$no_install" + install_wolfssl_from_git "$work_dir" "$git_tag" "$debug_mode" "$reinstall_mode" "$no_install" "$pqc_mode" echo "WolfSSL installation completed successfully" } diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index 3a1611b1..4a0438e7 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -216,6 +216,9 @@ if [ -n "$build_debian" ]; then if [ "$WOLFPROV_REPLACE_DEFAULT" = "1" ]; then OPENSSL_OPTS+=" --replace-default" fi + if [ "$WOLFPROV_PQC" = "1" ]; then + WOLFSSL_OPTS+=" --enable-pqc" + fi # wolfSSL and OpenSSL are independent and must be built first debian/install-wolfssl.sh $WOLFSSL_OPTS --no-install -r $DEB_OUTPUT_DIR diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 1d0babf8..00734ad1 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -774,8 +774,10 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) p->return_size = outLen; } else if (p->data_size < outLen) { - /* Buffer too small: report required size, let caller retry. */ + /* Buffer too small: report required size and fail so the + * caller can retry; do not claim a completed export. */ p->return_size = outLen; + ok = 0; } else { outLen = (word32)p->data_size; @@ -802,6 +804,7 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) } else if (p->data_size < outLen) { p->return_size = outLen; + ok = 0; } else { outLen = (word32)p->data_size; diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index e8705714..f84b5e28 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -224,15 +224,24 @@ static int wp_mldsa_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, (void)params; - if ((ctx == NULL) || (mldsa == NULL)) { + if (ctx == NULL) { ok = 0; } - if (ok && !wp_mldsa_up_ref(mldsa)) { + /* NULL key means "reinit, reuse the key already on the context" -- only + * valid if the context actually has one. */ + if (ok && (mldsa == NULL) && (ctx->mldsa == NULL)) { ok = 0; } + if (ok && (mldsa != NULL)) { + if (!wp_mldsa_up_ref(mldsa)) { + ok = 0; + } + if (ok) { + wp_mldsa_free(ctx->mldsa); + ctx->mldsa = mldsa; + } + } if (ok) { - wp_mldsa_free(ctx->mldsa); - ctx->mldsa = mldsa; wp_mldsa_buf_reset(ctx); } return ok; @@ -269,12 +278,22 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, int ok = 1; int rc; word32 sigSz; + /* FIPS 204 permits an empty message; give wolfSSL a valid pointer so a + * NULL+0 message does not become a backend-dependent NULL deref. */ + unsigned char dummy = 0; + const unsigned char* m = msg; (void)sigSize; if ((ctx == NULL) || (ctx->mldsa == NULL) || (sigLen == NULL)) { return 0; } + if ((msg == NULL) && (msgLen != 0)) { + return 0; + } + if (m == NULL) { + m = &dummy; + } sigSz = (word32)wp_mldsa_get_sig_size(ctx->mldsa); @@ -297,7 +316,7 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, * default; use the ctx variant with empty ctx to interop. */ rc = wc_MlDsaKey_SignCtx( (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), NULL, 0, sig, &outLen, - msg, (word32)msgLen, &ctx->rng); + m, (word32)msgLen, &ctx->rng); if (rc != 0) { ok = 0; } @@ -324,11 +343,19 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, int ok = 1; int rc; int res = 0; + /* FIPS 204 permits an empty message; give wolfSSL a valid pointer. */ + unsigned char dummy = 0; + const unsigned char* m = msg; - if ((ctx == NULL) || (ctx->mldsa == NULL) || (sig == NULL) || - (msg == NULL)) { + if ((ctx == NULL) || (ctx->mldsa == NULL) || (sig == NULL)) { return 0; } + if ((msg == NULL) && (msgLen != 0)) { + return 0; + } + if (m == NULL) { + m = &dummy; + } /* wolfSSL's ML-DSA API takes 32-bit lengths. Reject oversize inputs * explicitly rather than silently truncating. */ if ((sigLen > 0xFFFFFFFFU) || (msgLen > 0xFFFFFFFFU)) { @@ -338,7 +365,7 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, /* Match the sign path: FIPS 204 pure ML-DSA with empty context. */ rc = wc_MlDsaKey_VerifyCtx( (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), sig, (word32)sigLen, - NULL, 0, msg, (word32)msgLen, &res); + NULL, 0, m, (word32)msgLen, &res); if ((rc != 0) || (res != 1)) { ok = 0; } diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index ea84f5b9..e160fd3f 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -181,10 +181,9 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, ctSize = wp_mlkem_data_ct_size(data); ssSize = WP_MLKEM_SS_SIZE; - /* 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) { + /* Size-only query: both output buffers NULL. A mixed-NULL request (one + * buffer NULL, the other not) is a caller bug, not a size query. */ + if ((out == NULL) && (secret == NULL)) { if (outLen != NULL) { *outLen = ctSize; } @@ -193,7 +192,8 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, } return 1; } - if ((secret == NULL) || (outLen == NULL) || (secretLen == NULL)) { + if ((out == NULL) || (secret == NULL) || (outLen == NULL) || + (secretLen == NULL)) { return 0; } diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index eddb75ff..211e3278 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -754,8 +754,10 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) p->return_size = outLen; } else if (p->data_size < outLen) { - /* Buffer too small: report required size, let caller retry. */ + /* Buffer too small: report required size and fail so the + * caller can retry; do not claim a completed export. */ p->return_size = outLen; + ok = 0; } else { rc = wc_MlKemKey_EncodePublicKey(&mlkem->key, @@ -781,6 +783,7 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) } else if (p->data_size < outLen) { p->return_size = outLen; + ok = 0; } else { rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, @@ -949,19 +952,24 @@ 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 NULL. - */ -static const char* wp_mlkem_query_operation_name(int op) +/* Map each ML-KEM key type to its KEM operation name so OpenSSL fetches the + * matching KEM implementation without relying on fallback lookup. */ +static const char* wp_mlkem512_query_operation_name(int op) +{ + (void)op; + return WP_NAMES_ML_KEM_512; +} + +static const char* wp_mlkem768_query_operation_name(int op) +{ + (void)op; + return WP_NAMES_ML_KEM_768; +} + +static const char* wp_mlkem1024_query_operation_name(int op) { (void)op; - return NULL; + return WP_NAMES_ML_KEM_1024; } /* Per-level new() and gen_init() trampolines. */ @@ -1036,7 +1044,7 @@ const OSSL_DISPATCH wp_##alg##_keymgmt_functions[] = { \ { OSSL_FUNC_KEYMGMT_EXPORT_TYPES, \ (DFUNC)wp_mlkem_export_types }, \ { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ - (DFUNC)wp_mlkem_query_operation_name }, \ + (DFUNC)wp_##alg##_query_operation_name }, \ { 0, NULL } \ }; diff --git a/test/test_mldsa.c b/test/test_mldsa.c index 1c094213..1a125443 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -838,4 +838,101 @@ int test_mldsa_import_mismatched_pubpriv(void* data) return err; } +/* FIPS 204 permits an empty message: sign and verify zero-length input. */ +int test_mldsa_empty_message(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_MD_CTX* mdctx = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + PRINT_MSG("Empty message %s", mldsa_levels[i].name); + + err = mldsa_keygen(mldsa_levels[i].name, &k); + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, k, + NULL) != 1; + } + /* No update calls: message is the empty string. */ + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, &sigLen, NULL, 0) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, sig, &sigLen, NULL, 0) != 1; + if (err) PRINT_ERR_MSG("Empty-message sign failed"); + } + if (err == 0) { + err = mldsa_verify_msg(k, NULL, 0, sig, sigLen) != 1; + if (err) PRINT_ERR_MSG("Empty-message verify failed"); + } + + OPENSSL_free(sig); sig = NULL; sigLen = 0; + EVP_MD_CTX_free(mdctx); mdctx = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* Reinitialize a sign context with a NULL key: the key already on the + * context must be reused (OpenSSL reinit contract). */ +int test_mldsa_reinit_null_key(void* data) +{ + static const unsigned char msg[16] = "mldsa-reinit-msg"; + int err = 0; + EVP_PKEY* k = NULL; + EVP_MD_CTX* mdctx = NULL; + unsigned char* sig = NULL; + size_t sigLen = 0; + + (void)data; + PRINT_MSG("Reinit with NULL key reuses context key"); + + err = mldsa_keygen("ML-DSA-44", &k); + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, k, + NULL) != 1; + } + /* Reinit with NULL pkey: reuse the key already attached. */ + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, NULL, + NULL) != 1; + if (err) PRINT_ERR_MSG("Reinit with NULL key failed"); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, NULL, &sigLen, msg, sizeof(msg)) != 1; + } + if (err == 0) { + sig = (unsigned char*)OPENSSL_malloc(sigLen); + err = (sig == NULL); + } + if (err == 0) { + err = EVP_DigestSign(mdctx, sig, &sigLen, msg, sizeof(msg)) != 1; + } + if (err == 0) { + err = mldsa_verify_msg(k, msg, sizeof(msg), sig, sigLen) != 1; + if (err) PRINT_ERR_MSG("Sign after NULL-key reinit did not verify"); + } + + OPENSSL_free(sig); + EVP_MD_CTX_free(mdctx); + EVP_PKEY_free(k); + return err; +} + #endif /* WP_HAVE_MLDSA */ diff --git a/test/unit.c b/test/unit.c index 0689fe59..57e19879 100644 --- a/test/unit.c +++ b/test/unit.c @@ -506,6 +506,8 @@ TEST_CASE test_case[] = { TEST_DECL(test_mldsa_get_params, NULL), TEST_DECL(test_mldsa_digest_sign_init_rejects_md, NULL), TEST_DECL(test_mldsa_import_mismatched_pubpriv, NULL), + TEST_DECL(test_mldsa_empty_message, NULL), + TEST_DECL(test_mldsa_reinit_null_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 f97ee785..ad0d5ee6 100644 --- a/test/unit.h +++ b/test/unit.h @@ -504,6 +504,8 @@ int test_mldsa_oneshot_sign_verify(void *data); int test_mldsa_get_params(void *data); int test_mldsa_digest_sign_init_rejects_md(void *data); int test_mldsa_import_mismatched_pubpriv(void *data); +int test_mldsa_empty_message(void *data); +int test_mldsa_reinit_null_key(void *data); #endif #endif /* UNIT_H */ From 65953c2033f27fc3a84d9ad44145e4d956e4f6d6 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 4 Jun 2026 12:59:47 -0700 Subject: [PATCH 19/43] PQC: address review nits (drop experimental mentions, early-return inits) --- docs/INTEGRATION_GUIDE.md | 4 ++-- include/wolfprovider/settings.h | 2 +- scripts/utils-wolfssl.sh | 3 +-- src/wp_mldsa_sig.c | 24 +++++++++--------------- src/wp_mlkem_kem.c | 16 ++++++---------- 5 files changed, 19 insertions(+), 30 deletions(-) diff --git a/docs/INTEGRATION_GUIDE.md b/docs/INTEGRATION_GUIDE.md index ee90c561..bf7ba664 100644 --- a/docs/INTEGRATION_GUIDE.md +++ b/docs/INTEGRATION_GUIDE.md @@ -83,7 +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-mldsa` | ML-KEM and ML-DSA post-quantum algorithms (wolfSSL post-v5.9.1-stable). The `build-wolfprovider.sh --enable-pqc` flag sets these automatically. Neither algorithm requires `--enable-experimental`. | +| `--enable-mlkem --enable-mldsa` | 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:** @@ -175,7 +175,7 @@ ML-DSA uses pure mode with an empty context string (FIPS 204 sec 5.2, Algorithm ./scripts/build-wolfprovider.sh --enable-pqc ``` -This adds `--enable-mlkem --enable-mldsa` to the wolfSSL configure step (neither flag requires `--enable-experimental`). wolfProvider auto-detects the resulting `WOLFSSL_HAVE_MLKEM` / `WOLFSSL_HAVE_MLDSA` macros via `include/wolfprovider/settings.h` (gated on `__has_include` of `` / ``) and registers the six PQC algorithms. +This adds `--enable-mlkem --enable-mldsa` to the wolfSSL configure step. wolfProvider auto-detects the resulting `WOLFSSL_HAVE_MLKEM` / `WOLFSSL_HAVE_MLDSA` macros via `include/wolfprovider/settings.h` (gated on `__has_include` of `` / ``) and registers the six PQC algorithms. ### Usage Example diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index c21dd817..18498506 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -169,7 +169,7 @@ #ifdef HAVE_ED448 #define WP_HAVE_ED448 #endif -/* PQC: gate on both the wolfSSL feature macro AND header availability. The +/* Gate on both the wolfSSL feature macro AND header availability. The * canonical post-rename names (WOLFSSL_HAVE_MLKEM / WOLFSSL_HAVE_MLDSA and * wc_mlkem.h / wc_mldsa.h) are required. Older wolfSSL releases that only * exposed the pre-standardization names (HAVE_DILITHIUM, dilithium.h) are diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index c1be940c..270b0b1f 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -39,8 +39,7 @@ if [ "$WOLFPROV_SEED_SRC" = "1" ]; then fi # Enable ML-KEM and ML-DSA in wolfSSL when --enable-pqc is requested. -# Use the canonical FIPS 203 / FIPS 204 flag names. Neither algorithm -# requires --enable-experimental anymore. +# Use the canonical FIPS 203 / FIPS 204 flag names. if [ "$WOLFPROV_PQC" = "1" ]; then WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mlkem --enable-mldsa" fi diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index f84b5e28..90130cf4 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -220,31 +220,25 @@ static wp_MlDsaSigCtx* wp_mldsa_dupctx(wp_MlDsaSigCtx* srcCtx) static int wp_mldsa_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { - int ok = 1; - (void)params; if (ctx == NULL) { - ok = 0; + return 0; } /* NULL key means "reinit, reuse the key already on the context" -- only * valid if the context actually has one. */ - if (ok && (mldsa == NULL) && (ctx->mldsa == NULL)) { - ok = 0; + if ((mldsa == NULL) && (ctx->mldsa == NULL)) { + return 0; } - if (ok && (mldsa != NULL)) { + if (mldsa != NULL) { if (!wp_mldsa_up_ref(mldsa)) { - ok = 0; + return 0; } - if (ok) { - wp_mldsa_free(ctx->mldsa); - ctx->mldsa = mldsa; - } - } - if (ok) { - wp_mldsa_buf_reset(ctx); + wp_mldsa_free(ctx->mldsa); + ctx->mldsa = mldsa; } - return ok; + wp_mldsa_buf_reset(ctx); + return 1; } static int wp_mldsa_sign_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index e160fd3f..db767cf7 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -124,21 +124,17 @@ static wp_MlKemCtx* wp_mlkem_kem_dupctx(wp_MlKemCtx* srcCtx) 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; + return 0; } - if (ok) { - wp_mlkem_free(ctx->mlkem); - ctx->mlkem = mlkem; + if (!wp_mlkem_up_ref(mlkem)) { + return 0; } - return ok; + wp_mlkem_free(ctx->mlkem); + ctx->mlkem = mlkem; + return 1; } static int wp_mlkem_kem_encapsulate_init(wp_MlKemCtx* ctx, wp_MlKem* mlkem, From 8f4e35cc29b5b6767669ddc7b8ac79133d2e1fa4 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 4 Jun 2026 16:50:41 -0700 Subject: [PATCH 20/43] Add PQC OpenSSL-vector KAT CI, ML-DSA message API, seed/ikme keygen params, and WP_LOG_COMP_PQC tracing --- .github/workflows/wolfssl-pqc-kat.yml | 162 ++++++++++++++ include/wolfprovider/wp_logging.h | 4 +- scripts/test-pqc-kat.sh | 116 ++++++++++ src/wp_logging.c | 1 + src/wp_mldsa_kmgmt.c | 98 +++++++-- src/wp_mldsa_sig.c | 299 ++++++++++++++++++++++++-- src/wp_mlkem_kem.c | 90 +++++++- src/wp_mlkem_kmgmt.c | 95 +++++++- 8 files changed, 812 insertions(+), 53 deletions(-) create mode 100644 .github/workflows/wolfssl-pqc-kat.yml create mode 100755 scripts/test-pqc-kat.sh diff --git a/.github/workflows/wolfssl-pqc-kat.yml b/.github/workflows/wolfssl-pqc-kat.yml new file mode 100644 index 00000000..cdd48235 --- /dev/null +++ b/.github/workflows/wolfssl-pqc-kat.yml @@ -0,0 +1,162 @@ +name: wolfSSL PQC KAT (OpenSSL vectors) + +# Runs OpenSSL's own ML-KEM (FIPS 203) and ML-DSA (FIPS 204) EVP KAT vectors +# (NIST ACVP + Wycheproof, 2602 sub-tests) through wolfProvider using +# OpenSSL's own evp_test harness, unmodified. Closely mirrors the version +# matrix of wolfssl-versions-pqc.yml: a discover job resolves the latest +# wolfSSL -stable tag and the latest OpenSSL release, then the test job runs +# the full KAT under four configurations per PQC-eligible wolfSSL version: +# replace-default (wolfProvider is the compile-time default) +# non-replace (wolfProvider loaded via provider.conf) +# each in normal mode (every file must pass) and force-fail anti-test mode +# (every file must fail, proving wolfProvider truly served the crypto). The +# force_fail axis lives in the matrix; the test step reports a raw result and +# check-workflow-result.sh inverts the expectation, matching the OSP workflows. + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +# TODO(revert before merge): the malformed-key vectors in OpenSSL's PQC KAT +# set only pass with the wolfcrypt FIPS 203/204 decode-validation fix, which +# currently lives on aidangarske/wolfssl branch pqc-decode-validation. The PQC +# rows therefore build wolfSSL from that fork/branch (matrix wolfssl-git). +# Once the fix merges to wolfSSL master, point the PQC rows back at the +# upstream repo and the dynamic latest-stable / master refs (see discover job). + +jobs: + discover-versions: + name: Resolve wolfSSL/OpenSSL matrix + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + openssl-tag: ${{ steps.set-matrix.outputs.openssl-tag }} + steps: + - name: Resolve latest wolfSSL -stable and latest OpenSSL release + 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 + # Latest stable OpenSSL release (exclude pre-releases). The PQC KAT + # vectors require OpenSSL 3.5+ (first release with native ML-KEM / + # ML-DSA and the 30-test_evp_data PQC files). + OSSL=$(git ls-remote --tags --refs \ + https://github.com/openssl/openssl.git 'openssl-3.*' \ + | awk -F/ '{print $NF}' | grep -E '^openssl-3\.[0-9.]+$' \ + | sort -V | tail -n 1) + if [ -z "${OSSL:-}" ]; then + echo "::error::Could not resolve latest OpenSSL release tag" + exit 1 + fi + echo "Latest stable wolfSSL: $LATEST" + echo "Latest OpenSSL: $OSSL" + echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT" + PQC_FLOOR="v5.9.1-stable" + if [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$LATEST" \ + | sort -V | tail -n1)" != "$PQC_FLOOR" ]; then + LATEST_PQC=true + else + LATEST_PQC=false + fi + # Each PQC version expands to replace-default x non-replace, each in + # normal and force-fail anti-test mode (the matrix owns the force-fail + # axis; the test step hands its raw result to check-workflow-result.sh + # which inverts the expectation). The pre-PQC row builds only. + # + # TODO(revert before merge): the PQC rows build the fork branch + # pqc-decode-validation. Restore upstream and the latest-stable / + # master refs once the wolfcrypt fix is on wolfSSL master. + UPSTREAM="https://github.com/wolfSSL/wolfssl.git" + FORK="https://github.com/aidangarske/wolfssl.git" + MATRIX=$(jq -nc \ + --arg latest "$LATEST" \ + --arg upstream "$UPSTREAM" \ + --arg fork "$FORK" \ + --argjson latest_pqc "$LATEST_PQC" ' + def rows($git; $ref; $pqc; $label): + if $pqc then + [ {"replace":true,"ff":"WOLFPROV_FORCE_FAIL=1", + "sfx":" [replace-default] [force-fail]"}, + {"replace":true,"ff":"","sfx":" [replace-default]"}, + {"replace":false,"ff":"WOLFPROV_FORCE_FAIL=1", + "sfx":" [non-replace] [force-fail]"}, + {"replace":false,"ff":"","sfx":" [non-replace]"} ] + | map({"name":($label+.sfx),"wolfssl-git":$git, + "wolfssl-ref":$ref,"pqc":true,"replace":.replace, + "force_fail":.ff}) + else + [ {"name":($label+" [build-only]"),"wolfssl-git":$git, + "wolfssl-ref":$ref,"pqc":false,"replace":false, + "force_fail":""} ] + end; + { include: + ( rows($upstream; "v5.8.0-stable"; false; "pre-PQC v5.8.0-stable") + + rows($fork; "pqc-decode-validation"; $latest_pqc; + ("latest stable " + $latest)) + + rows($fork; "pqc-decode-validation"; true; "master") ) + }') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + + pqc-kat: + name: ${{ matrix.name }} + needs: discover-versions + runs-on: ubuntu-22.04 + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build wolfProvider (PQC=${{ matrix.pqc }}, replace=${{ matrix.replace }}) + run: | + ARGS="--enable-pqc" + if [ "${{ matrix.replace }}" = "true" ]; then + ARGS="$ARGS --replace-default" + fi + if [ "${{ matrix.pqc }}" != "true" ]; then + ARGS="" + fi + WOLFSSL_GIT=${{ matrix.wolfssl-git }} \ + OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ + ./scripts/build-wolfprovider.sh $ARGS + + # Runs all 2602 OpenSSL ML-KEM/ML-DSA vectors. In normal mode every file + # must pass and the full count must run; in force-fail mode the run fails + # and check-workflow-result.sh inverts that to a pass, proving wolfProvider + # genuinely served the crypto with no silent OpenSSL fallback. + - name: PQC KAT (all 2602 OpenSSL vectors) + if: matrix.pqc == true + shell: bash + run: | + set +e + export ${{ matrix.force_fail }} + WOLFPROV_CLEAN=0 \ + WOLFPROV_REPLACE_DEFAULT=${{ matrix.replace && '1' || '0' }} \ + WOLFSSL_GIT=${{ matrix.wolfssl-git }} \ + OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ + ./scripts/test-pqc-kat.sh + TEST_RESULT=$? + $GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \ + $TEST_RESULT "${{ matrix.force_fail }}" pqc-kat diff --git a/include/wolfprovider/wp_logging.h b/include/wolfprovider/wp_logging.h index 18cab941..2254143a 100644 --- a/include/wolfprovider/wp_logging.h +++ b/include/wolfprovider/wp_logging.h @@ -187,6 +187,7 @@ #define WP_LOG_COMP_QUERY 0x8000000 /* wolfprov_query operations */ #define WP_LOG_COMP_TLS1_PRF 0x10000000 /* TLS1 PRF operations */ #define WP_LOG_COMP_SSHKDF 0x20000000 /* SSHKDF operations */ +#define WP_LOG_COMP_PQC 0x40000000 /* post-quantum: ML-KEM, ML-DSA */ /* log all components */ #define WP_LOG_COMP_ALL ( \ @@ -219,7 +220,8 @@ WP_LOG_COMP_X448 | \ WP_LOG_COMP_QUERY | \ WP_LOG_COMP_TLS1_PRF | \ - WP_LOG_COMP_SSHKDF ) + WP_LOG_COMP_SSHKDF | \ + WP_LOG_COMP_PQC ) /* default components logged */ #define WP_LOG_COMP_DEFAULT WP_LOG_COMP_ALL diff --git a/scripts/test-pqc-kat.sh b/scripts/test-pqc-kat.sh new file mode 100755 index 00000000..ec2b30c8 --- /dev/null +++ b/scripts/test-pqc-kat.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# +# Copyright (C) 2006-2026 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 . + +# Run OpenSSL's own ML-KEM/ML-DSA EVP KAT vectors (NIST ACVP + Wycheproof, +# 2602 sub-tests) through wolfProvider using OpenSSL's own evp_test harness. +# This proves wolfcrypt serves the FIPS 203 / FIPS 204 reference vectors +# unmodified via the OpenSSL provider interface. +# +# The script reports a raw result: exit 0 only when every vector file passes +# and all 2602 sub-tests ran. The caller owns force-fail interpretation: under +# WOLFPROV_FORCE_FAIL=1 every operation fails, so this exits non-zero, and the +# CI job inverts that via check-workflow-result.sh. Build mode (replace-default +# or not) is selected by WOLFPROV_REPLACE_DEFAULT, honored by init_wolfprov. + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +source ${SCRIPT_DIR}/utils-wolfprovider.sh + +# OpenSSL's own KAT data files, run unmodified: the wolfcrypt FIPS 203/204 +# decode fix makes every ML-KEM/ML-DSA vector pass as-is, so nothing is +# staged or edited here. +VECTOR_DIR=${OPENSSL_SOURCE_DIR}/test/recipes/30-test_evp_data +EVP_TEST=${OPENSSL_TEST}/evp_test +EXPECTED_TESTS=2602 + +build_evp_test() { + if [ -x "${EVP_TEST}" ]; then + return 0 + fi + # 'no-tests' only drops test programs from the default build; the Makefile + # still has the rule, so this one target builds with no reconfigure and + # the replace-default patch (in the source files) stays intact. + printf "Building evp_test ...\n" + (cd ${OPENSSL_SOURCE_DIR} && make -j${NUMCPU:-4} test/evp_test >/dev/null 2>&1) + if [ ! -x "${EVP_TEST}" ]; then + printf "ERROR: failed to build evp_test\n" + return 1 + fi +} + +# Make the runtime linker find libwolfprov, mirroring scripts/env-setup. +set_lib_env() { + local libs="${WOLFPROV_INSTALL_DIR}/lib:${WOLFSSL_INSTALL_DIR}/lib" + libs="${libs}:${OPENSSL_INSTALL_DIR}/lib:${OPENSSL_INSTALL_DIR}/lib64" + export LD_LIBRARY_PATH="${libs}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + export DYLD_LIBRARY_PATH="${libs}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + export OPENSSL_MODULES="${WOLFPROV_INSTALL_DIR}/lib" +} + +run_pqc_kat() { + local bad=0 + local files=0 + local total=0 + local out line n + + printf "PQC KAT: %d sub-tests expected across all files\n" \ + "${EXPECTED_TESTS}" + + for f in ${VECTOR_DIR}/evppkey_ml_kem_*.txt \ + ${VECTOR_DIR}/evppkey_ml_dsa_*.txt; do + files=$((files + 1)) + printf "\t%-42s ... " "$(basename ${f})" + out=$(${EVP_TEST} -config ${WOLFPROV_CONFIG} "${f}" 2>&1) + local rc=$? + line=$(echo "${out}" | grep -oE 'Completed [0-9]+ tests') + n=$(echo "${line}" | grep -oE '[0-9]+') + total=$((total + ${n:-0})) + if [ ${rc} -eq 0 ]; then + printf "PASS (%s)\n" "${n:-0}" + else + printf "FAIL\n" + bad=$((bad + 1)) + fi + done + + printf "Ran %d files, %d sub-tests, %d failures\n" \ + "${files}" "${total}" "${bad}" + if [ ${bad} -ne 0 ]; then + return 1 + fi + if [ ${total} -ne ${EXPECTED_TESTS} ]; then + printf "ERROR: expected %d sub-tests, ran %d\n" \ + "${EXPECTED_TESTS}" "${total}" + return 1 + fi + return 0 +} + +if [ -z "${NUMCPU}" ]; then + if [[ "${OSTYPE}" == "darwin"* ]]; then + NUMCPU=$(sysctl -n hw.ncpu) + else + NUMCPU=$(grep -c ^processor /proc/cpuinfo) + fi +fi + +init_wolfprov +set_lib_env +build_evp_test || exit 1 +run_pqc_kat +exit $? diff --git a/src/wp_logging.c b/src/wp_logging.c index 4fe50135..cad93e44 100644 --- a/src/wp_logging.c +++ b/src/wp_logging.c @@ -532,6 +532,7 @@ static void wolfProv_LogComponentToMask(const char* level, size_t len, void* ctx { "WP_LOG_COMP_QUERY", XSTRLEN("WP_LOG_COMP_QUERY"), WP_LOG_COMP_QUERY }, { "WP_LOG_COMP_TLS1_PRF", XSTRLEN("WP_LOG_COMP_TLS1_PRF"), WP_LOG_COMP_TLS1_PRF }, { "WP_LOG_COMP_SSHKDF", XSTRLEN("WP_LOG_COMP_SSHKDF"), WP_LOG_COMP_SSHKDF }, + { "WP_LOG_COMP_PQC", XSTRLEN("WP_LOG_COMP_PQC"), WP_LOG_COMP_PQC }, { "WP_LOG_COMP_ALL", XSTRLEN("WP_LOG_COMP_ALL"), WP_LOG_COMP_ALL }, diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 00734ad1..21868070 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -85,6 +85,9 @@ typedef struct wp_MlDsa wp_MlDsa; /** * ML-DSA key generation context. */ +/* FIPS 204 keygen seed (xi), in bytes. */ +#define WP_MLDSA_SEED_SZ 32 + typedef struct wp_MlDsaGenCtx { /** wolfSSL random number generator. */ WC_RNG rng; @@ -94,6 +97,10 @@ typedef struct wp_MlDsaGenCtx { WOLFPROV_CTX* provCtx; /** Parts of key to generate. */ int selection; + /** Deterministic keygen seed (xi); empty = use RNG. */ + unsigned char seed[WP_MLDSA_SEED_SZ]; + /** Length of seed (0 = not set). */ + size_t seedLen; } wp_MlDsaGenCtx; @@ -138,6 +145,8 @@ int wp_mldsa_up_ref(wp_MlDsa* mldsa) int ok = 1; int rc; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_up_ref"); + rc = wc_LockMutex(&mldsa->mutex); if (rc < 0) { ok = 0; @@ -146,9 +155,12 @@ int wp_mldsa_up_ref(wp_MlDsa* mldsa) mldsa->refCnt++; wc_UnLockMutex(&mldsa->mutex); } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; #else + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_up_ref"); mldsa->refCnt++; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; #endif } @@ -172,9 +184,12 @@ void* wp_mldsa_get_key(wp_MlDsa* mldsa) */ int wp_mldsa_get_sig_size(const wp_MlDsa* mldsa) { + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_get_sig_size"); if (mldsa == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), (int)mldsa->data->sigSize); return (int)mldsa->data->sigSize; } @@ -264,7 +279,7 @@ void wp_mldsa_free(wp_MlDsa* 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. + * @param [in] selection Parts of key (public/private) to duplicate. * @return New ML-DSA key object on success, NULL on failure. */ static wp_MlDsa* wp_mldsa_dup(const wp_MlDsa* src, int selection) @@ -378,6 +393,8 @@ static int wp_mldsa_has(const wp_MlDsa* mldsa, int selection) { int ok = 1; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_has"); + if (!wolfssl_prov_is_running()) { ok = 0; } @@ -390,6 +407,7 @@ static int wp_mldsa_has(const wp_MlDsa* mldsa, int selection) if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { ok &= mldsa->hasPriv; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -412,10 +430,14 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) word32 allocA = 0; word32 allocB = 0; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_match"); + if (!wolfssl_prov_is_running() || (a == NULL) || (b == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if (a->data->level != b->data->level) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { @@ -475,6 +497,7 @@ static int wp_mldsa_match(const wp_MlDsa* a, const wp_MlDsa* b, int selection) OPENSSL_clear_free(bufA, allocA); OPENSSL_clear_free(bufB, allocB); } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -498,6 +521,8 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, unsigned char* derivedPub = NULL; word32 derivedPubLen = 0; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_import"); + if (!wolfssl_prov_is_running() || (mldsa == NULL)) { ok = 0; } @@ -572,20 +597,23 @@ static int wp_mldsa_import(wp_MlDsa* mldsa, int selection, if (ok && (privData == NULL) && (pubData == NULL)) { ok = 0; } - /* When both parts imported, ask wolfSSL to verify they are consistent. - * Catches mismatched pub/priv that the earlier derived-pub probe could - * not (wolfSSL master's ImportPrivRaw does not auto-populate pub). */ - if (ok && (privData != NULL) && (pubData != NULL)) { +#ifdef WOLFSSL_MLDSA_CHECK_KEY + /* Validate the imported private key when the public component is + * available: catches mismatched pub/priv and out-of-range s1/s2 + * coefficients that ImportPrivRaw alone accepts. */ + if (ok && (privData != NULL) && mldsa->hasPub) { if (wc_MlDsaKey_CheckKey(&mldsa->key) != 0) { ok = 0; } } +#endif if (!ok) { /* Clear flags on failure so partial-init state is not advertised. */ mldsa->hasPriv = 0; mldsa->hasPub = 0; } OPENSSL_free(derivedPub); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -657,6 +685,8 @@ static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_export"); + if (!wolfssl_prov_is_running() || (mldsa == NULL)) { ok = 0; } @@ -703,6 +733,7 @@ static int wp_mldsa_export(wp_MlDsa* mldsa, int selection, OPENSSL_free(pubBuf); /* Zero full allocation in case ExportPrivRaw truncated privLen. */ OPENSSL_clear_free(privBuf, privAllocLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -717,6 +748,7 @@ 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_SECURITY_CATEGORY, 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), @@ -739,7 +771,10 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) int rc; OSSL_PARAM* p; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_get_params"); + if (mldsa == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -755,6 +790,13 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) ok = 0; } } + if (ok) { + /* NIST security category equals the ML-DSA level (2, 3 or 5). */ + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_CATEGORY); + if ((p != NULL) && !OSSL_PARAM_set_int(p, (int)mldsa->data->level)) { + ok = 0; + } + } if (ok) { p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_MAX_SIZE); if ((p != NULL) && @@ -819,6 +861,7 @@ static int wp_mldsa_get_params(wp_MlDsa* mldsa, OSSL_PARAM params[]) } } } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -846,8 +889,10 @@ static const OSSL_PARAM* wp_mldsa_settable_params(WOLFPROV_CTX* provCtx) */ static int wp_mldsa_set_params(wp_MlDsa* mldsa, const OSSL_PARAM params[]) { + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_set_params"); (void)mldsa; (void)params; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -915,7 +960,14 @@ static wp_MlDsa* wp_mldsa_gen(wp_MlDsaGenCtx* ctx, OSSL_CALLBACK* osslcb, mldsa = wp_mldsa_new(ctx->provCtx, ctx->data); if ((mldsa != NULL) && keyPair) { - int rc = wc_MlDsaKey_MakeKey(&mldsa->key, &ctx->rng); + int rc; + /* Deterministic keygen from a supplied seed (xi), else RNG. */ + if (ctx->seedLen == WP_MLDSA_SEED_SZ) { + rc = wc_MlDsaKey_MakeKeyFromSeed(&mldsa->key, ctx->seed); + } + else { + rc = wc_MlDsaKey_MakeKey(&mldsa->key, &ctx->rng); + } if (rc != 0) { wp_mldsa_free(mldsa); mldsa = NULL; @@ -929,17 +981,38 @@ static wp_MlDsa* wp_mldsa_gen(wp_MlDsaGenCtx* ctx, OSSL_CALLBACK* osslcb, } /** - * Set parameters into ML-DSA generation context. None supported. + * Set parameters into ML-DSA generation context. * - * @param [in] ctx Generation context. Unused. - * @param [in] params Array of parameters. Unused. - * @return 1 always. + * @param [in] ctx Generation context. + * @param [in] params Array of parameters (ML-DSA keygen seed). + * @return 1 on success, 0 on failure. */ static int wp_mldsa_gen_set_params(wp_MlDsaGenCtx* ctx, const OSSL_PARAM params[]) { - (void)ctx; - (void)params; + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_gen_set_params"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (params == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_ML_DSA_SEED); + if (p != NULL) { + void* vp = ctx->seed; + ctx->seedLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->seed), + &ctx->seedLen)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -954,6 +1027,7 @@ static const OSSL_PARAM* wp_mldsa_gen_settable_params(wp_MlDsaGenCtx* ctx, WOLFPROV_CTX* provCtx) { static OSSL_PARAM wp_mldsa_gen_settable[] = { + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED, NULL, 0), OSSL_PARAM_END }; (void)ctx; diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 90130cf4..d6332402 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -38,6 +38,10 @@ * 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. */ +/* FIPS 204 signing randomizer (rnd) and external-mu sizes, in bytes. */ +#define WP_MLDSA_RND_SZ 32 +#define WP_MLDSA_CTX_MAX 255 + typedef struct wp_MlDsaSigCtx { /** Provider context. */ WOLFPROV_CTX* provCtx; @@ -51,8 +55,23 @@ typedef struct wp_MlDsaSigCtx { size_t mdLen; /** Capacity of mdBuf in bytes. */ size_t mdCap; + /** FIPS 204 context string. */ + unsigned char context[WP_MLDSA_CTX_MAX]; + /** Length of context string. */ + size_t contextLen; + /** Test-only signing randomizer (overrides deterministic/hedged). */ + unsigned char testEntropy[WP_MLDSA_RND_SZ]; + /** Length of test entropy (0 = not set). */ + size_t testEntropyLen; + /** Deterministic signing (rnd = zeros) when set. */ + unsigned int deterministic; + /** External-mu mode: the message IS the 64-byte mu. */ + unsigned int mu; } wp_MlDsaSigCtx; +static int wp_mldsa_set_ctx_params(wp_MlDsaSigCtx* ctx, + const OSSL_PARAM params[]); + /** * Append data into the streaming message buffer. @@ -76,6 +95,8 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, size_t doubled; unsigned char* tmp; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_buf_append"); + needed = ctx->mdLen + dataLen; if (needed < ctx->mdLen) { ok = 0; @@ -114,6 +135,7 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, XMEMCPY(ctx->mdBuf + ctx->mdLen, data, dataLen); ctx->mdLen += dataLen; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -206,6 +228,13 @@ static wp_MlDsaSigCtx* wp_mldsa_dupctx(wp_MlDsaSigCtx* srcCtx) return NULL; } } + /* Carry the signature params so a dup'd context signs identically. */ + XMEMCPY(dstCtx->context, srcCtx->context, srcCtx->contextLen); + dstCtx->contextLen = srcCtx->contextLen; + XMEMCPY(dstCtx->testEntropy, srcCtx->testEntropy, srcCtx->testEntropyLen); + dstCtx->testEntropyLen = srcCtx->testEntropyLen; + dstCtx->deterministic = srcCtx->deterministic; + dstCtx->mu = srcCtx->mu; return dstCtx; } @@ -220,37 +249,55 @@ static wp_MlDsaSigCtx* wp_mldsa_dupctx(wp_MlDsaSigCtx* srcCtx) static int wp_mldsa_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_init"); (void)params; if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } /* NULL key means "reinit, reuse the key already on the context" -- only * valid if the context actually has one. */ if ((mldsa == NULL) && (ctx->mldsa == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if (mldsa != NULL) { if (!wp_mldsa_up_ref(mldsa)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } wp_mldsa_free(ctx->mldsa); ctx->mldsa = mldsa; } wp_mldsa_buf_reset(ctx); + /* Match OpenSSL: re-init clears external-mu but persists the context + * string, deterministic flag and test-entropy until explicitly changed. */ + ctx->mu = 0; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } static int wp_mldsa_sign_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { - return wp_mldsa_init(ctx, mldsa, params); + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_sign_init"); + ok = wp_mldsa_init(ctx, mldsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } static int wp_mldsa_verify_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { - return wp_mldsa_init(ctx, mldsa, params); + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_verify_init"); + ok = wp_mldsa_init(ctx, mldsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /** @@ -266,6 +313,27 @@ static int wp_mldsa_verify_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, * @param [in] msgLen Message length. * @return 1 on success, 0 on failure. */ +/* Fill the 32-byte FIPS 204 signing randomizer: test entropy if supplied, + * zeros when deterministic, otherwise random (hedged). */ +static int wp_mldsa_fill_rnd(wp_MlDsaSigCtx* ctx, unsigned char* rnd) +{ + int rc = 0; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_fill_rnd"); + + if (ctx->testEntropyLen == WP_MLDSA_RND_SZ) { + XMEMCPY(rnd, ctx->testEntropy, WP_MLDSA_RND_SZ); + } + else if (ctx->deterministic) { + XMEMSET(rnd, 0, WP_MLDSA_RND_SZ); + } + else { + rc = wc_RNG_GenerateBlock(&ctx->rng, rnd, WP_MLDSA_RND_SZ); + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), rc); + return rc; +} + static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, size_t* sigLen, size_t sigSize, const unsigned char* msg, size_t msgLen) { @@ -277,12 +345,16 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, unsigned char dummy = 0; const unsigned char* m = msg; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_sign"); + (void)sigSize; if ((ctx == NULL) || (ctx->mldsa == NULL) || (sigLen == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if ((msg == NULL) && (msgLen != 0)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if (m == NULL) { @@ -293,6 +365,7 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, if (sig == NULL) { *sigLen = sigSz; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } if (*sigLen < sigSz) { @@ -305,19 +378,33 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, } if (ok) { word32 outLen = sigSz; - /* 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_MlDsaKey_SignCtx( - (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), NULL, 0, sig, &outLen, - m, (word32)msgLen, &ctx->rng); - if (rc != 0) { + unsigned char rnd[WP_MLDSA_RND_SZ]; + wc_MlDsaKey* key = (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa); + + if (wp_mldsa_fill_rnd(ctx, rnd) != 0) { ok = 0; } + if (ok && ctx->mu) { + /* External-mu mode: the message is the 64-byte mu. */ + rc = wc_MlDsaKey_SignMuWithSeed(key, sig, &outLen, m, + (word32)msgLen, rnd); + if (rc != 0) { + ok = 0; + } + } + else if (ok) { + /* FIPS 204 sec 5.2 pure ML-DSA with the supplied context. */ + rc = wc_MlDsaKey_SignCtxWithSeed(key, ctx->context, + (byte)ctx->contextLen, sig, &outLen, m, (word32)msgLen, rnd); + if (rc != 0) { + ok = 0; + } + } if (ok) { *sigLen = outLen; } } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -341,10 +428,14 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, unsigned char dummy = 0; const unsigned char* m = msg; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_verify"); + if ((ctx == NULL) || (ctx->mldsa == NULL) || (sig == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if ((msg == NULL) && (msgLen != 0)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if (m == NULL) { @@ -353,16 +444,25 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, /* wolfSSL's ML-DSA API takes 32-bit lengths. Reject oversize inputs * explicitly rather than silently truncating. */ if ((sigLen > 0xFFFFFFFFU) || (msgLen > 0xFFFFFFFFU)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - /* Match the sign path: FIPS 204 pure ML-DSA with empty context. */ - rc = wc_MlDsaKey_VerifyCtx( - (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), sig, (word32)sigLen, - NULL, 0, m, (word32)msgLen, &res); + /* Match the sign path: external-mu mode or pure ML-DSA with context. */ + if (ctx->mu) { + rc = wc_MlDsaKey_VerifyMu( + (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), sig, (word32)sigLen, + m, (word32)msgLen, &res); + } + else { + rc = wc_MlDsaKey_VerifyCtx( + (wc_MlDsaKey*)wp_mldsa_get_key(ctx->mldsa), sig, (word32)sigLen, + ctx->context, (byte)ctx->contextLen, m, (word32)msgLen, &res); + } if ((rc != 0) || (res != 1)) { ok = 0; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -379,19 +479,31 @@ static int wp_mldsa_verify(wp_MlDsaSigCtx* ctx, const unsigned char* sig, static int wp_mldsa_digest_sign_init(wp_MlDsaSigCtx* ctx, const char* mdName, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_digest_sign_init"); if ((mdName != NULL) && (mdName[0] != '\0')) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - return wp_mldsa_init(ctx, mldsa, params); + ok = wp_mldsa_init(ctx, mldsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } static int wp_mldsa_digest_verify_init(wp_MlDsaSigCtx* ctx, const char* mdName, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_digest_verify_init"); if ((mdName != NULL) && (mdName[0] != '\0')) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - return wp_mldsa_init(ctx, mldsa, params); + ok = wp_mldsa_init(ctx, mldsa, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /** @@ -405,10 +517,16 @@ static int wp_mldsa_digest_verify_init(wp_MlDsaSigCtx* ctx, const char* mdName, static int wp_mldsa_digest_signverify_update(wp_MlDsaSigCtx* ctx, const unsigned char* data, size_t dataLen) { + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_digest_signverify_update"); if ((ctx == NULL) || (ctx->mldsa == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - return wp_mldsa_buf_append(ctx, data, dataLen); + ok = wp_mldsa_buf_append(ctx, data, dataLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /** @@ -425,10 +543,16 @@ 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) { + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_digest_sign_final"); if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - return wp_mldsa_sign(ctx, sig, sigLen, sigSize, ctx->mdBuf, ctx->mdLen); + ok = wp_mldsa_sign(ctx, sig, sigLen, sigSize, ctx->mdBuf, ctx->mdLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /** @@ -442,17 +566,73 @@ 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) { + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_digest_verify_final"); + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + ok = wp_mldsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/* OpenSSL 3.5+ ML-DSA signature message API. The init carries the FIPS 204 + * signature params (context, deterministic, mu, test-entropy); update/final + * reuse the digest-sign message accumulation. */ +static int wp_mldsa_message_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, + const OSSL_PARAM params[]) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_message_init"); + + ok = wp_mldsa_init(ctx, mldsa, params); + if (ok) { + ok = wp_mldsa_set_ctx_params(ctx, params); + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +static int wp_mldsa_sign_message_final(wp_MlDsaSigCtx* ctx, unsigned char* sig, + size_t* sigLen, size_t sigSize) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_sign_message_final"); + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + ok = wp_mldsa_sign(ctx, sig, sigLen, sigSize, ctx->mdBuf, ctx->mdLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +static int wp_mldsa_verify_message_final(wp_MlDsaSigCtx* ctx, + const unsigned char* sig, size_t sigLen) +{ + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_verify_message_final"); if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } - return wp_mldsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); + ok = wp_mldsa_verify(ctx, sig, sigLen, ctx->mdBuf, ctx->mdLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /* No supported params; OSSL contract is unconditional success. */ static int wp_mldsa_get_ctx_params(wp_MlDsaSigCtx* ctx, OSSL_PARAM* params) { + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_get_ctx_params"); (void)ctx; (void)params; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -467,19 +647,82 @@ static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, return wp_mldsa_gettable; } -/* No supported params; OSSL contract is unconditional success. */ +/* Honor the FIPS 204 signature params OpenSSL drives ML-DSA with: context + * string, deterministic/hedged selection, external-mu, and a test-only + * randomizer. message-encoding must be pure (1); raw is unsupported. */ static int wp_mldsa_set_ctx_params(wp_MlDsaSigCtx* ctx, const OSSL_PARAM params[]) { - (void)ctx; - (void)params; - return 1; + int ok = 1; + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_set_ctx_params"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (params == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_CONTEXT_STRING); + if (p != NULL) { + void* vp = ctx->context; + ctx->contextLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->context), + &ctx->contextLen)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_DETERMINISTIC); + if ((p != NULL) && !OSSL_PARAM_get_uint(p, &ctx->deterministic)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_MU); + if ((p != NULL) && !OSSL_PARAM_get_uint(p, &ctx->mu)) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, + OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING); + if (p != NULL) { + unsigned int enc = 1; + /* Only FIPS 204 pure encoding (1) is supported. */ + if (!OSSL_PARAM_get_uint(p, &enc) || (enc != 1)) { + ok = 0; + } + } + } + if (ok) { + p = OSSL_PARAM_locate_const(params, OSSL_SIGNATURE_PARAM_TEST_ENTROPY); + if (p != NULL) { + void* vp = ctx->testEntropy; + ctx->testEntropyLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->testEntropy), + &ctx->testEntropyLen)) { + ok = 0; + } + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } static const OSSL_PARAM* wp_mldsa_settable_ctx_params(wp_MlDsaSigCtx* ctx, WOLFPROV_CTX* provCtx) { static const OSSL_PARAM wp_mldsa_settable[] = { + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_CONTEXT_STRING, NULL, 0), + OSSL_PARAM_uint(OSSL_SIGNATURE_PARAM_DETERMINISTIC, NULL), + OSSL_PARAM_uint(OSSL_SIGNATURE_PARAM_MU, NULL), + OSSL_PARAM_uint(OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING, NULL), + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_TEST_ENTROPY, NULL, 0), OSSL_PARAM_END }; (void)ctx; @@ -515,6 +758,18 @@ const OSSL_DISPATCH wp_mldsa_signature_functions[] = { (DFUNC)wp_mldsa_digest_signverify_update }, { OSSL_FUNC_SIGNATURE_DIGEST_VERIFY_FINAL, (DFUNC)wp_mldsa_digest_verify_final }, + { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_INIT, + (DFUNC)wp_mldsa_message_init }, + { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_UPDATE, + (DFUNC)wp_mldsa_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_SIGN_MESSAGE_FINAL, + (DFUNC)wp_mldsa_sign_message_final }, + { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_INIT, + (DFUNC)wp_mldsa_message_init }, + { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_UPDATE, + (DFUNC)wp_mldsa_digest_signverify_update }, + { OSSL_FUNC_SIGNATURE_VERIFY_MESSAGE_FINAL, + (DFUNC)wp_mldsa_verify_message_final }, { OSSL_FUNC_SIGNATURE_GET_CTX_PARAMS, (DFUNC)wp_mldsa_get_ctx_params }, { OSSL_FUNC_SIGNATURE_GETTABLE_CTX_PARAMS, diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index db767cf7..5d2ba9ab 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -35,6 +35,9 @@ /** * ML-KEM KEM context. */ +/* FIPS 203 encapsulation entropy (m), in bytes. */ +#define WP_MLKEM_IKME_SZ 32 + typedef struct wp_MlKemCtx { /** Provider context. */ WOLFPROV_CTX* provCtx; @@ -42,6 +45,10 @@ typedef struct wp_MlKemCtx { wp_MlKem* mlkem; /** RNG for encapsulate. */ WC_RNG rng; + /** Test-only encapsulation entropy (ikme); empty = use RNG. */ + unsigned char ikme[WP_MLKEM_IKME_SZ]; + /** Length of ikme (0 = not set). */ + size_t ikmeLen; } wp_MlKemCtx; @@ -118,35 +125,54 @@ static wp_MlKemCtx* wp_mlkem_kem_dupctx(wp_MlKemCtx* srcCtx) * * @param [in, out] ctx KEM context. * @param [in] mlkem ML-KEM key (reference taken). - * @param [in] params Parameters. Unused. + * @param [in] params Init-time parameters (e.g. encap ikme entropy). * @return 1 on success, 0 on failure. */ +static int wp_mlkem_kem_set_ctx_params(wp_MlKemCtx* ctx, + const OSSL_PARAM params[]); + static int wp_mlkem_kem_init(wp_MlKemCtx* ctx, wp_MlKem* mlkem, const OSSL_PARAM params[]) { - (void)params; + int ok; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_init"); if ((ctx == NULL) || (mlkem == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if (!wp_mlkem_up_ref(mlkem)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } wp_mlkem_free(ctx->mlkem); ctx->mlkem = mlkem; - return 1; + /* Apply any init-time params (e.g. the ikme encap entropy). */ + ok = wp_mlkem_kem_set_ctx_params(ctx, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + 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); + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_encapsulate_init"); + ok = wp_mlkem_kem_init(ctx, mlkem, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } 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); + int ok; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_decapsulate_init"); + ok = wp_mlkem_kem_init(ctx, mlkem, params); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /** @@ -169,7 +195,10 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, word32 ctSize; word32 ssSize; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_encapsulate"); + if ((ctx == NULL) || (ctx->mlkem == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -186,10 +215,12 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, if (secretLen != NULL) { *secretLen = ssSize; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } if ((out == NULL) || (secret == NULL) || (outLen == NULL) || (secretLen == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -200,8 +231,16 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, ok = 0; } if (ok) { - int rc = wc_MlKemKey_Encapsulate( - (MlKemKey*)wp_mlkem_get_key(ctx->mlkem), out, secret, &ctx->rng); + int rc; + MlKemKey* key = (MlKemKey*)wp_mlkem_get_key(ctx->mlkem); + /* Deterministic encap from supplied entropy (ikme), else RNG. */ + if (ctx->ikmeLen == WP_MLKEM_IKME_SZ) { + rc = wc_MlKemKey_EncapsulateWithRandom(key, out, secret, ctx->ikme, + (int)ctx->ikmeLen); + } + else { + rc = wc_MlKemKey_Encapsulate(key, out, secret, &ctx->rng); + } if (rc != 0) { ok = 0; } @@ -210,6 +249,7 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, *outLen = ctSize; *secretLen = ssSize; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -233,7 +273,10 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, word32 ssSize; word32 ctSize; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_decapsulate"); + if ((ctx == NULL) || (ctx->mlkem == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -245,9 +288,11 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, if (outLen != NULL) { *outLen = ssSize; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } if ((outLen == NULL) || (in == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -267,14 +312,17 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, if (ok) { *outLen = ssSize; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } /* No supported params; OSSL contract is unconditional success. */ static int wp_mlkem_kem_get_ctx_params(wp_MlKemCtx* ctx, OSSL_PARAM* params) { + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_get_ctx_params"); (void)ctx; (void)params; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -289,12 +337,33 @@ static const OSSL_PARAM* wp_mlkem_kem_gettable_ctx_params(wp_MlKemCtx* ctx, return wp_mlkem_kem_gettable; } -/* No supported params; OSSL contract is unconditional success. */ +/* Honor the test-only encapsulation entropy (ikme) used by ACVP KATs. */ static int wp_mlkem_kem_set_ctx_params(wp_MlKemCtx* ctx, const OSSL_PARAM params[]) { - (void)ctx; - (void)params; + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_kem_set_ctx_params"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (params == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + p = OSSL_PARAM_locate_const(params, OSSL_KEM_PARAM_IKME); + if (p != NULL) { + void* vp = ctx->ikme; + ctx->ikmeLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->ikme), + &ctx->ikmeLen)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -302,6 +371,7 @@ 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_octet_string(OSSL_KEM_PARAM_IKME, NULL, 0), OSSL_PARAM_END }; (void)ctx; diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index 211e3278..614efa4b 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -85,6 +85,9 @@ typedef struct wp_MlKem wp_MlKem; /** * ML-KEM key generation context. */ +/* FIPS 203 keygen seed (d || z), in bytes. */ +#define WP_MLKEM_SEED_SZ 64 + typedef struct wp_MlKemGenCtx { /** wolfSSL random number generator. */ WC_RNG rng; @@ -94,6 +97,10 @@ typedef struct wp_MlKemGenCtx { WOLFPROV_CTX* provCtx; /** Parts of key to generate. */ int selection; + /** Deterministic keygen seed (d || z); empty = use RNG. */ + unsigned char seed[WP_MLKEM_SEED_SZ]; + /** Length of seed (0 = not set). */ + size_t seedLen; } wp_MlKemGenCtx; @@ -138,6 +145,8 @@ int wp_mlkem_up_ref(wp_MlKem* mlkem) int ok = 1; int rc; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_up_ref"); + rc = wc_LockMutex(&mlkem->mutex); if (rc < 0) { ok = 0; @@ -146,9 +155,12 @@ int wp_mlkem_up_ref(wp_MlKem* mlkem) mlkem->refCnt++; wc_UnLockMutex(&mlkem->mutex); } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; #else + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_up_ref"); mlkem->refCnt++; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; #endif } @@ -266,7 +278,7 @@ void wp_mlkem_free(wp_MlKem* 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. + * @param [in] selection Parts of key (public/private) to duplicate. * @return New ML-KEM key object on success, NULL on failure. */ static wp_MlKem* wp_mlkem_dup(const wp_MlKem* src, int selection) @@ -377,6 +389,8 @@ static int wp_mlkem_has(const wp_MlKem* mlkem, int selection) { int ok = 1; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_has"); + if (!wolfssl_prov_is_running()) { ok = 0; } @@ -389,6 +403,7 @@ static int wp_mlkem_has(const wp_MlKem* mlkem, int selection) if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { ok &= mlkem->hasPriv; } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -409,10 +424,14 @@ static int wp_mlkem_match(const wp_MlKem* a, const wp_MlKem* b, int selection) word32 lenB; int rc; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_match"); + if (!wolfssl_prov_is_running() || (a == NULL) || (b == NULL)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if (a->data->type != b->data->type) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { @@ -469,6 +488,7 @@ static int wp_mlkem_match(const wp_MlKem* a, const wp_MlKem* b, int selection) OPENSSL_clear_free(bufA, lenA); OPENSSL_clear_free(bufB, lenB); } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -492,6 +512,8 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, unsigned char* derivedPub = NULL; word32 derivedPubLen = 0; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_import"); + if (!wolfssl_prov_is_running() || (mlkem == NULL)) { ok = 0; } @@ -571,6 +593,7 @@ static int wp_mlkem_import(wp_MlKem* mlkem, int selection, mlkem->hasPub = 0; } OPENSSL_free(derivedPub); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -641,6 +664,8 @@ static int wp_mlkem_export(wp_MlKem* mlkem, int selection, int expPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; int expPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_export"); + if (!wolfssl_prov_is_running() || (mlkem == NULL)) { ok = 0; } @@ -685,6 +710,7 @@ static int wp_mlkem_export(wp_MlKem* mlkem, int selection, } OPENSSL_free(pubBuf); OPENSSL_clear_free(privBuf, privLen); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -699,6 +725,7 @@ 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_SECURITY_CATEGORY, 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), @@ -721,7 +748,10 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) int rc; OSSL_PARAM* p; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_get_params"); + if (mlkem == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } @@ -736,6 +766,22 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) ok = 0; } } + if (ok) { + /* NIST security category: ML-KEM-512=1, 768=3, 1024=5. */ + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_CATEGORY); + if (p != NULL) { + int cat = 5; + if (mlkem->data->securityBits == 128) { + cat = 1; + } + else if (mlkem->data->securityBits == 192) { + cat = 3; + } + if (!OSSL_PARAM_set_int(p, cat)) { + ok = 0; + } + } + } if (ok) { p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_MAX_SIZE); if ((p != NULL) && @@ -797,6 +843,7 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) } } } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -824,8 +871,10 @@ static const OSSL_PARAM* wp_mlkem_settable_params(WOLFPROV_CTX* provCtx) */ static int wp_mlkem_set_params(wp_MlKem* mlkem, const OSSL_PARAM params[]) { + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_set_params"); (void)mlkem; (void)params; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -893,7 +942,15 @@ static wp_MlKem* wp_mlkem_gen(wp_MlKemGenCtx* ctx, OSSL_CALLBACK* osslcb, mlkem = wp_mlkem_new(ctx->provCtx, ctx->data); if ((mlkem != NULL) && keyPair) { - int rc = wc_MlKemKey_MakeKey(&mlkem->key, &ctx->rng); + int rc; + /* Deterministic keygen from a supplied seed (d || z), else RNG. */ + if (ctx->seedLen == WP_MLKEM_SEED_SZ) { + rc = wc_MlKemKey_MakeKeyWithRandom(&mlkem->key, ctx->seed, + (int)ctx->seedLen); + } + else { + rc = wc_MlKemKey_MakeKey(&mlkem->key, &ctx->rng); + } if (rc != 0) { wp_mlkem_free(mlkem); mlkem = NULL; @@ -907,17 +964,38 @@ static wp_MlKem* wp_mlkem_gen(wp_MlKemGenCtx* ctx, OSSL_CALLBACK* osslcb, } /** - * Set parameters into ML-KEM generation context. None supported. + * Set parameters into ML-KEM generation context. * - * @param [in] ctx Generation context. Unused. - * @param [in] params Array of parameters. Unused. - * @return 1 always. + * @param [in] ctx Generation context. + * @param [in] params Array of parameters (ML-KEM keygen seed). + * @return 1 on success, 0 on failure. */ static int wp_mlkem_gen_set_params(wp_MlKemGenCtx* ctx, const OSSL_PARAM params[]) { - (void)ctx; - (void)params; + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_gen_set_params"); + + if (ctx == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + if (params == NULL) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); + return 1; + } + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_ML_KEM_SEED); + if (p != NULL) { + void* vp = ctx->seed; + ctx->seedLen = 0; + if (!OSSL_PARAM_get_octet_string(p, &vp, sizeof(ctx->seed), + &ctx->seedLen)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -932,6 +1010,7 @@ static const OSSL_PARAM* wp_mlkem_gen_settable_params(wp_MlKemGenCtx* ctx, WOLFPROV_CTX* provCtx) { static OSSL_PARAM wp_mlkem_gen_settable[] = { + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_ML_KEM_SEED, NULL, 0), OSSL_PARAM_END }; (void)ctx; From 343d15c41b528d172a8f947d81305b3e77f92957 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 4 Jun 2026 20:13:34 -0700 Subject: [PATCH 21/43] Make ML-KEM/ML-DSA opt-in (--enable-pqc/--enable-mlkem/--enable-mldsa) with version-floor gates and per-algorithm CI coverage --- .github/workflows/wolfssl-pqc-kat.yml | 15 +-- .github/workflows/wolfssl-versions-pqc.yml | 105 ++++++++++-------- README.md | 11 +- configure.ac | 33 ++++++ include/wolfprovider/settings.h | 90 +++++++++------ scripts/build-wolfprovider.sh | 91 +++++++++++++-- scripts/test-pqc-kat.sh | 9 +- scripts/utils-wolfprovider.sh | 10 ++ scripts/utils-wolfssl.sh | 9 +- .../tests/pqc_interop/test_pqc_interop.c | 3 +- 10 files changed, 274 insertions(+), 102 deletions(-) diff --git a/.github/workflows/wolfssl-pqc-kat.yml b/.github/workflows/wolfssl-pqc-kat.yml index cdd48235..0769c704 100644 --- a/.github/workflows/wolfssl-pqc-kat.yml +++ b/.github/workflows/wolfssl-pqc-kat.yml @@ -89,7 +89,7 @@ jobs: --arg upstream "$UPSTREAM" \ --arg fork "$FORK" \ --argjson latest_pqc "$LATEST_PQC" ' - def rows($git; $ref; $pqc; $label): + def rows($git; $ref; $pqc; $lbl): if $pqc then [ {"replace":true,"ff":"WOLFPROV_FORCE_FAIL=1", "sfx":" [replace-default] [force-fail]"}, @@ -97,11 +97,11 @@ jobs: {"replace":false,"ff":"WOLFPROV_FORCE_FAIL=1", "sfx":" [non-replace] [force-fail]"}, {"replace":false,"ff":"","sfx":" [non-replace]"} ] - | map({"name":($label+.sfx),"wolfssl-git":$git, + | map({"name":($lbl+.sfx),"wolfssl-git":$git, "wolfssl-ref":$ref,"pqc":true,"replace":.replace, "force_fail":.ff}) else - [ {"name":($label+" [build-only]"),"wolfssl-git":$git, + [ {"name":($lbl+" [build-only]"),"wolfssl-git":$git, "wolfssl-ref":$ref,"pqc":false,"replace":false, "force_fail":""} ] end; @@ -150,13 +150,10 @@ jobs: shell: bash run: | set +e + # The build step already built the provider with --enable-pqc; the KAT + # only runs against it (no rebuild). Force-fail is a runtime env var. export ${{ matrix.force_fail }} - WOLFPROV_CLEAN=0 \ - WOLFPROV_REPLACE_DEFAULT=${{ matrix.replace && '1' || '0' }} \ - WOLFSSL_GIT=${{ matrix.wolfssl-git }} \ - OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ - WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ - ./scripts/test-pqc-kat.sh + ./scripts/test-pqc-kat.sh TEST_RESULT=$? $GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \ $TEST_RESULT "${{ matrix.force_fail }}" pqc-kat diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 212b4597..7245dcc8 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -6,11 +6,11 @@ name: wolfSSL Versions (PQC) # floor, then the build job runs three rows: pre-PQC floor, dynamically # resolved latest -stable, and master. # -# 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 +# PQC is opt-in (--enable-pqc). PQC_FLOOR is v5.9.1-stable: the wc_MlDsaKey_* +# seed/message API 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. +# PQC rows build with --enable-pqc against the latest OpenSSL (>= 3.6 required); +# older/no-flag rows build without it and verify PQC is absent (opt-in). on: push: @@ -32,8 +32,9 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} latest-stable: ${{ steps.set-matrix.outputs.latest-stable }} + openssl-tag: ${{ steps.set-matrix.outputs.openssl-tag }} steps: - - name: Resolve latest -stable wolfSSL tag + - name: Resolve latest -stable wolfSSL tag and latest OpenSSL release id: set-matrix run: | set -euo pipefail @@ -44,8 +45,19 @@ jobs: echo "::error::Could not resolve latest wolfSSL -stable tag" exit 1 fi + # PQC needs OpenSSL 3.6+, so always build against the latest release. + OSSL=$(git ls-remote --tags --refs \ + https://github.com/openssl/openssl.git 'openssl-3.*' \ + | awk -F/ '{print $NF}' | grep -E '^openssl-3\.[0-9.]+$' \ + | sort -V | tail -n 1) + if [ -z "${OSSL:-}" ]; then + echo "::error::Could not resolve latest OpenSSL release tag" + exit 1 + fi echo "Latest stable wolfSSL: $LATEST" + echo "Latest OpenSSL: $OSSL" echo "latest-stable=$LATEST" >> "$GITHUB_OUTPUT" + echo "openssl-tag=$OSSL" >> "$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 @@ -58,17 +70,26 @@ jobs: LATEST_PQC_ELIGIBLE=false fi echo "latest-stable PQC eligible: $LATEST_PQC_ELIGIBLE" + # Each row carries the build flag (enable) and which PQC test families + # must result (expect: both | mlkem | mldsa | none). This exercises the + # combined, per-algorithm, and opt-in-absent paths. 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} + {"name":"pre-PQC (v5.8.0-stable)", + "wolfssl-ref":"v5.8.0-stable","enable":"","expect":"none"}, + {"name":("latest stable (" + $latest + ")"),"wolfssl-ref":$latest, + "enable":(if $latest_pqc then "--enable-pqc" else "" end), + "expect":(if $latest_pqc then "both" else "none" end)}, + {"name":"master (--enable-pqc)", + "wolfssl-ref":"master","enable":"--enable-pqc","expect":"both"}, + {"name":"master (no flag, opt-in check)", + "wolfssl-ref":"master","enable":"","expect":"none"}, + {"name":"master (--enable-mlkem only)", + "wolfssl-ref":"master","enable":"--enable-mlkem","expect":"mlkem"}, + {"name":"master (--enable-mldsa only)", + "wolfssl-ref":"master","enable":"--enable-mldsa","expect":"mldsa"} ] }') echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" @@ -87,48 +108,44 @@ jobs: with: fetch-depth: 1 - # 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 }}) + # Every row builds against the latest OpenSSL release. PQC needs OpenSSL + # 3.6+ (and the enable flag enforces that floor); the interop step then + # compares wolfProvider against that release's native ML-KEM/ML-DSA. + - name: Build wolfProvider (${{ matrix.enable || 'no PQC flag' }}) run: | - if [ "${{ matrix.pqc }}" = "true" ]; then - OPENSSL_TAG=openssl-3.5.4 \ - WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ - ./scripts/build-wolfprovider.sh --enable-pqc - else - OPENSSL_TAG=openssl-3.5.4 \ - WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ - ./scripts/build-wolfprovider.sh - fi + OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ + ./scripts/build-wolfprovider.sh ${{ matrix.enable }} - # 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 + # Opt-in is per-algorithm: assert exactly the expected PQC test families + # are present (both / mlkem / mldsa / none). This catches a leaked + # algorithm, a missing one, or PQC dragged in without a flag. + - name: Verify PQC test presence matches opt-in (${{ matrix.expect }}) run: | - ./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; } + tests=$(./test/unit.test --list) || exit 1 + kem=0; dsa=0 + printf '%s\n' "$tests" | grep -q 'test_mlkem_keygen' && kem=1 + printf '%s\n' "$tests" | grep -q 'test_mldsa_sign_verify' && dsa=1 + echo "expect=${{ matrix.expect }} mlkem=$kem mldsa=$dsa" + case "${{ matrix.expect }}" in + both) [ "$kem" = 1 ] && [ "$dsa" = 1 ] ;; + mlkem) [ "$kem" = 1 ] && [ "$dsa" = 0 ] ;; + mldsa) [ "$kem" = 0 ] && [ "$dsa" = 1 ] ;; + none) [ "$kem" = 0 ] && [ "$dsa" = 0 ] ;; + *) false ;; + esac || { echo "ERROR: PQC test families do not match expect=${{ matrix.expect }}"; 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 - # in the default provider, so this proves wolfProvider's bytes are - # FIPS 203/204 standards-compliant against two reference implementations. + # Only runs on PQC-enabled rows; the latest OpenSSL (3.6+, the PQC floor) + # 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. # 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 + if: matrix.expect == 'both' run: | LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" \ ./test/pqc_interop.test diff --git a/README.md b/README.md index 49f677d7..cc54332d 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,15 @@ Information on how to configure, build, and test wolfProvider can be found here: * 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`. +PQC is opt-in and requires wolfSSL master/v5.9.2-stable+ and OpenSSL 3.6+. + +* With the script: `./scripts/build-wolfprovider.sh --enable-pqc` + (or `--enable-mlkem` / `--enable-mldsa` for one only) +* Building wolfProvider directly: `./configure --enable-pqc` + (or `--enable-mlkem` / `--enable-mldsa`); build wolfSSL with the matching + `--enable-mlkem` / `--enable-mldsa` and link an OpenSSL 3.6+ + +Without an enable flag no PQC code is compiled, regardless of what wolfSSL enables. * 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) diff --git a/configure.ac b/configure.ac index 7e476c09..3a575b17 100644 --- a/configure.ac +++ b/configure.ac @@ -166,6 +166,35 @@ if test "x$ENABLED_SEED_SRC" = "xyes"; then AM_CFLAGS="$AM_CFLAGS -DWP_HAVE_SEED_SRC" fi +AC_ARG_ENABLE([pqc], + [AS_HELP_STRING([--enable-pqc],[Enable both ML-KEM (FIPS 203) and ML-DSA (FIPS 204). Requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+ (default: disabled).])], + [ ENABLED_PQC=$enableval ], + [ ENABLED_PQC=no ] + ) +AC_ARG_ENABLE([mlkem], + [AS_HELP_STRING([--enable-mlkem],[Enable ML-KEM (FIPS 203) only (default: disabled).])], + [ ENABLED_MLKEM=$enableval ], + [ ENABLED_MLKEM= ] + ) +AC_ARG_ENABLE([mldsa], + [AS_HELP_STRING([--enable-mldsa],[Enable ML-DSA (FIPS 204) only (default: disabled).])], + [ ENABLED_MLDSA=$enableval ], + [ ENABLED_MLDSA= ] + ) + +# --enable-pqc is shorthand for both ML-KEM and ML-DSA, unless one was +# explicitly disabled (--enable-pqc --disable-mldsa keeps ML-DSA off). +if test "x$ENABLED_PQC" = "xyes"; then + if test "x$ENABLED_MLKEM" != "xno"; then ENABLED_MLKEM=yes; fi + if test "x$ENABLED_MLDSA" != "xno"; then ENABLED_MLDSA=yes; fi +fi +if test "x$ENABLED_MLKEM" = "xyes"; then + AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_MLKEM" +fi +if test "x$ENABLED_MLDSA" = "xyes"; then + AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_MLDSA" +fi + # Set OpenSSL lib directory for installing libdefault.so if test "x$ENABLED_REPLACE_DEFAULT" = "xyes"; then if test -d "$OPENSSL_INSTALL_DIR/lib64"; then @@ -220,10 +249,14 @@ echo " * C Flags: $CFLAGS" echo " * Debug enabled: $ax_enable_debug" echo " * Debug silent mode: $ENABLED_DEBUG_SILENT" echo +test "x$ENABLED_MLKEM" = "xyes" || ENABLED_MLKEM=no +test "x$ENABLED_MLDSA" = "xyes" || ENABLED_MLDSA=no echo " Features " echo " * User settings: $ENABLED_USERSETTINGS" echo " * Dynamic provider: $ENABLED_DYNAMIC_PROVIDER" echo " * Replace default: $ENABLED_REPLACE_DEFAULT" +echo " * ML-KEM (FIPS 203): $ENABLED_MLKEM" +echo " * ML-DSA (FIPS 204): $ENABLED_MLDSA" echo "" echo "---" diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index 18498506..a3cdd25a 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -27,6 +27,7 @@ #endif #include #include +#include #define WP_HAVE_DIGEST #if !defined(NO_MD5) @@ -169,42 +170,65 @@ #ifdef HAVE_ED448 #define WP_HAVE_ED448 #endif -/* Gate on both the wolfSSL feature macro AND header availability. The - * canonical post-rename names (WOLFSSL_HAVE_MLKEM / WOLFSSL_HAVE_MLDSA and - * wc_mlkem.h / wc_mldsa.h) are required. Older wolfSSL releases that only - * exposed the pre-standardization names (HAVE_DILITHIUM, dilithium.h) are - * intentionally treated as PQC-absent here so that wolfProvider only ever - * builds against the canonical FIPS 203 / FIPS 204 surface. */ -#ifdef WOLFSSL_HAVE_MLKEM - #if defined(__has_include) - #if __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 +/* PQC is opt-in and per-algorithm: ML-KEM is compiled only when + * build-wolfprovider.sh --enable-mlkem / --enable-pqc defines + * WOLFPROV_HAVE_MLKEM, and ML-DSA only when --enable-mldsa / --enable-pqc + * defines WOLFPROV_HAVE_MLDSA. Without those flags no PQC code is built + * regardless of what the linked wolfSSL enables. Each also requires the + * canonical wolfSSL header (wc_mlkem.h / wc_mldsa.h; older wolfSSL exposing + * only dilithium.h is treated as PQC-absent) and OpenSSL >= 3.6: OpenSSL 3.5 + * has the algorithms but not the seed, ikme, security-category and FIPS 204 + * signature message params wolfProvider uses, which arrived in 3.6. To support + * OpenSSL 3.5 later, add param-name fallbacks and lower this floor. The build + * script enforces these version floors so a wrong version is an explicit error + * rather than a silent no-op. */ +#if !defined(__has_include) + #define WP_MLKEM_HEADER + #define WP_MLDSA_HEADER +#else + #if __has_include() + #define WP_MLKEM_HEADER #endif -#endif -#ifdef WOLFSSL_HAVE_MLDSA - #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 + #if __has_include() + #define WP_MLDSA_HEADER #endif #endif +/* wolfSSL must be master or v5.9.2-stable+: a release newer than v5.9.1, or a + * dev build carrying wc_mldsa.h. wc_mldsa.h is the deliberate post-v5.9.1 + * marker for BOTH algorithms because the PQC seed/message API and that header + * shipped together after v5.9.1; master still reports the v5.9.1 version hex, + * so the hex check alone cannot tell them apart. wc_mlkem.h is NOT used here: + * it already exists in v5.9.1, so it cannot mark "newer than v5.9.1". An + * ML-KEM-only build against a wolfSSL stripped of wc_mldsa.h fails closed with + * the #error below, which is the safe outcome. */ +#if (LIBWOLFSSL_VERSION_HEX > 0x05009001L) || defined(WP_MLDSA_HEADER) + #define WP_WOLFSSL_PQC_CAPABLE +#endif +#if defined(WOLFPROV_HAVE_MLKEM) && defined(WOLFSSL_HAVE_MLKEM) && \ + defined(WP_MLKEM_HEADER) && defined(WP_WOLFSSL_PQC_CAPABLE) && \ + (OPENSSL_VERSION_NUMBER >= 0x30600000L) + #define WP_HAVE_MLKEM + #define WP_HAVE_ML_KEM_512 + #define WP_HAVE_ML_KEM_768 + #define WP_HAVE_ML_KEM_1024 +#endif +#if defined(WOLFPROV_HAVE_MLDSA) && defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WP_MLDSA_HEADER) && defined(WP_WOLFSSL_PQC_CAPABLE) && \ + (OPENSSL_VERSION_NUMBER >= 0x30600000L) + #define WP_HAVE_MLDSA + #define WP_HAVE_ML_DSA_44 + #define WP_HAVE_ML_DSA_65 + #define WP_HAVE_ML_DSA_87 +#endif +/* Fail loudly if PQC was requested but the prerequisites are missing, so a + * direct ./configure (bypassing the build script's version gate) does not + * silently produce a non-PQC build. */ +#if defined(WOLFPROV_HAVE_MLKEM) && !defined(WP_HAVE_MLKEM) + #error "ML-KEM requested but unavailable: needs OpenSSL >= 3.6 and wolfSSL master or v5.9.2-stable+ with ML-KEM." +#endif +#if defined(WOLFPROV_HAVE_MLDSA) && !defined(WP_HAVE_MLDSA) + #error "ML-DSA requested but unavailable: needs OpenSSL >= 3.6 and wolfSSL master or v5.9.2-stable+ with ML-DSA." +#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 4a0438e7..9f4969a8 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -32,8 +32,9 @@ 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-mldsa to wolfSSL configure." + echo " --enable-pqc Enable both ML-KEM and ML-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)." + echo " --enable-mlkem Enable ML-KEM only." + echo " --enable-mldsa Enable ML-DSA only." echo "" echo "Environment Variables:" echo " OPENSSL_TAG OpenSSL tag to use (e.g., openssl-3.5.0)" @@ -53,7 +54,9 @@ 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 " WOLFPROV_PQC If set to 1, enables both ML-KEM and ML-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)" + echo " WOLFPROV_MLKEM If set to 1, enables ML-KEM only" + echo " WOLFPROV_MLDSA If set to 1, enables ML-DSA only" echo "" } @@ -150,7 +153,14 @@ for arg in "$@"; do WOLFPROV_SEED_SRC=1 ;; --enable-pqc) - WOLFPROV_PQC=1 + WOLFPROV_MLKEM=1 + WOLFPROV_MLDSA=1 + ;; + --enable-mlkem) + WOLFPROV_MLKEM=1 + ;; + --enable-mldsa) + WOLFPROV_MLDSA=1 ;; *) args_wrong+="$arg, " @@ -195,6 +205,29 @@ if [ "$WOLFPROV_REPLACE_DEFAULT" = "1" ] && [ "$WOLFPROV_FIPS_BASELINE" = "1" ]; exit 1 fi +# Normalize the PQC flags before any build path reads them. WOLFPROV_PQC is the +# legacy "both algorithms" switch (also a documented env var); WOLFPROV_MLKEM / +# WOLFPROV_MLDSA are the per-algorithm switches. Keep them consistent in both +# directions so either form works. +if [ "$WOLFPROV_PQC" = "1" ]; then + WOLFPROV_MLKEM=1 + WOLFPROV_MLDSA=1 +fi +if [ "$WOLFPROV_MLKEM" = "1" ] || [ "$WOLFPROV_MLDSA" = "1" ]; then + WOLFPROV_PQC=1 +fi + +# The Debian package path builds against the distribution OpenSSL (bookworm +# ships 3.0.x), which has no ML-KEM/ML-DSA and is far below the 3.6 PQC floor, +# so a PQC package cannot compile. Reject it up front rather than producing a +# broken build. Once Debian ships OpenSSL 3.6+, PQC support here is a small +# addition: forward the per-algorithm flags through install-wolfprov.sh and +# debian/rules (mirroring the --debug/--fips flags). +if [ -n "$build_debian" ] && [ "$WOLFPROV_PQC" = "1" ]; then + echo "ERROR: PQC (--enable-pqc/--enable-mlkem/--enable-mldsa) is not supported with --debian; the distro OpenSSL is older than the required 3.6+." + exit 1 +fi + if [ -n "$build_debian" ]; then set -e @@ -216,9 +249,7 @@ if [ -n "$build_debian" ]; then if [ "$WOLFPROV_REPLACE_DEFAULT" = "1" ]; then OPENSSL_OPTS+=" --replace-default" fi - if [ "$WOLFPROV_PQC" = "1" ]; then - WOLFSSL_OPTS+=" --enable-pqc" - fi + # PQC is rejected above for the Debian path (distro OpenSSL is < 3.6). # wolfSSL and OpenSSL are independent and must be built first debian/install-wolfssl.sh $WOLFSSL_OPTS --no-install -r $DEB_OUTPUT_DIR @@ -262,12 +293,58 @@ if [ -n "$args" ]; then echo "" fi +# PQC needs newer wolfSSL/OpenSSL than the repo defaults, so when PQC is +# requested and the user has not pinned a version, default to PQC-capable ones +# (the version gate below still enforces the floors for explicit pins). +if [ "$WOLFPROV_PQC" = "1" ]; then + if [ -z "$WOLFSSL_TAG" ]; then + WOLFSSL_TAG=master + echo "PQC: defaulting WOLFSSL_TAG=master" + fi + if [ -z "$OPENSSL_TAG" ]; then + OPENSSL_TAG=openssl-3.6.0 + echo "PQC: defaulting OPENSSL_TAG=openssl-3.6.0" + fi +fi + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" LOG_FILE=${SCRIPT_DIR}/build-release.log source ${SCRIPT_DIR}/utils-wolfprovider.sh echo "Using openssl: $OPENSSL_TAG, wolfssl: $WOLFSSL_TAG" +# ML-KEM / ML-DSA need the wolfSSL FIPS 203/204 seed and message APIs that land +# after v5.9.1-stable, and the matching OpenSSL provider params that arrive in +# 3.6. Refuse PQC on older releases so the failure is an explicit message, not +# an opaque missing-symbol build error. master and non -stable wolfSSL refs +# (branches/commits) are assumed new enough. +# 'sort -V' is GNU-only; on a host without it skip the gate (a compile-time +# guard in settings.h still rejects too-old versions) rather than misfiring. +if [ "$WOLFPROV_PQC" = "1" ] && ! printf '1\n2\n' | sort -V >/dev/null 2>&1; then + echo "WARNING: 'sort -V' unavailable; skipping PQC version check (compile-time guard still applies)." +elif [ "$WOLFPROV_PQC" = "1" ]; then + PQC_MIN_WOLFSSL="v5.9.2-stable" + case "$WOLFSSL_TAG" in + v*-stable) + if [ "$(printf '%s\n%s\n' "$PQC_MIN_WOLFSSL" "$WOLFSSL_TAG" \ + | sort -V | head -n1)" != "$PQC_MIN_WOLFSSL" ]; then + echo "ERROR: ML-KEM/ML-DSA require wolfSSL master or ${PQC_MIN_WOLFSSL} or higher (got ${WOLFSSL_TAG})." + exit 1 + fi + ;; + esac + PQC_MIN_OPENSSL="openssl-3.6.0" + case "$OPENSSL_TAG" in + openssl-3.*) + if [ "$(printf '%s\n%s\n' "$PQC_MIN_OPENSSL" "$OPENSSL_TAG" \ + | sort -V | head -n1)" != "$PQC_MIN_OPENSSL" ]; then + echo "ERROR: ML-KEM/ML-DSA require ${PQC_MIN_OPENSSL} or higher (got ${OPENSSL_TAG})." + exit 1 + fi + ;; + esac +fi + init_wolfprov exit $? diff --git a/scripts/test-pqc-kat.sh b/scripts/test-pqc-kat.sh index ec2b30c8..4809cedf 100755 --- a/scripts/test-pqc-kat.sh +++ b/scripts/test-pqc-kat.sh @@ -25,8 +25,9 @@ # The script reports a raw result: exit 0 only when every vector file passes # and all 2602 sub-tests ran. The caller owns force-fail interpretation: under # WOLFPROV_FORCE_FAIL=1 every operation fails, so this exits non-zero, and the -# CI job inverts that via check-workflow-result.sh. Build mode (replace-default -# or not) is selected by WOLFPROV_REPLACE_DEFAULT, honored by init_wolfprov. +# CI job inverts that via check-workflow-result.sh. wolfProvider (replace-default +# or not) must already be built by a prior build-wolfprovider.sh step; this +# script does not build it, only runs the KAT against it. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" source ${SCRIPT_DIR}/utils-wolfprovider.sh @@ -109,7 +110,9 @@ if [ -z "${NUMCPU}" ]; then fi fi -init_wolfprov +# wolfProvider must already be built (e.g. build-wolfprovider.sh --enable-pqc). +# This script only runs the KAT against that build; it does not rebuild, so it +# cannot drop the opt-in PQC flags. WOLFPROV_FORCE_FAIL is honored at runtime. set_lib_env build_evp_test || exit 1 run_pqc_kat diff --git a/scripts/utils-wolfprovider.sh b/scripts/utils-wolfprovider.sh index 7e98109a..0c8976ad 100644 --- a/scripts/utils-wolfprovider.sh +++ b/scripts/utils-wolfprovider.sh @@ -106,6 +106,16 @@ install_wolfprov() { WOLFPROV_CONFIG_CFLAGS="${WOLFPROV_CONFIG_CFLAGS} -DWOLFPROV_REPLACE_DEFAULT" fi + # PQC is opt-in and per-algorithm: only the requested algorithm is compiled + # in. Without these flags the PQC code is absent regardless of what the + # linked wolfSSL enables. + if [ "$WOLFPROV_MLKEM" = "1" ]; then + WOLFPROV_CONFIG_OPTS+=" --enable-mlkem" + fi + if [ "$WOLFPROV_MLDSA" = "1" ]; then + WOLFPROV_CONFIG_OPTS+=" --enable-mldsa" + fi + if [ "$WOLFPROV_SEED_SRC" = "1" ]; then WOLFPROV_CONFIG_OPTS+=" --enable-seed-src" fi diff --git a/scripts/utils-wolfssl.sh b/scripts/utils-wolfssl.sh index 270b0b1f..410a33cf 100644 --- a/scripts/utils-wolfssl.sh +++ b/scripts/utils-wolfssl.sh @@ -39,9 +39,12 @@ if [ "$WOLFPROV_SEED_SRC" = "1" ]; then fi # Enable ML-KEM and ML-DSA in wolfSSL when --enable-pqc is requested. -# Use the canonical FIPS 203 / FIPS 204 flag names. -if [ "$WOLFPROV_PQC" = "1" ]; then - WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mlkem --enable-mldsa" +# Use the canonical FIPS 203 / FIPS 204 flag names, per requested algorithm. +if [ "$WOLFPROV_MLKEM" = "1" ]; then + WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mlkem" +fi +if [ "$WOLFPROV_MLDSA" = "1" ]; then + WOLFSSL_CONFIG_OPTS="${WOLFSSL_CONFIG_OPTS} --enable-mldsa" fi WOLFSSL_DEBUG_ASN_TEMPLATE=${DWOLFSSL_DEBUG_ASN_TEMPLATE:-0} diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index b17e692e..a991f64a 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -22,7 +22,8 @@ * * 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+) + * 2. OpenSSL default provider (native ML-KEM / ML-DSA; wolfProvider PQC + * requires OpenSSL 3.6+, so this runs against the latest OpenSSL release) * 3. wolfSSL direct (wc_MlKemKey_* / wc_MlDsaKey_* APIs, no provider) * * For each algorithm at each NIST level, every cross-pair is tested: From 164174c2ce28b7a29917b6091529d47267621a36 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 4 Jun 2026 20:57:42 -0700 Subject: [PATCH 22/43] KAT CI: add --enable-openssl-test so replace-default builds OpenSSL evp_test (replace-default uses no-tests) --- .github/workflows/wolfssl-pqc-kat.yml | 5 +++++ scripts/build-wolfprovider.sh | 4 ++++ scripts/test-pqc-kat.sh | 19 ++++++++----------- scripts/utils-openssl.sh | 5 ++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/wolfssl-pqc-kat.yml b/.github/workflows/wolfssl-pqc-kat.yml index 0769c704..af34304d 100644 --- a/.github/workflows/wolfssl-pqc-kat.yml +++ b/.github/workflows/wolfssl-pqc-kat.yml @@ -136,6 +136,11 @@ jobs: if [ "${{ matrix.pqc }}" != "true" ]; then ARGS="" fi + # The KAT runs OpenSSL's evp_test; replace-default builds omit it + # ('no-tests') unless we ask for it here. + if [ "${{ matrix.pqc }}" = "true" ]; then + ARGS="$ARGS --enable-openssl-test" + fi WOLFSSL_GIT=${{ matrix.wolfssl-git }} \ OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ diff --git a/scripts/build-wolfprovider.sh b/scripts/build-wolfprovider.sh index 9f4969a8..41236bc9 100755 --- a/scripts/build-wolfprovider.sh +++ b/scripts/build-wolfprovider.sh @@ -35,6 +35,7 @@ show_help() { echo " --enable-pqc Enable both ML-KEM and ML-DSA (requires wolfSSL master/v5.9.2+ and OpenSSL 3.6+)." echo " --enable-mlkem Enable ML-KEM only." echo " --enable-mldsa Enable ML-DSA only." + echo " --enable-openssl-test Build OpenSSL with its test suite (e.g. evp_test). For CI that runs OpenSSL's own tests." echo "" echo "Environment Variables:" echo " OPENSSL_TAG OpenSSL tag to use (e.g., openssl-3.5.0)" @@ -162,6 +163,9 @@ for arg in "$@"; do --enable-mldsa) WOLFPROV_MLDSA=1 ;; + --enable-openssl-test) + WOLFPROV_OPENSSL_TEST=1 + ;; *) args_wrong+="$arg, " ;; diff --git a/scripts/test-pqc-kat.sh b/scripts/test-pqc-kat.sh index 4809cedf..f0383dd3 100755 --- a/scripts/test-pqc-kat.sh +++ b/scripts/test-pqc-kat.sh @@ -39,19 +39,16 @@ VECTOR_DIR=${OPENSSL_SOURCE_DIR}/test/recipes/30-test_evp_data EVP_TEST=${OPENSSL_TEST}/evp_test EXPECTED_TESTS=2602 -build_evp_test() { +require_evp_test() { if [ -x "${EVP_TEST}" ]; then return 0 fi - # 'no-tests' only drops test programs from the default build; the Makefile - # still has the rule, so this one target builds with no reconfigure and - # the replace-default patch (in the source files) stays intact. - printf "Building evp_test ...\n" - (cd ${OPENSSL_SOURCE_DIR} && make -j${NUMCPU:-4} test/evp_test >/dev/null 2>&1) - if [ ! -x "${EVP_TEST}" ]; then - printf "ERROR: failed to build evp_test\n" - return 1 - fi + # evp_test must come from the OpenSSL build. A replace-default build omits + # OpenSSL's test suite ('no-tests') unless --enable-openssl-test was passed; + # non-replace builds include it by default. + printf "ERROR: evp_test not found at %s\n" "${EVP_TEST}" + printf " Build with: build-wolfprovider.sh --enable-pqc --enable-openssl-test\n" + return 1 } # Make the runtime linker find libwolfprov, mirroring scripts/env-setup. @@ -114,6 +111,6 @@ fi # This script only runs the KAT against that build; it does not rebuild, so it # cannot drop the opt-in PQC flags. WOLFPROV_FORCE_FAIL is honored at runtime. set_lib_env -build_evp_test || exit 1 +require_evp_test || exit 1 run_pqc_kat exit $? diff --git a/scripts/utils-openssl.sh b/scripts/utils-openssl.sh index 5285aa7a..c94bf582 100755 --- a/scripts/utils-openssl.sh +++ b/scripts/utils-openssl.sh @@ -418,7 +418,10 @@ install_openssl() { if [ "$WOLFPROV_DEBUG" = "1" ]; then CONFIG_CMD+=" enable-trace --debug" fi - if [ "$WOLFPROV_REPLACE_DEFAULT" = "1" ]; then + # Replace-default builds skip the OpenSSL test suite for speed, unless + # --enable-openssl-test asks for it (e.g. CI that runs evp_test). + if [ "$WOLFPROV_REPLACE_DEFAULT" = "1" ] && \ + [ "$WOLFPROV_OPENSSL_TEST" != "1" ]; then CONFIG_CMD+=" no-external-tests no-tests" fi From c0e556f462ab1a107c3c17b5dc354659dbaa7caa Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 09:43:24 -0700 Subject: [PATCH 23/43] Add pure ML-KEM TLS groups (MLKEM512/768/1024) with encoded-public-key support for TLS 1.3 key exchange --- src/wp_mlkem_kmgmt.c | 70 +++++++++++++++++++++++++++++++++++++++----- src/wp_tls_capa.c | 53 +++++++++++++++++++++++---------- 2 files changed, 99 insertions(+), 24 deletions(-) diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index 614efa4b..2c3e8199 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -727,6 +727,7 @@ static const OSSL_PARAM* wp_mlkem_gettable_params(WOLFPROV_CTX* provCtx) OSSL_PARAM_int(OSSL_PKEY_PARAM_SECURITY_BITS, NULL), OSSL_PARAM_int(OSSL_PKEY_PARAM_SECURITY_CATEGORY, NULL), OSSL_PARAM_int(OSSL_PKEY_PARAM_MAX_SIZE, NULL), + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, NULL, 0), 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 @@ -817,6 +818,34 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) } } } + if (ok) { + /* Encoded public key (used by TLS key_share) is the raw ML-KEM + * encapsulation key, identical to the public key. */ + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY); + if (p != NULL) { + word32 outLen = mlkem->data->pubKeySize; + if (!mlkem->hasPub) { + ok = 0; + } + else if (p->data == NULL) { + p->return_size = outLen; + } + else if (p->data_size < outLen) { + p->return_size = outLen; + ok = 0; + } + else { + 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) { @@ -856,6 +885,7 @@ static int wp_mlkem_get_params(wp_MlKem* mlkem, OSSL_PARAM params[]) static const OSSL_PARAM* wp_mlkem_settable_params(WOLFPROV_CTX* provCtx) { static const OSSL_PARAM wp_mlkem_supported_settable_params[] = { + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, NULL, 0), OSSL_PARAM_END }; (void)provCtx; @@ -863,19 +893,43 @@ static const OSSL_PARAM* wp_mlkem_settable_params(WOLFPROV_CTX* provCtx) } /** - * Set ML-KEM key parameters. None supported. + * Set ML-KEM key parameters. Supports the encoded public key so the TLS layer + * can import a peer's ML-KEM encapsulation key from a key_share. * - * @param [in] mlkem ML-KEM key object. Unused. - * @param [in] params Array of parameters. Unused. - * @return 1 always. + * @param [in] mlkem ML-KEM key object. + * @param [in] params Array of parameters. + * @return 1 on success, 0 on failure. */ static int wp_mlkem_set_params(wp_MlKem* mlkem, const OSSL_PARAM params[]) { + int ok = 1; + unsigned char* data = NULL; + size_t len = 0; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mlkem_set_params"); - (void)mlkem; - (void)params; - WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); - return 1; + + if (mlkem == NULL) { + ok = 0; + } + if (ok && !wp_params_get_octet_string_ptr(params, + OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, &data, &len)) { + ok = 0; + } + if (ok && (data != NULL)) { + if (len != mlkem->data->pubKeySize) { + ok = 0; + } + else if (wc_MlKemKey_DecodePublicKey(&mlkem->key, data, + (word32)len) != 0) { + ok = 0; + } + else { + mlkem->hasPub = 1; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } /* diff --git a/src/wp_tls_capa.c b/src/wp_tls_capa.c index cb2c998f..30da195d 100644 --- a/src/wp_tls_capa.c +++ b/src/wp_tls_capa.c @@ -27,6 +27,7 @@ #include #include +#include #include #include "wolfprovider/internal.h" @@ -39,6 +40,7 @@ typedef struct wp_tls_group_consts { int maxTls; /** Maximum TLS version (or 0 for all). */ int minDtls; /** Minimum DTLS version, -1 not supported. */ int maxDtls; /** Maximum DTLS version (or 0 for all). */ + int isKem; /** 1 when the group is a KEM (e.g. ML-KEM). */ } wp_tls_group_consts; #define WP_TLS_12_DOWN TLS1_VERSION , TLS1_2_VERSION @@ -51,21 +53,28 @@ typedef struct wp_tls_group_consts { /** List of group constants. */ static const wp_tls_group_consts wp_group_const_list[35] = { - { WOLFSSL_ECC_SECP192R1 , 80, WP_TLS_12_DOWN, WP_DTLS_12_DOWN }, - { WOLFSSL_ECC_SECP224R1 , 112, WP_TLS_12_DOWN, WP_DTLS_12_DOWN }, - { WOLFSSL_ECC_SECP256R1 , 128, WP_TLS_10_UP , WP_DTLS_10_UP }, - { WOLFSSL_ECC_SECP384R1 , 192, WP_TLS_10_UP , WP_DTLS_10_UP }, - { WOLFSSL_ECC_SECP521R1 , 256, WP_TLS_10_UP , WP_DTLS_10_UP }, - { WOLFSSL_ECC_BRAINPOOLP256R1, 128, WP_TLS_12_DOWN, WP_DTLS_12_DOWN }, - { WOLFSSL_ECC_BRAINPOOLP384R1, 192, WP_TLS_12_DOWN, WP_DTLS_12_DOWN }, - { WOLFSSL_ECC_BRAINPOOLP512R1, 256, WP_TLS_12_DOWN, WP_DTLS_12_DOWN }, - { WOLFSSL_ECC_X25519 , 128, WP_TLS_10_UP , WP_DTLS_10_UP }, - { WOLFSSL_ECC_X448 , 224, WP_TLS_10_UP , WP_DTLS_10_UP }, - { WOLFSSL_FFDHE_2048 , 112, WP_TLS_13_UP , WP_DTLS_NONE }, - { WOLFSSL_FFDHE_3072 , 128, WP_TLS_13_UP , WP_DTLS_NONE }, - { WOLFSSL_FFDHE_4096 , 128, WP_TLS_13_UP , WP_DTLS_NONE }, - { WOLFSSL_FFDHE_6144 , 128, WP_TLS_13_UP , WP_DTLS_NONE }, - { WOLFSSL_FFDHE_8192 , 192, WP_TLS_13_UP , WP_DTLS_NONE }, + { WOLFSSL_ECC_SECP192R1 , 80, WP_TLS_12_DOWN, WP_DTLS_12_DOWN, 0 }, + { WOLFSSL_ECC_SECP224R1 , 112, WP_TLS_12_DOWN, WP_DTLS_12_DOWN, 0 }, + { WOLFSSL_ECC_SECP256R1 , 128, WP_TLS_10_UP , WP_DTLS_10_UP , 0 }, + { WOLFSSL_ECC_SECP384R1 , 192, WP_TLS_10_UP , WP_DTLS_10_UP , 0 }, + { WOLFSSL_ECC_SECP521R1 , 256, WP_TLS_10_UP , WP_DTLS_10_UP , 0 }, + { WOLFSSL_ECC_BRAINPOOLP256R1, 128, WP_TLS_12_DOWN, WP_DTLS_12_DOWN, 0 }, + { WOLFSSL_ECC_BRAINPOOLP384R1, 192, WP_TLS_12_DOWN, WP_DTLS_12_DOWN, 0 }, + { WOLFSSL_ECC_BRAINPOOLP512R1, 256, WP_TLS_12_DOWN, WP_DTLS_12_DOWN, 0 }, + { WOLFSSL_ECC_X25519 , 128, WP_TLS_10_UP , WP_DTLS_10_UP , 0 }, + { WOLFSSL_ECC_X448 , 224, WP_TLS_10_UP , WP_DTLS_10_UP , 0 }, + { WOLFSSL_FFDHE_2048 , 112, WP_TLS_13_UP , WP_DTLS_NONE , 0 }, + { WOLFSSL_FFDHE_3072 , 128, WP_TLS_13_UP , WP_DTLS_NONE , 0 }, + { WOLFSSL_FFDHE_4096 , 128, WP_TLS_13_UP , WP_DTLS_NONE , 0 }, + { WOLFSSL_FFDHE_6144 , 128, WP_TLS_13_UP , WP_DTLS_NONE , 0 }, + { WOLFSSL_FFDHE_8192 , 192, WP_TLS_13_UP , WP_DTLS_NONE , 0 }, +#ifdef WP_HAVE_MLKEM + /* Pure ML-KEM (FIPS 203) groups, by IANA codepoint (512/513/514). + * TLS 1.3 only and flagged as KEMs. */ + { 512 , 128, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, + { 513 , 192, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, + { 514 , 256, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, +#endif }; /** Parameters for a group. Index references constant list. */ @@ -89,6 +98,8 @@ static const wp_tls_group_consts wp_group_const_list[35] = { (int *)&wp_group_const_list[idx].minDtls), \ OSSL_PARAM_int(OSSL_CAPABILITY_TLS_GROUP_MAX_DTLS, \ (int *)&wp_group_const_list[idx].maxDtls), \ + OSSL_PARAM_int(OSSL_CAPABILITY_TLS_GROUP_IS_KEM, \ + (int *)&wp_group_const_list[idx].isKem), \ OSSL_PARAM_END \ } @@ -96,6 +107,11 @@ static const wp_tls_group_consts wp_group_const_list[35] = { #define WP_TLS_GROUP_ENTRY_EC(tlsName, internalName, idx) \ WP_TLS_GROUP_ENTRY(tlsName, internalName, idx, "EC", 3) +/** Parameters for a pure ML-KEM group. The alg name matches the keymgmt/KEM + * wolfProvider registers (ML-KEM-512/768/1024). */ +#define WP_TLS_GROUP_ENTRY_MLKEM(tlsName, idx, alg) \ + WP_TLS_GROUP_ENTRY(tlsName, alg, idx, alg, sizeof(alg)) + /** Parameters for an X25519 group. Index references constant list. */ #define WP_TLS_GROUP_ENTRY_X25519(tlsName, internalName, idx) \ WP_TLS_GROUP_ENTRY(tlsName, internalName, idx, "X25519", 7) @@ -109,7 +125,7 @@ static const wp_tls_group_consts wp_group_const_list[35] = { WP_TLS_GROUP_ENTRY(tlsName, internalName, idx, "DH", 3) /** List of parameters for TLS groups. */ -static const OSSL_PARAM wp_param_group_list[][10] = { +static const OSSL_PARAM wp_param_group_list[][11] = { WP_TLS_GROUP_ENTRY_EC( "secp192r1" , "prime192v1" , 0 ), WP_TLS_GROUP_ENTRY_EC( "P-192" , "prime192v1" , 0 ), WP_TLS_GROUP_ENTRY_EC( "secp224r1" , "secp224r1" , 1 ), @@ -132,6 +148,11 @@ static const OSSL_PARAM wp_param_group_list[][10] = { WP_TLS_GROUP_ENTRY_DH( "ffdhe4096" , "ffdhe4096" , 12), WP_TLS_GROUP_ENTRY_DH( "ffdhe6144" , "ffdhe6144" , 13), WP_TLS_GROUP_ENTRY_DH( "ffdhe8192" , "ffdhe8192" , 14), +#ifdef WP_HAVE_MLKEM + WP_TLS_GROUP_ENTRY_MLKEM( "MLKEM512" , 15, "ML-KEM-512" ), + WP_TLS_GROUP_ENTRY_MLKEM( "MLKEM768" , 16, "ML-KEM-768" ), + WP_TLS_GROUP_ENTRY_MLKEM( "MLKEM1024" , 17, "ML-KEM-1024"), +#endif }; /** Count of supported TLS groups. */ From 9a91a591ab1d97519e5d26f415b6c7ae94620338 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 10:04:31 -0700 Subject: [PATCH 24/43] Add hybrid PQC TLS groups (X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024) --- include/wolfprovider/alg_funcs.h | 51 ++ src/include.am | 2 + src/wp_mlx_kem.c | 516 ++++++++++++ src/wp_mlx_kmgmt.c | 1360 ++++++++++++++++++++++++++++++ src/wp_tls_capa.c | 12 + src/wp_wolfprov.c | 12 + 6 files changed, 1953 insertions(+) create mode 100644 src/wp_mlx_kem.c create mode 100644 src/wp_mlx_kmgmt.c diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 7133595d..9db00ac4 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -175,6 +175,11 @@ typedef void (*DFUNC)(void); #define WP_NAMES_ML_KEM_768 "ML-KEM-768" #define WP_NAMES_ML_KEM_1024 "ML-KEM-1024" +/* Hybrid PQC TLS key-exchange group names. */ +#define WP_NAMES_X25519MLKEM768 "X25519MLKEM768" +#define WP_NAMES_SECP256R1MLKEM768 "SecP256r1MLKEM768" +#define WP_NAMES_SECP384R1MLKEM1024 "SecP384r1MLKEM1024" + /* ML-DSA names (NIST FIPS 204). */ #define WP_NAMES_ML_DSA_44 "ML-DSA-44" #define WP_NAMES_ML_DSA_65 "ML-DSA-65" @@ -245,6 +250,48 @@ word32 wp_mlkem_data_ct_size(const wp_MlKemData* data); * of the parameter set. */ #define WP_MLKEM_SS_SIZE 32 +/* Internal hybrid (ML-KEM + classical ECDH) types and functions. */ +typedef struct wp_Mlx wp_Mlx; + +/** + * Per-group hybrid variant data. Matches OpenSSL's hybrid_vtable so the + * concatenated key_share interoperates with native OpenSSL. + */ +typedef struct wp_MlxData { + /** Classical component type (X25519 or ECC). */ + int classicalType; + /** wolfSSL ECC curve id (when classicalType is ECC). */ + int curveId; + /** wolfSSL ML-KEM parameter type (WC_ML_KEM_768/1024). */ + int mlkemType; + /** ML-KEM public key size in bytes. */ + word32 mlkemPubSize; + /** ML-KEM private key size in bytes. */ + word32 mlkemPrivSize; + /** ML-KEM ciphertext size in bytes. */ + word32 mlkemCtSize; + /** Classical public key size in bytes (encoded). */ + word32 classicalPubSize; + /** Classical private key size in bytes. */ + word32 classicalPrivSize; + /** Classical shared secret size in bytes. */ + word32 classicalShSecSize; + /** Slot the ML-KEM component occupies (0 or 1) in the concatenation. */ + int mlkemSlot; + /** Security bits (those of the ML-KEM component). */ + int securityBits; + /** Group/algorithm name string. */ + const char* name; +} wp_MlxData; + +int wp_mlx_up_ref(wp_Mlx* mlx); +void wp_mlx_free(wp_Mlx* mlx); +void* wp_mlx_get_mlkem_key(wp_Mlx* mlx); +void* wp_mlx_get_classical_key(wp_Mlx* mlx); +const wp_MlxData* wp_mlx_get_data(const wp_Mlx* mlx); +int wp_mlx_has_pub(const wp_Mlx* mlx); +int wp_mlx_has_priv(const wp_Mlx* mlx); + /* Internal ML-DSA types and functions. */ typedef struct wp_MlDsa wp_MlDsa; @@ -364,6 +411,7 @@ 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[]; +extern const OSSL_DISPATCH wp_mlx_asym_kem_functions[]; /* Key Management implementations. */ extern const OSSL_DISPATCH wp_rsa_keymgmt_functions[]; @@ -380,6 +428,9 @@ 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_mlx_x25519_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mlx_p256_keymgmt_functions[]; +extern const OSSL_DISPATCH wp_mlx_p384_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[]; diff --git a/src/include.am b/src/include.am index 21db6007..156e11fc 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_mlx_kmgmt.c +libwolfprov_la_SOURCES += src/wp_mlx_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 diff --git a/src/wp_mlx_kem.c b/src/wp_mlx_kem.c new file mode 100644 index 00000000..0f44fdd6 --- /dev/null +++ b/src/wp_mlx_kem.c @@ -0,0 +1,516 @@ +/* wp_mlx_kem.c + * + * Copyright (C) 2006-2026 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 +#include +#include + +/** Classical component is X25519 (must match wp_mlx_kmgmt.c). */ +#define WP_MLX_CLASSICAL_X25519 0 + +/** + * Hybrid KEM context. + */ +typedef struct wp_MlxCtx { + /** Provider context. */ + WOLFPROV_CTX* provCtx; + /** wolfProvider hybrid key (owned reference). */ + wp_Mlx* mlx; + /** RNG for ML-KEM encapsulation and classical key generation. */ + WC_RNG rng; +} wp_MlxCtx; + + +/** + * Create a new hybrid KEM context object. + * + * @param [in] provCtx Provider context. + * @return New KEM context on success, NULL on failure. + */ +static wp_MlxCtx* wp_mlx_kem_newctx(WOLFPROV_CTX* provCtx) +{ + wp_MlxCtx* ctx = NULL; + + if (wolfssl_prov_is_running()) { + ctx = (wp_MlxCtx*)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 a hybrid KEM context object. + * + * @param [in, out] ctx KEM context. May be NULL. + */ +static void wp_mlx_kem_freectx(wp_MlxCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + wp_mlx_free(ctx->mlx); + OPENSSL_free(ctx); + } +} + +/** + * Duplicate a hybrid KEM context. + * + * @param [in] srcCtx Source KEM context. + * @return Duplicated context on success, NULL on failure. + */ +static wp_MlxCtx* wp_mlx_kem_dupctx(wp_MlxCtx* srcCtx) +{ + wp_MlxCtx* dstCtx = NULL; + + if ((!wolfssl_prov_is_running()) || (srcCtx == NULL)) { + return NULL; + } + + dstCtx = wp_mlx_kem_newctx(srcCtx->provCtx); + if (dstCtx == NULL) { + return NULL; + } + if (srcCtx->mlx != NULL) { + if (!wp_mlx_up_ref(srcCtx->mlx)) { + wp_mlx_kem_freectx(dstCtx); + return NULL; + } + dstCtx->mlx = srcCtx->mlx; + } + return dstCtx; +} + +/** + * Initialize a hybrid KEM context with a key. + * + * @param [in, out] ctx KEM context. + * @param [in] mlx Hybrid key (reference taken). + * @param [in] params Init-time parameters. Unused. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_kem_init(wp_MlxCtx* ctx, wp_Mlx* mlx, + const OSSL_PARAM params[]) +{ + (void)params; + + if ((ctx == NULL) || (mlx == NULL)) { + return 0; + } + if (!wp_mlx_up_ref(mlx)) { + return 0; + } + wp_mlx_free(ctx->mlx); + ctx->mlx = mlx; + return 1; +} + +static int wp_mlx_kem_encapsulate_init(wp_MlxCtx* ctx, wp_Mlx* mlx, + const OSSL_PARAM params[]) +{ + if (!wp_mlx_has_pub(mlx)) { + return 0; + } + return wp_mlx_kem_init(ctx, mlx, params); +} + +static int wp_mlx_kem_decapsulate_init(wp_MlxCtx* ctx, wp_Mlx* mlx, + const OSSL_PARAM params[]) +{ + if (!wp_mlx_has_priv(mlx)) { + return 0; + } + return wp_mlx_kem_init(ctx, mlx, params); +} + +/** + * Compute the classical ECDH shared secret with an ephemeral private key and + * write the ephemeral public key (for the ciphertext). + * + * @param [in] ctx KEM context. + * @param [out] cbuf Buffer for the ephemeral classical public key. + * @param [out] sbuf Buffer for the classical shared secret. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_kem_classical_encap(wp_MlxCtx* ctx, unsigned char* cbuf, + unsigned char* sbuf) +{ + int ok = 1; + int rc; + const wp_MlxData* data = wp_mlx_get_data(ctx->mlx); + word32 pubLen = data->classicalPubSize; + word32 ssLen = data->classicalShSecSize; + + if (data->classicalType == WP_MLX_CLASSICAL_X25519) { + curve25519_key eph; + + rc = wc_curve25519_init(&eph); + if (rc != 0) { + ok = 0; + } + if (ok) { + rc = wc_curve25519_make_key(&ctx->rng, CURVE25519_KEYSIZE, &eph); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_curve25519_export_public_ex(&eph, cbuf, &pubLen, + EC25519_LITTLE_ENDIAN); + if ((rc != 0) || (pubLen != data->classicalPubSize)) { + ok = 0; + } + } + if (ok) { + rc = wc_curve25519_shared_secret_ex(&eph, + (curve25519_key*)wp_mlx_get_classical_key(ctx->mlx), sbuf, + &ssLen, EC25519_LITTLE_ENDIAN); + if ((rc != 0) || (ssLen != data->classicalShSecSize)) { + ok = 0; + } + } + wc_curve25519_free(&eph); + } + else { + ecc_key eph; + + rc = wc_ecc_init(&eph); + if (rc != 0) { + ok = 0; + } + if (ok) { + rc = wc_ecc_make_key_ex(&ctx->rng, 0, &eph, data->curveId); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_ecc_set_rng(&eph, &ctx->rng); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_ecc_export_x963(&eph, cbuf, &pubLen); + if ((rc != 0) || (pubLen != data->classicalPubSize)) { + ok = 0; + } + } + if (ok) { + PRIVATE_KEY_UNLOCK(); + rc = wc_ecc_shared_secret(&eph, + (ecc_key*)wp_mlx_get_classical_key(ctx->mlx), sbuf, &ssLen); + PRIVATE_KEY_LOCK(); + if ((rc != 0) || (ssLen != data->classicalShSecSize)) { + ok = 0; + } + } + wc_ecc_free(&eph); + } + return ok; +} + +/** + * Encapsulate: ML-KEM encaps plus classical ECDHE, concatenated per slot. + * + * @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_mlx_kem_encapsulate(wp_MlxCtx* ctx, unsigned char* out, + size_t* outLen, unsigned char* secret, size_t* secretLen) +{ + int ok = 1; + int rc; + const wp_MlxData* data; + size_t ctSize; + size_t ssSize; + int slot; + unsigned char* mlkemCt; + unsigned char* mlkemSs; + unsigned char* classicalCt; + unsigned char* classicalSs; + + if ((ctx == NULL) || (ctx->mlx == NULL)) { + return 0; + } + data = wp_mlx_get_data(ctx->mlx); + slot = data->mlkemSlot; + ctSize = (size_t)data->mlkemCtSize + data->classicalPubSize; + ssSize = (size_t)WP_MLKEM_SS_SIZE + data->classicalShSecSize; + + if ((out == NULL) && (secret == NULL)) { + if (outLen != NULL) { + *outLen = ctSize; + } + if (secretLen != NULL) { + *secretLen = ssSize; + } + return 1; + } + if ((out == NULL) || (secret == NULL) || (outLen == NULL) || + (secretLen == NULL)) { + return 0; + } + if ((*outLen < ctSize) || (*secretLen < ssSize)) { + return 0; + } + + /* ML-KEM piece at slot offset; classical piece in the other slot. */ + mlkemCt = out + (size_t)slot * data->classicalPubSize; + mlkemSs = secret + (size_t)slot * data->classicalShSecSize; + classicalCt = out + (size_t)(1 - slot) * data->mlkemCtSize; + classicalSs = secret + (size_t)(1 - slot) * WP_MLKEM_SS_SIZE; + + rc = wc_MlKemKey_Encapsulate((MlKemKey*)wp_mlx_get_mlkem_key(ctx->mlx), + mlkemCt, mlkemSs, &ctx->rng); + if (rc != 0) { + ok = 0; + } + if (ok) { + ok = wp_mlx_kem_classical_encap(ctx, classicalCt, classicalSs); + } + if (ok) { + *outLen = ctSize; + *secretLen = ssSize; + } + return ok; +} + +/** + * Compute the classical ECDH shared secret on the decapsulation side. + * + * @param [in] ctx KEM context. + * @param [in] cbuf Peer's classical public key from the ciphertext. + * @param [out] sbuf Buffer for the classical shared secret. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_kem_classical_decap(wp_MlxCtx* ctx, const unsigned char* cbuf, + unsigned char* sbuf) +{ + int ok = 1; + int rc; + const wp_MlxData* data = wp_mlx_get_data(ctx->mlx); + word32 ssLen = data->classicalShSecSize; + + if (data->classicalType == WP_MLX_CLASSICAL_X25519) { + curve25519_key peer; + + rc = wc_curve25519_init(&peer); + if (rc != 0) { + ok = 0; + } + if (ok) { + rc = wc_curve25519_import_public_ex(cbuf, data->classicalPubSize, + &peer, EC25519_LITTLE_ENDIAN); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_curve25519_shared_secret_ex( + (curve25519_key*)wp_mlx_get_classical_key(ctx->mlx), &peer, + sbuf, &ssLen, EC25519_LITTLE_ENDIAN); + if ((rc != 0) || (ssLen != data->classicalShSecSize)) { + ok = 0; + } + } + wc_curve25519_free(&peer); + } + else { + ecc_key peer; + ecc_key* priv = (ecc_key*)wp_mlx_get_classical_key(ctx->mlx); + + rc = wc_ecc_init(&peer); + if (rc != 0) { + ok = 0; + } + if (ok) { + rc = wc_ecc_import_x963_ex(cbuf, data->classicalPubSize, &peer, + data->curveId); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_ecc_set_rng(priv, &ctx->rng); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + PRIVATE_KEY_UNLOCK(); + rc = wc_ecc_shared_secret(priv, &peer, sbuf, &ssLen); + PRIVATE_KEY_LOCK(); + if ((rc != 0) || (ssLen != data->classicalShSecSize)) { + ok = 0; + } + } + wc_ecc_free(&peer); + } + return ok; +} + +/** + * Decapsulate: ML-KEM decaps plus classical ECDH, concatenated per slot. + * + * @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_mlx_kem_decapsulate(wp_MlxCtx* ctx, unsigned char* out, + size_t* outLen, const unsigned char* in, size_t inLen) +{ + int ok = 1; + int rc; + const wp_MlxData* data; + size_t ssSize; + size_t ctSize; + int slot; + const unsigned char* mlkemCt; + const unsigned char* classicalCt; + unsigned char* mlkemSs; + unsigned char* classicalSs; + + if ((ctx == NULL) || (ctx->mlx == NULL)) { + return 0; + } + data = wp_mlx_get_data(ctx->mlx); + slot = data->mlkemSlot; + ssSize = (size_t)WP_MLKEM_SS_SIZE + data->classicalShSecSize; + ctSize = (size_t)data->mlkemCtSize + data->classicalPubSize; + + if (out == NULL) { + if (outLen != NULL) { + *outLen = ssSize; + } + return 1; + } + if ((outLen == NULL) || (in == NULL)) { + return 0; + } + if (*outLen < ssSize) { + return 0; + } + if (inLen != ctSize) { + return 0; + } + + mlkemCt = in + (size_t)slot * data->classicalPubSize; + mlkemSs = out + (size_t)slot * data->classicalShSecSize; + classicalCt = in + (size_t)(1 - slot) * data->mlkemCtSize; + classicalSs = out + (size_t)(1 - slot) * WP_MLKEM_SS_SIZE; + + rc = wc_MlKemKey_Decapsulate((MlKemKey*)wp_mlx_get_mlkem_key(ctx->mlx), + mlkemSs, mlkemCt, data->mlkemCtSize); + if (rc != 0) { + ok = 0; + } + if (ok) { + ok = wp_mlx_kem_classical_decap(ctx, classicalCt, classicalSs); + } + if (ok) { + *outLen = ssSize; + } + return ok; +} + +/* No supported params; OSSL contract is unconditional success. */ +static int wp_mlx_kem_get_ctx_params(wp_MlxCtx* ctx, OSSL_PARAM* params) +{ + (void)ctx; + (void)params; + return 1; +} + +static const OSSL_PARAM* wp_mlx_kem_gettable_ctx_params(wp_MlxCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlx_kem_gettable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mlx_kem_gettable; +} + +static int wp_mlx_kem_set_ctx_params(wp_MlxCtx* ctx, const OSSL_PARAM params[]) +{ + (void)ctx; + (void)params; + return 1; +} + +static const OSSL_PARAM* wp_mlx_kem_settable_ctx_params(wp_MlxCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlx_kem_settable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mlx_kem_settable; +} + +/** Dispatch table for the hybrid KEM (shared across all three groups). */ +const OSSL_DISPATCH wp_mlx_asym_kem_functions[] = { + { OSSL_FUNC_KEM_NEWCTX, (DFUNC)wp_mlx_kem_newctx }, + { OSSL_FUNC_KEM_FREECTX, (DFUNC)wp_mlx_kem_freectx }, + { OSSL_FUNC_KEM_DUPCTX, (DFUNC)wp_mlx_kem_dupctx }, + { OSSL_FUNC_KEM_ENCAPSULATE_INIT, (DFUNC)wp_mlx_kem_encapsulate_init }, + { OSSL_FUNC_KEM_ENCAPSULATE, (DFUNC)wp_mlx_kem_encapsulate }, + { OSSL_FUNC_KEM_DECAPSULATE_INIT, (DFUNC)wp_mlx_kem_decapsulate_init }, + { OSSL_FUNC_KEM_DECAPSULATE, (DFUNC)wp_mlx_kem_decapsulate }, + { OSSL_FUNC_KEM_GET_CTX_PARAMS, (DFUNC)wp_mlx_kem_get_ctx_params }, + { OSSL_FUNC_KEM_GETTABLE_CTX_PARAMS, + (DFUNC)wp_mlx_kem_gettable_ctx_params }, + { OSSL_FUNC_KEM_SET_CTX_PARAMS, (DFUNC)wp_mlx_kem_set_ctx_params }, + { OSSL_FUNC_KEM_SETTABLE_CTX_PARAMS, + (DFUNC)wp_mlx_kem_settable_ctx_params }, + { 0, NULL } +}; + +#endif /* WP_HAVE_MLKEM */ diff --git a/src/wp_mlx_kmgmt.c b/src/wp_mlx_kmgmt.c new file mode 100644 index 00000000..57f75629 --- /dev/null +++ b/src/wp_mlx_kmgmt.c @@ -0,0 +1,1360 @@ +/* wp_mlx_kmgmt.c + * + * Copyright (C) 2006-2026 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 +#include + +#ifdef WP_HAVE_MLKEM + +#include +#include +#include + +/** Supported selections (key parts) in this key manager. */ +#define WP_MLX_POSSIBLE_SELECTIONS \ + (OSSL_KEYMGMT_SELECT_KEYPAIR | OSSL_KEYMGMT_SELECT_ALL_PARAMETERS) + +/** Classical component is X25519. */ +#define WP_MLX_CLASSICAL_X25519 0 +/** Classical component is an EC (NIST prime) curve. */ +#define WP_MLX_CLASSICAL_ECC 1 + +/** + * Per-group hybrid variant data. + * + * Matches OpenSSL's hybrid_vtable so the concatenated key_share interoperates. + */ +const wp_MlxData mlxX25519Mlkem768Data = { + WP_MLX_CLASSICAL_X25519, + 0, + 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, + CURVE25519_PUB_KEY_SIZE, + CURVE25519_KEYSIZE, + CURVE25519_KEYSIZE, + 0, + 192, + "X25519MLKEM768" +}; + +const wp_MlxData mlxSecP256r1Mlkem768Data = { + WP_MLX_CLASSICAL_ECC, + ECC_SECP256R1, + 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, + 65, + 32, + 32, + 1, + 192, + "SecP256r1MLKEM768" +}; + +const wp_MlxData mlxSecP384r1Mlkem1024Data = { + WP_MLX_CLASSICAL_ECC, + ECC_SECP384R1, + 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, + 97, + 48, + 48, + 1, + 256, + "SecP384r1MLKEM1024" +}; + +/** + * Hybrid (ML-KEM + classical) key object. + */ +struct wp_Mlx { + /** wolfSSL ML-KEM key. */ + MlKemKey mlkem; + /** Classical key (X25519 or ECC, selected by variant). */ + union { + curve25519_key x25519; + ecc_key ecc; + } classical; + /** Per-group variant data. */ + const wp_MlxData* 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; + /** Classical key object has been initialized. */ + unsigned int classicalInit:1; +}; + +typedef struct wp_Mlx wp_Mlx; + +/** + * Hybrid key generation context. + */ +typedef struct wp_MlxGenCtx { + /** wolfSSL random number generator. */ + WC_RNG rng; + /** Per-group variant data. */ + const wp_MlxData* data; + /** Provider context. */ + WOLFPROV_CTX* provCtx; + /** Parts of key to generate. */ + int selection; +} wp_MlxGenCtx; + + +/** + * Get the wolfSSL ML-KEM key from the hybrid object. + * + * @param [in] mlx Hybrid key object. + * @return Pointer to wolfSSL MlKemKey, returned as void*. + */ +void* wp_mlx_get_mlkem_key(wp_Mlx* mlx) +{ + return &mlx->mlkem; +} + +/** + * Get the classical (X25519/ECC) wolfSSL key from the hybrid object. + * + * @param [in] mlx Hybrid key object. + * @return Pointer to wolfSSL classical key, returned as void*. + */ +void* wp_mlx_get_classical_key(wp_Mlx* mlx) +{ + return &mlx->classical; +} + +/** + * Get the per-group variant data from the hybrid object. + * + * @param [in] mlx Hybrid key object. + * @return Pointer to variant data. + */ +const wp_MlxData* wp_mlx_get_data(const wp_Mlx* mlx) +{ + return mlx->data; +} + +/** + * Whether the hybrid key has a usable public key. + * + * @param [in] mlx Hybrid key object. + * @return 1 when a public key is available, 0 otherwise. + */ +int wp_mlx_has_pub(const wp_Mlx* mlx) +{ + return (mlx != NULL) && mlx->hasPub; +} + +/** + * Whether the hybrid key has a usable private key. + * + * @param [in] mlx Hybrid key object. + * @return 1 when a private key is available, 0 otherwise. + */ +int wp_mlx_has_priv(const wp_Mlx* mlx) +{ + return (mlx != NULL) && mlx->hasPriv; +} + +/** + * Initialize the classical wolfSSL key object for the variant. + * + * @param [in, out] mlx Hybrid key object. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_classical_init(wp_Mlx* mlx) +{ + int rc; + + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_init(&mlx->classical.x25519); + } + else { + rc = wc_ecc_init(&mlx->classical.ecc); + } + if (rc == 0) { + mlx->classicalInit = 1; + } + return rc; +} + +/** + * Free the classical wolfSSL key object. + * + * @param [in, out] mlx Hybrid key object. + */ +static void wp_mlx_classical_free(wp_Mlx* mlx) +{ + if (mlx->classicalInit) { + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + wc_curve25519_free(&mlx->classical.x25519); + } + else { + wc_ecc_free(&mlx->classical.ecc); + } + mlx->classicalInit = 0; + } +} + +/** + * Increment reference count for key. + * + * @param [in, out] mlx Hybrid key object. + * @return 1 on success, 0 on failure. + */ +int wp_mlx_up_ref(wp_Mlx* mlx) +{ +#ifndef WP_SINGLE_THREADED + int ok = 1; + int rc; + + rc = wc_LockMutex(&mlx->mutex); + if (rc < 0) { + ok = 0; + } + if (ok) { + mlx->refCnt++; + wc_UnLockMutex(&mlx->mutex); + } + return ok; +#else + mlx->refCnt++; + return 1; +#endif +} + +/** + * Create a new hybrid key object. + * + * @param [in] provCtx Provider context. + * @param [in] data Per-group variant data. + * @return New hybrid key object on success, NULL on failure. + */ +static wp_Mlx* wp_mlx_new(WOLFPROV_CTX* provCtx, const wp_MlxData* data) +{ + wp_Mlx* mlx = NULL; + + if (wolfssl_prov_is_running() && (data != NULL)) { + mlx = (wp_Mlx*)OPENSSL_zalloc(sizeof(*mlx)); + } + if (mlx != NULL) { + int ok = 1; + int rc; + + rc = wc_MlKemKey_Init(&mlx->mlkem, data->mlkemType, NULL, + INVALID_DEVID); + if (rc != 0) { + ok = 0; + } + if (ok) { + mlx->data = data; + rc = wp_mlx_classical_init(mlx); + if (rc != 0) { + wc_MlKemKey_Free(&mlx->mlkem); + ok = 0; + } + } + #ifndef WP_SINGLE_THREADED + if (ok) { + rc = wc_InitMutex(&mlx->mutex); + if (rc != 0) { + wp_mlx_classical_free(mlx); + wc_MlKemKey_Free(&mlx->mlkem); + ok = 0; + } + } + #endif + if (ok) { + mlx->provCtx = provCtx; + mlx->refCnt = 1; + } + if (!ok) { + OPENSSL_free(mlx); + mlx = NULL; + } + } + + return mlx; +} + +/** + * Dispose of hybrid key object. + * + * @param [in, out] mlx Hybrid key object. May be NULL. + */ +void wp_mlx_free(wp_Mlx* mlx) +{ + if (mlx != NULL) { + int cnt; + #ifndef WP_SINGLE_THREADED + int rc; + + rc = wc_LockMutex(&mlx->mutex); + cnt = --mlx->refCnt; + if (rc == 0) { + wc_UnLockMutex(&mlx->mutex); + } + #else + cnt = --mlx->refCnt; + #endif + + if (cnt == 0) { + #ifndef WP_SINGLE_THREADED + wc_FreeMutex(&mlx->mutex); + #endif + wp_mlx_classical_free(mlx); + wc_MlKemKey_Free(&mlx->mlkem); + OPENSSL_free(mlx); + } + } +} + +/** + * Encode the classical public key into the supplied buffer. + * + * X25519 keys are exported little-endian to match OpenSSL; EC keys use the + * uncompressed X9.63 point encoding. + * + * @param [in] mlx Hybrid key object. + * @param [out] out Buffer to hold the public key. + * @param [in, out] len On in, buffer size; on out, bytes written. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_classical_export_pub(wp_Mlx* mlx, unsigned char* out, + word32* len) +{ + int rc; + + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_export_public_ex(&mlx->classical.x25519, out, len, + EC25519_LITTLE_ENDIAN); + } + else { + rc = wc_ecc_export_x963(&mlx->classical.ecc, out, len); + } + return rc; +} + +/** + * Decode the classical public key from the supplied buffer. + * + * @param [in, out] mlx Hybrid key object. + * @param [in] in Encoded public key. + * @param [in] len Length of encoded public key. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_classical_import_pub(wp_Mlx* mlx, const unsigned char* in, + word32 len) +{ + int rc; + + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_import_public_ex(in, len, &mlx->classical.x25519, + EC25519_LITTLE_ENDIAN); + } + else { + rc = wc_ecc_import_x963(in, len, &mlx->classical.ecc); + } + return rc; +} + +/** + * Encode the classical private key into the supplied buffer. + * + * @param [in] mlx Hybrid key object. + * @param [out] out Buffer to hold the private key. + * @param [in, out] len On in, buffer size; on out, bytes written. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_classical_export_priv(wp_Mlx* mlx, unsigned char* out, + word32* len) +{ + int rc; + + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_export_private_raw_ex(&mlx->classical.x25519, out, + len, EC25519_LITTLE_ENDIAN); + } + else { + rc = wc_ecc_export_private_only(&mlx->classical.ecc, out, len); + } + return rc; +} + +/** + * Duplicate hybrid key object. + * + * @param [in] src Source hybrid key object. + * @param [in] selection Parts of key (public/private) to duplicate. + * @return New hybrid key object on success, NULL on failure. + */ +static wp_Mlx* wp_mlx_dup(const wp_Mlx* src, int selection) +{ + wp_Mlx* dst = NULL; + unsigned char* mPubBuf = NULL; + unsigned char* mPrivBuf = NULL; + unsigned char* cPubBuf = NULL; + unsigned char* cPrivBuf = NULL; + word32 len; + int rc; + int ok = 1; + 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_mlx_new(src->provCtx, src->data); + if (dst == NULL) { + return NULL; + } + + if (dupPub) { + len = src->data->mlkemPubSize; + mPubBuf = (unsigned char*)OPENSSL_malloc(len); + cPubBuf = (unsigned char*)OPENSSL_malloc(src->data->classicalPubSize); + if ((mPubBuf == NULL) || (cPubBuf == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePublicKey((MlKemKey*)&src->mlkem, mPubBuf, + len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_MlKemKey_DecodePublicKey(&dst->mlkem, mPubBuf, len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + len = src->data->classicalPubSize; + rc = wp_mlx_classical_export_pub((wp_Mlx*)src, cPubBuf, &len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wp_mlx_classical_import_pub(dst, cPubBuf, len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPub = 1; + } + } + + if (ok && dupPriv) { + len = src->data->mlkemPrivSize; + mPrivBuf = (unsigned char*)OPENSSL_malloc(len); + cPrivBuf = (unsigned char*)OPENSSL_malloc(src->data->classicalPrivSize); + if ((mPrivBuf == NULL) || (cPrivBuf == NULL)) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePrivateKey((MlKemKey*)&src->mlkem, mPrivBuf, + len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + rc = wc_MlKemKey_DecodePrivateKey(&dst->mlkem, mPrivBuf, len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + len = src->data->classicalPrivSize; + rc = wp_mlx_classical_export_priv((wp_Mlx*)src, cPrivBuf, &len); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + if (src->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_import_private_ex(cPrivBuf, len, + &dst->classical.x25519, EC25519_LITTLE_ENDIAN); + } + else { + rc = wc_ecc_import_private_key_ex(cPrivBuf, len, NULL, 0, + &dst->classical.ecc, src->data->curveId); + } + if (rc != 0) { + ok = 0; + } + } + if (ok) { + dst->hasPriv = 1; + } + } + + OPENSSL_free(mPubBuf); + OPENSSL_free(cPubBuf); + if (mPrivBuf != NULL) { + OPENSSL_clear_free(mPrivBuf, src->data->mlkemPrivSize); + } + if (cPrivBuf != NULL) { + OPENSSL_clear_free(cPrivBuf, src->data->classicalPrivSize); + } + + if (!ok) { + wp_mlx_free(dst); + return NULL; + } + return dst; +} + +/** + * Load a hybrid key from a reference. + * + * @param [in, out] pMlx Pointer to a hybrid key reference. + * @param [in] size Size of reference object. Unused. + * @return Hybrid key object on success. + */ +static const wp_Mlx* wp_mlx_load(const wp_Mlx** pMlx, size_t size) +{ + const wp_Mlx* mlx = *pMlx; + (void)size; + *pMlx = NULL; + return mlx; +} + +/** + * Check hybrid key object has the components required. + * + * @param [in] mlx Hybrid key object. + * @param [in] selection Parts of key required. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_has(const wp_Mlx* mlx, int selection) +{ + int ok = 1; + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (mlx == NULL)) { + ok = 0; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0)) { + ok &= mlx->hasPub; + } + if (ok && ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0)) { + ok &= mlx->hasPriv; + } + return ok; +} + +/** + * Compare two hybrid keys. + * + * @param [in] a First hybrid key. + * @param [in] b Second hybrid key. + * @param [in] selection Parts of key to compare. + * @return 1 if match, 0 otherwise. + */ +static int wp_mlx_match(const wp_Mlx* a, const wp_Mlx* 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 != b->data) { + return 0; + } + if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + lenA = a->data->mlkemPubSize + a->data->classicalPubSize; + bufA = (unsigned char*)OPENSSL_malloc(lenA); + bufB = (unsigned char*)OPENSSL_malloc(lenA); + if ((bufA == NULL) || (bufB == NULL)) { + ok = 0; + } + if (ok) { + lenA = a->data->mlkemPubSize; + rc = wc_MlKemKey_EncodePublicKey((MlKemKey*)&a->mlkem, bufA, lenA); + if (rc == 0) { + rc = wc_MlKemKey_EncodePublicKey((MlKemKey*)&b->mlkem, bufB, + lenA); + } + if (rc != 0) { + ok = 0; + } + } + if (ok && (XMEMCMP(bufA, bufB, lenA) != 0)) { + ok = 0; + } + if (ok) { + lenA = a->data->classicalPubSize; + lenB = b->data->classicalPubSize; + rc = wp_mlx_classical_export_pub((wp_Mlx*)a, bufA, &lenA); + if (rc == 0) { + rc = wp_mlx_classical_export_pub((wp_Mlx*)b, 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 hybrid key material from the concatenated (slot-order) encoding. + * + * @param [in, out] mlx Hybrid key object. + * @param [in] pub Concatenated public key (or NULL). + * @param [in] priv Concatenated private key (or NULL). + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_load_keys(wp_Mlx* mlx, const unsigned char* pub, + const unsigned char* priv) +{ + int ok = 1; + int rc; + size_t mlkemOff; + size_t classicalOff; + int slot = mlx->data->mlkemSlot; + + if (priv != NULL) { + mlkemOff = (size_t)slot * mlx->data->classicalPrivSize; + classicalOff = (size_t)(1 - slot) * mlx->data->mlkemPrivSize; + rc = wc_MlKemKey_DecodePrivateKey(&mlx->mlkem, priv + mlkemOff, + mlx->data->mlkemPrivSize); + if (rc != 0) { + ok = 0; + } + if (ok) { + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_import_private_ex(priv + classicalOff, + mlx->data->classicalPrivSize, &mlx->classical.x25519, + EC25519_LITTLE_ENDIAN); + } + else { + rc = wc_ecc_import_private_key_ex(priv + classicalOff, + mlx->data->classicalPrivSize, NULL, 0, &mlx->classical.ecc, + mlx->data->curveId); + } + if (rc != 0) { + ok = 0; + } + } + if (ok) { + mlx->hasPriv = 1; + mlx->hasPub = 1; + } + } + else if (pub != NULL) { + mlkemOff = (size_t)slot * mlx->data->classicalPubSize; + classicalOff = (size_t)(1 - slot) * mlx->data->mlkemPubSize; + rc = wc_MlKemKey_DecodePublicKey(&mlx->mlkem, pub + mlkemOff, + mlx->data->mlkemPubSize); + if (rc != 0) { + ok = 0; + } + if (ok) { + rc = wp_mlx_classical_import_pub(mlx, pub + classicalOff, + mlx->data->classicalPubSize); + if (rc != 0) { + ok = 0; + } + } + if (ok) { + mlx->hasPub = 1; + } + } + else { + ok = 0; + } + + if (!ok) { + mlx->hasPub = 0; + mlx->hasPriv = 0; + } + return ok; +} + +/** + * Import a hybrid key from parameters. + * + * @param [in, out] mlx Hybrid 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_mlx_import(wp_Mlx* mlx, int selection, const OSSL_PARAM params[]) +{ + int ok = 1; + unsigned char* privData = NULL; + unsigned char* pubData = NULL; + size_t privLen = 0; + size_t pubLen = 0; + + if (!wolfssl_prov_is_running() || (mlx == NULL)) { + ok = 0; + } + if (ok && ((selection & WP_MLX_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) && (privLen != + (size_t)mlx->data->mlkemPrivSize + + mlx->data->classicalPrivSize)) { + ok = 0; + } + } + 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) && (pubLen != + (size_t)mlx->data->mlkemPubSize + + mlx->data->classicalPubSize)) { + ok = 0; + } + } + if (ok && (privData == NULL) && (pubData == NULL)) { + ok = 0; + } + if (ok) { + ok = wp_mlx_load_keys(mlx, pubData, privData); + } + return ok; +} + +/** Hybrid key parameters for import/export type queries. */ +static const OSSL_PARAM wp_mlx_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_mlx_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_mlx_key_params[idx]; +} + +static const OSSL_PARAM* wp_mlx_import_types(int selection) +{ + return wp_mlx_key_types(selection); +} + +static const OSSL_PARAM* wp_mlx_export_types(int selection) +{ + return wp_mlx_key_types(selection); +} + +/** + * Build the concatenated (slot-order) public key encoding. + * + * @param [in] mlx Hybrid key object. + * @param [out] out Buffer of mlkemPubSize + classicalPubSize bytes. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_encode_pub(wp_Mlx* mlx, unsigned char* out) +{ + int rc; + word32 len; + int slot = mlx->data->mlkemSlot; + size_t mlkemOff = (size_t)slot * mlx->data->classicalPubSize; + size_t classicalOff = (size_t)(1 - slot) * mlx->data->mlkemPubSize; + + rc = wc_MlKemKey_EncodePublicKey(&mlx->mlkem, out + mlkemOff, + mlx->data->mlkemPubSize); + if (rc == 0) { + len = mlx->data->classicalPubSize; + rc = wp_mlx_classical_export_pub(mlx, out + classicalOff, &len); + } + return rc; +} + +/** + * Build the concatenated (slot-order) private key encoding. + * + * @param [in] mlx Hybrid key object. + * @param [out] out Buffer of mlkemPrivSize + classicalPrivSize bytes. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_encode_priv(wp_Mlx* mlx, unsigned char* out) +{ + int rc; + word32 len; + int slot = mlx->data->mlkemSlot; + size_t mlkemOff = (size_t)slot * mlx->data->classicalPrivSize; + size_t classicalOff = (size_t)(1 - slot) * mlx->data->mlkemPrivSize; + + rc = wc_MlKemKey_EncodePrivateKey(&mlx->mlkem, out + mlkemOff, + mlx->data->mlkemPrivSize); + if (rc == 0) { + len = mlx->data->classicalPrivSize; + rc = wp_mlx_classical_export_priv(mlx, out + classicalOff, &len); + } + return rc; +} + +/** + * Export hybrid key data via callback. + * + * @param [in] mlx Hybrid 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_mlx_export(wp_Mlx* mlx, 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() || (mlx == NULL)) { + ok = 0; + } + XMEMSET(params, 0, sizeof(params)); + + if (ok && expPub && mlx->hasPub) { + pubLen = mlx->data->mlkemPubSize + mlx->data->classicalPubSize; + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wp_mlx_encode_pub(mlx, pubBuf); + 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 && mlx->hasPriv) { + privLen = mlx->data->mlkemPrivSize + mlx->data->classicalPrivSize; + privBuf = (unsigned char*)OPENSSL_malloc(privLen); + if (privBuf == NULL) { + ok = 0; + } + if (ok) { + rc = wp_mlx_encode_priv(mlx, privBuf); + 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); + if (privBuf != NULL) { + OPENSSL_clear_free(privBuf, privLen); + } + return ok; +} + +/** + * Gettable parameters for hybrid key. + * + * @param [in] provCtx Provider context. Unused. + * @return Array of supported gettable parameters. + */ +static const OSSL_PARAM* wp_mlx_gettable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlx_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_ENCODED_PUBLIC_KEY, NULL, 0), + 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_mlx_supported_gettable_params; +} + +/** + * Fill an octet-string OSSL_PARAM with the concatenated public key. + * + * @param [in] mlx Hybrid key object. + * @param [in, out] p Parameter to populate. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_get_pub_param(wp_Mlx* mlx, OSSL_PARAM* p) +{ + int ok = 1; + word32 outLen = mlx->data->mlkemPubSize + mlx->data->classicalPubSize; + + if (!mlx->hasPub) { + ok = 0; + } + else if (p->data == NULL) { + p->return_size = outLen; + } + else if (p->data_size < outLen) { + p->return_size = outLen; + ok = 0; + } + else { + if (wp_mlx_encode_pub(mlx, (unsigned char*)p->data) != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + return ok; +} + +/** + * Get hybrid key parameters. + * + * @param [in] mlx Hybrid key object. + * @param [in, out] params Array of parameters and values. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_get_params(wp_Mlx* mlx, OSSL_PARAM params[]) +{ + int ok = 1; + OSSL_PARAM* p; + + if (mlx == NULL) { + return 0; + } + + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_BITS); + if ((p != NULL) && !OSSL_PARAM_set_int(p, + (int)(mlx->data->mlkemPubSize + mlx->data->classicalPubSize) * 8)) { + ok = 0; + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_SECURITY_BITS); + if ((p != NULL) && + !OSSL_PARAM_set_int(p, mlx->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)(mlx->data->mlkemCtSize + mlx->data->classicalPubSize))) { + ok = 0; + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PUB_KEY); + if (p != NULL) { + ok = wp_mlx_get_pub_param(mlx, p); + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY); + if (p != NULL) { + ok = wp_mlx_get_pub_param(mlx, p); + } + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_PKEY_PARAM_PRIV_KEY); + if (p != NULL) { + word32 outLen = mlx->data->mlkemPrivSize + + mlx->data->classicalPrivSize; + if (!mlx->hasPriv) { + ok = 0; + } + else if (p->data == NULL) { + p->return_size = outLen; + } + else if (p->data_size < outLen) { + p->return_size = outLen; + ok = 0; + } + else if (wp_mlx_encode_priv(mlx, (unsigned char*)p->data) != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + return ok; +} + +/** + * Settable parameters for hybrid key. + * + * @param [in] provCtx Provider context. Unused. + * @return Settable parameter list. + */ +static const OSSL_PARAM* wp_mlx_settable_params(WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mlx_supported_settable_params[] = { + OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, NULL, 0), + OSSL_PARAM_END + }; + (void)provCtx; + return wp_mlx_supported_settable_params; +} + +/** + * Set hybrid key parameters. Supports importing the peer's concatenated + * key_share via the encoded public key. + * + * @param [in] mlx Hybrid key object. + * @param [in] params Array of parameters. + * @return 1 on success, 0 on failure. + */ +static int wp_mlx_set_params(wp_Mlx* mlx, const OSSL_PARAM params[]) +{ + int ok = 1; + unsigned char* data = NULL; + size_t len = 0; + + if (mlx == NULL) { + ok = 0; + } + if (ok && !wp_params_get_octet_string_ptr(params, + OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, &data, &len)) { + ok = 0; + } + if (ok && (data != NULL)) { + if (len != (size_t)mlx->data->mlkemPubSize + + mlx->data->classicalPubSize) { + ok = 0; + } + else { + ok = wp_mlx_load_keys(mlx, data, NULL); + } + } + return ok; +} + +/* + * Hybrid key generation + */ + +/** + * Create hybrid 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. Unused. + * @param [in] data Per-group variant data. + * @return New generation context on success, NULL on failure. + */ +static wp_MlxGenCtx* wp_mlx_gen_init_base(WOLFPROV_CTX* provCtx, int selection, + const OSSL_PARAM params[], const wp_MlxData* data) +{ + wp_MlxGenCtx* ctx = NULL; + + (void)params; + + if (wolfssl_prov_is_running() && + ((selection & WP_MLX_POSSIBLE_SELECTIONS) != 0)) { + ctx = (wp_MlxGenCtx*)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 the classical (X25519/ECC) key pair. + * + * @param [in, out] mlx Hybrid key object. + * @param [in] rng RNG to use. + * @return 0 on success, negative on failure. + */ +static int wp_mlx_classical_make_key(wp_Mlx* mlx, WC_RNG* rng) +{ + int rc; + + if (mlx->data->classicalType == WP_MLX_CLASSICAL_X25519) { + rc = wc_curve25519_make_key(rng, CURVE25519_KEYSIZE, + &mlx->classical.x25519); + } + else { + rc = wc_ecc_make_key_ex(rng, 0, &mlx->classical.ecc, + mlx->data->curveId); + if (rc == 0) { + rc = wc_ecc_set_rng(&mlx->classical.ecc, rng); + } + } + return rc; +} + +/** + * Generate a hybrid key pair (both ML-KEM and classical components). + * + * @param [in, out] ctx Generation context. + * @param [in] osslcb Progress callback. Unused. + * @param [in] cbarg Argument for callback. Unused. + * @return Hybrid key object on success, NULL on failure. + */ +static wp_Mlx* wp_mlx_gen(wp_MlxGenCtx* ctx, OSSL_CALLBACK* osslcb, void* cbarg) +{ + wp_Mlx* mlx; + int keyPair = (ctx->selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0; + + (void)osslcb; + (void)cbarg; + + mlx = wp_mlx_new(ctx->provCtx, ctx->data); + if ((mlx != NULL) && keyPair) { + int rc; + + rc = wc_MlKemKey_MakeKey(&mlx->mlkem, &ctx->rng); + if (rc == 0) { + rc = wp_mlx_classical_make_key(mlx, &ctx->rng); + } + if (rc != 0) { + wp_mlx_free(mlx); + mlx = NULL; + } + else { + mlx->hasPub = 1; + mlx->hasPriv = 1; + } + } + return mlx; +} + +/** + * Free hybrid generation context. + * + * @param [in, out] ctx Generation context. + */ +static void wp_mlx_gen_cleanup(wp_MlxGenCtx* ctx) +{ + if (ctx != NULL) { + wc_FreeRng(&ctx->rng); + OPENSSL_free(ctx); + } +} + +static int wp_mlx_gen_set_params(wp_MlxGenCtx* ctx, const OSSL_PARAM params[]) +{ + (void)params; + return ctx != NULL; +} + +static const OSSL_PARAM* wp_mlx_gen_settable_params(wp_MlxGenCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static OSSL_PARAM wp_mlx_gen_settable[] = { + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_mlx_gen_settable; +} + +/* Map each hybrid key type to its KEM operation name so OpenSSL fetches the + * matching KEM implementation without relying on fallback lookup. */ +static const char* wp_mlx_x25519_query_operation_name(int op) +{ + (void)op; + return WP_NAMES_X25519MLKEM768; +} + +static const char* wp_mlx_p256_query_operation_name(int op) +{ + (void)op; + return WP_NAMES_SECP256R1MLKEM768; +} + +static const char* wp_mlx_p384_query_operation_name(int op) +{ + (void)op; + return WP_NAMES_SECP384R1MLKEM1024; +} + +/* Per-group new() and gen_init() trampolines. */ + +static wp_Mlx* wp_mlx_x25519_new(WOLFPROV_CTX* provCtx) +{ + return wp_mlx_new(provCtx, &mlxX25519Mlkem768Data); +} + +static wp_Mlx* wp_mlx_p256_new(WOLFPROV_CTX* provCtx) +{ + return wp_mlx_new(provCtx, &mlxSecP256r1Mlkem768Data); +} + +static wp_Mlx* wp_mlx_p384_new(WOLFPROV_CTX* provCtx) +{ + return wp_mlx_new(provCtx, &mlxSecP384r1Mlkem1024Data); +} + +static wp_MlxGenCtx* wp_mlx_x25519_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mlx_gen_init_base(provCtx, selection, params, + &mlxX25519Mlkem768Data); +} + +static wp_MlxGenCtx* wp_mlx_p256_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mlx_gen_init_base(provCtx, selection, params, + &mlxSecP256r1Mlkem768Data); +} + +static wp_MlxGenCtx* wp_mlx_p384_gen_init(WOLFPROV_CTX* provCtx, + int selection, const OSSL_PARAM params[]) +{ + return wp_mlx_gen_init_base(provCtx, selection, params, + &mlxSecP384r1Mlkem1024Data); +} + +/* + * Dispatch tables + */ + +#define IMPLEMENT_MLX_KEYMGMT_DISPATCH(alg) \ +const OSSL_DISPATCH wp_mlx_##alg##_keymgmt_functions[] = { \ + { OSSL_FUNC_KEYMGMT_NEW, (DFUNC)wp_mlx_##alg##_new }, \ + { OSSL_FUNC_KEYMGMT_FREE, (DFUNC)wp_mlx_free }, \ + { OSSL_FUNC_KEYMGMT_DUP, (DFUNC)wp_mlx_dup }, \ + { OSSL_FUNC_KEYMGMT_GEN_INIT, (DFUNC)wp_mlx_##alg##_gen_init }, \ + { OSSL_FUNC_KEYMGMT_GEN_SET_PARAMS, (DFUNC)wp_mlx_gen_set_params }, \ + { OSSL_FUNC_KEYMGMT_GEN_SETTABLE_PARAMS, \ + (DFUNC)wp_mlx_gen_settable_params }, \ + { OSSL_FUNC_KEYMGMT_GEN, (DFUNC)wp_mlx_gen }, \ + { OSSL_FUNC_KEYMGMT_GEN_CLEANUP, (DFUNC)wp_mlx_gen_cleanup }, \ + { OSSL_FUNC_KEYMGMT_LOAD, (DFUNC)wp_mlx_load }, \ + { OSSL_FUNC_KEYMGMT_GET_PARAMS, (DFUNC)wp_mlx_get_params }, \ + { OSSL_FUNC_KEYMGMT_GETTABLE_PARAMS, (DFUNC)wp_mlx_gettable_params }, \ + { OSSL_FUNC_KEYMGMT_SET_PARAMS, (DFUNC)wp_mlx_set_params }, \ + { OSSL_FUNC_KEYMGMT_SETTABLE_PARAMS, (DFUNC)wp_mlx_settable_params }, \ + { OSSL_FUNC_KEYMGMT_HAS, (DFUNC)wp_mlx_has }, \ + { OSSL_FUNC_KEYMGMT_MATCH, (DFUNC)wp_mlx_match }, \ + { OSSL_FUNC_KEYMGMT_IMPORT, (DFUNC)wp_mlx_import }, \ + { OSSL_FUNC_KEYMGMT_IMPORT_TYPES, (DFUNC)wp_mlx_import_types }, \ + { OSSL_FUNC_KEYMGMT_EXPORT, (DFUNC)wp_mlx_export }, \ + { OSSL_FUNC_KEYMGMT_EXPORT_TYPES, (DFUNC)wp_mlx_export_types }, \ + { OSSL_FUNC_KEYMGMT_QUERY_OPERATION_NAME, \ + (DFUNC)wp_mlx_##alg##_query_operation_name }, \ + { 0, NULL } \ +}; + +IMPLEMENT_MLX_KEYMGMT_DISPATCH(x25519) +IMPLEMENT_MLX_KEYMGMT_DISPATCH(p256) +IMPLEMENT_MLX_KEYMGMT_DISPATCH(p384) + +#endif /* WP_HAVE_MLKEM */ diff --git a/src/wp_tls_capa.c b/src/wp_tls_capa.c index 30da195d..18aaba72 100644 --- a/src/wp_tls_capa.c +++ b/src/wp_tls_capa.c @@ -74,6 +74,10 @@ static const wp_tls_group_consts wp_group_const_list[35] = { { 512 , 128, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, { 513 , 192, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, { 514 , 256, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, + /* Hybrid PQC groups, by IANA codepoint. TLS 1.3 only, flagged as KEMs. */ + { 4588 , 192, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, + { 4587 , 192, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, + { 4589 , 256, WP_TLS_13_UP , WP_DTLS_NONE, 1 }, #endif }; @@ -112,6 +116,11 @@ static const wp_tls_group_consts wp_group_const_list[35] = { #define WP_TLS_GROUP_ENTRY_MLKEM(tlsName, idx, alg) \ WP_TLS_GROUP_ENTRY(tlsName, alg, idx, alg, sizeof(alg)) +/** Parameters for a hybrid ML-KEM group. The alg name matches the keymgmt/KEM + * wolfProvider registers (X25519MLKEM768, etc.). */ +#define WP_TLS_GROUP_ENTRY_MLX(name, idx) \ + WP_TLS_GROUP_ENTRY(name, name, idx, name, sizeof(name)) + /** Parameters for an X25519 group. Index references constant list. */ #define WP_TLS_GROUP_ENTRY_X25519(tlsName, internalName, idx) \ WP_TLS_GROUP_ENTRY(tlsName, internalName, idx, "X25519", 7) @@ -152,6 +161,9 @@ static const OSSL_PARAM wp_param_group_list[][11] = { WP_TLS_GROUP_ENTRY_MLKEM( "MLKEM512" , 15, "ML-KEM-512" ), WP_TLS_GROUP_ENTRY_MLKEM( "MLKEM768" , 16, "ML-KEM-768" ), WP_TLS_GROUP_ENTRY_MLKEM( "MLKEM1024" , 17, "ML-KEM-1024"), + WP_TLS_GROUP_ENTRY_MLX( "X25519MLKEM768" , 18 ), + WP_TLS_GROUP_ENTRY_MLX( "SecP256r1MLKEM768" , 19 ), + WP_TLS_GROUP_ENTRY_MLX( "SecP384r1MLKEM1024", 20 ), #endif }; diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index f9f4a708..ff77fec3 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -670,6 +670,12 @@ static const OSSL_ALGORITHM wolfprov_keymgmt[] = { wp_mlkem768_keymgmt_functions, "" }, { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, wp_mlkem1024_keymgmt_functions, "" }, + { WP_NAMES_X25519MLKEM768, WOLFPROV_PROPERTIES, + wp_mlx_x25519_keymgmt_functions, "" }, + { WP_NAMES_SECP256R1MLKEM768, WOLFPROV_PROPERTIES, + wp_mlx_p256_keymgmt_functions, "" }, + { WP_NAMES_SECP384R1MLKEM1024, WOLFPROV_PROPERTIES, + wp_mlx_p384_keymgmt_functions, "" }, #endif #ifdef WP_HAVE_MLDSA { WP_NAMES_ML_DSA_44, WOLFPROV_PROPERTIES, @@ -774,6 +780,12 @@ static const OSSL_ALGORITHM wolfprov_asym_kem[] = { wp_mlkem_asym_kem_functions, "" }, { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, wp_mlkem_asym_kem_functions, "" }, + { WP_NAMES_X25519MLKEM768, WOLFPROV_PROPERTIES, + wp_mlx_asym_kem_functions, "" }, + { WP_NAMES_SECP256R1MLKEM768, WOLFPROV_PROPERTIES, + wp_mlx_asym_kem_functions, "" }, + { WP_NAMES_SECP384R1MLKEM1024, WOLFPROV_PROPERTIES, + wp_mlx_asym_kem_functions, "" }, #endif { NULL, NULL, NULL, NULL } }; From ef4602c6fae2da8068bb4a29041e6981b7ec3ca9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 10:22:52 -0700 Subject: [PATCH 25/43] PQC interop: add TLS 1.3 handshake group interop (wolfProvider <-> default, both directions, all ML-KEM and hybrid groups) --- .../tests/pqc_interop/test_pqc_interop.c | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index a991f64a..3d2e56df 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -36,6 +36,12 @@ * byte encodings are standards-compliant end-to-end, not just internally * round-trippable. * + * A final stage drives a real TLS 1.3 handshake over an in-memory BIO pair + * with wolfProvider on one peer and the OpenSSL default provider on the other, + * in both directions, for every PQC key-exchange group (pure ML-KEM and the + * hybrids X25519MLKEM768 / SecP256r1MLKEM768 / SecP384r1MLKEM1024). This + * proves the negotiated key-share bytes interoperate on the wire. + * * Usage: test_pqc_interop [provider_path] * provider_path defaults to ".libs" (relative to current dir). * Set WOLFPROV_PATH env var to override. @@ -54,6 +60,8 @@ #include #include #include +#include +#include #include @@ -74,6 +82,16 @@ static OSSL_LIB_CTX* wp_ctx; #define oss_ctx ((OSSL_LIB_CTX*)NULL) static OSSL_PROVIDER* wp_prov; static WC_RNG g_rng; +/* Server identity for the TLS-handshake group interop. Loaded once on the + * default library context (which always has PEM decoders); the key's own + * context signs CertificateVerify, independent of the group provider under + * test. */ +static X509* g_cert; +static EVP_PKEY* g_key; + +#ifndef CERTS_DIR +#define CERTS_DIR "certs" +#endif static int load_all(const char* wp_path) { @@ -100,6 +118,23 @@ static int load_all(const char* wp_path) fprintf(stderr, "wc_InitRng failed\n"); ok = 0; } + if (ok) { + BIO* bio = BIO_new_file(CERTS_DIR "/server-cert.pem", "r"); + if (bio != NULL) { + g_cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); + BIO_free(bio); + } + bio = BIO_new_file(CERTS_DIR "/server-key.pem", "r"); + if (bio != NULL) { + g_key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + BIO_free(bio); + } + if ((g_cert == NULL) || (g_key == NULL)) { + fprintf(stderr, "Failed to load TLS server cert/key from %s\n", + CERTS_DIR); + ok = 0; + } + } if (!ok) { if (wp_prov != NULL) { OSSL_PROVIDER_unload(wp_prov); @@ -114,6 +149,8 @@ static int load_all(const char* wp_path) static void unload_all(void) { wc_FreeRng(&g_rng); + if (g_key) EVP_PKEY_free(g_key); + if (g_cert) X509_free(g_cert); if (wp_prov) OSSL_PROVIDER_unload(wp_prov); if (wp_ctx) OSSL_LIB_CTX_free(wp_ctx); } @@ -581,9 +618,136 @@ static int test_mldsa_pair_to_wp(const char* alg, const char* partner) } +/* + * TLS 1.3 group interop - drives a real handshake over an in-memory BIO pair + * with one peer on wolfProvider's library context and the other on OpenSSL's + * default context, proving the key-exchange byte format (ML-KEM ciphertext / + * shared secret, and the classical share for hybrids) interoperates both ways. + */ + +/* Run the handshake to completion over a BIO pair. Returns 1 on success. */ +static int tls_drive_handshake(SSL* client, SSL* server) +{ + int ok = 0; + int i; + int cdone = 0; + int sdone = 0; + int rc; + int err; + + for (i = 0; i < 64; i++) { + if (!cdone) { + rc = SSL_do_handshake(client); + if (rc == 1) { + cdone = 1; + } + else { + err = SSL_get_error(client, rc); + if ((err != SSL_ERROR_WANT_READ) && + (err != SSL_ERROR_WANT_WRITE)) { + break; + } + } + } + if (!sdone) { + rc = SSL_do_handshake(server); + if (rc == 1) { + sdone = 1; + } + else { + err = SSL_get_error(server, rc); + if ((err != SSL_ERROR_WANT_READ) && + (err != SSL_ERROR_WANT_WRITE)) { + break; + } + } + } + if (cdone && sdone) { + ok = 1; + break; + } + } + return ok; +} + +/* One TLS 1.3 handshake for a group. wpServer != 0 puts wolfProvider on the + * server side and the default provider on the client; 0 swaps them. */ +static int test_tls_group(const char* group, int wpServer) +{ + int ok = 0; + OSSL_LIB_CTX* serverLib = wpServer ? wp_ctx : oss_ctx; + OSSL_LIB_CTX* clientLib = wpServer ? oss_ctx : wp_ctx; + SSL_CTX* sctx = NULL; + SSL_CTX* cctx = NULL; + SSL* server = NULL; + SSL* client = NULL; + BIO* sbio = NULL; + BIO* cbio = NULL; + const char* neg = NULL; + + sctx = SSL_CTX_new_ex(serverLib, NULL, TLS_server_method()); + cctx = SSL_CTX_new_ex(clientLib, NULL, TLS_client_method()); + if ((sctx == NULL) || (cctx == NULL)) { + goto end; + } + if (SSL_CTX_use_certificate(sctx, g_cert) != 1) { + goto end; + } + if (SSL_CTX_use_PrivateKey(sctx, g_key) != 1) { + goto end; + } + SSL_CTX_set_verify(cctx, SSL_VERIFY_NONE, NULL); + if ((SSL_CTX_set_min_proto_version(sctx, TLS1_3_VERSION) != 1) || + (SSL_CTX_set_max_proto_version(sctx, TLS1_3_VERSION) != 1) || + (SSL_CTX_set_min_proto_version(cctx, TLS1_3_VERSION) != 1) || + (SSL_CTX_set_max_proto_version(cctx, TLS1_3_VERSION) != 1)) { + goto end; + } + if ((SSL_CTX_set1_groups_list(sctx, group) != 1) || + (SSL_CTX_set1_groups_list(cctx, group) != 1)) { + goto end; + } + + server = SSL_new(sctx); + client = SSL_new(cctx); + if ((server == NULL) || (client == NULL)) { + goto end; + } + if (BIO_new_bio_pair(&cbio, 0, &sbio, 0) != 1) { + goto end; + } + SSL_set_bio(client, cbio, cbio); + SSL_set_bio(server, sbio, sbio); + SSL_set_connect_state(client); + SSL_set_accept_state(server); + + if (!tls_drive_handshake(client, server)) { + goto end; + } + neg = SSL_get0_group_name(client); + ok = (neg != NULL) && (OPENSSL_strcasecmp(neg, group) == 0); + +end: + if (!ok) { + ERR_print_errors_fp(stderr); + } + printf(" %-20s %-9s server -> %-9s client : %s\n", group, + wpServer ? "wolfProv" : "default", wpServer ? "default" : "wolfProv", + ok ? "PASS" : "FAIL"); + if (server != NULL) SSL_free(server); + if (client != NULL) SSL_free(client); + if (sctx != NULL) SSL_CTX_free(sctx); + if (cctx != NULL) SSL_CTX_free(cctx); + return ok; +} + int main(int argc, char* argv[]) { int fail = 0; + const char* groups[] = { + "MLKEM512", "MLKEM768", "MLKEM1024", + "X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024" + }; 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"; @@ -620,6 +784,13 @@ int main(int argc, char* argv[]) if (!test_mldsa_pair_to_wp(mldsa[i], "direct")) fail++; } + printf("\nTLS 1.3 group interop (handshake over BIO pair):\n"); + printf(" (wolfProvider) <-> (OpenSSL default), both directions\n"); + for (i = 0; i < sizeof(groups) / sizeof(groups[0]); i++) { + if (!test_tls_group(groups[i], 1)) fail++; + if (!test_tls_group(groups[i], 0)) fail++; + } + unload_all(); printf("\n%s: %d failure(s)\n", fail == 0 ? "ALL PASS" : "FAILED", fail); return fail ? 1 : 0; From 8b190ba50edaa6461985b3fd97d0d865a9a5ebe9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 11:00:23 -0700 Subject: [PATCH 26/43] Add ML-DSA encoder/decoder (PEM/DER SPKI+PKCS8) so ML-DSA keys and certs load through wolfProvider --- include/wolfprovider/alg_funcs.h | 24 ++ src/wp_mldsa_kmgmt.c | 617 +++++++++++++++++++++++++++++++ src/wp_wolfprov.c | 78 ++++ 3 files changed, 719 insertions(+) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 9db00ac4..9f6c0441 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -470,6 +470,12 @@ extern const OSSL_DISPATCH wp_x448_spki_decoder_functions[]; extern const OSSL_DISPATCH wp_x448_pki_decoder_functions[]; extern const OSSL_DISPATCH wp_ed448_spki_decoder_functions[]; extern const OSSL_DISPATCH wp_ed448_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_pki_decoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_spki_decoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_pki_decoder_functions[]; extern const OSSL_DISPATCH wp_pem_to_der_decoder_functions[]; extern const OSSL_DISPATCH wp_epki_to_pki_decoder_functions[]; /* Encode implementations. */ @@ -529,6 +535,24 @@ extern const OSSL_DISPATCH wp_ed448_pki_der_encoder_functions[]; extern const OSSL_DISPATCH wp_ed448_pki_pem_encoder_functions[]; extern const OSSL_DISPATCH wp_ed448_epki_der_encoder_functions[]; extern const OSSL_DISPATCH wp_ed448_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa44_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa65_epki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_spki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_spki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_pki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_pki_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_epki_der_encoder_functions[]; +extern const OSSL_DISPATCH wp_mldsa87_epki_pem_encoder_functions[]; /* Storage implementations. */ extern const OSSL_DISPATCH wp_file_store_functions[]; diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 21868070..d4e86de8 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -1146,4 +1146,621 @@ IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(mldsa44) IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(mldsa65) IMPLEMENT_MLDSA_KEYMGMT_DISPATCH(mldsa87) +/* + * ML-DSA encoder/decoder + */ + +/* Extra slack added to the queried DER length before allocating. */ +#define WP_MLDSA_DER_SLACK 32 + +/** Type for function that decodes a key into a wolfSSL key. */ +typedef int (*WP_MLDSA_DECODE)(const byte* input, word32* inOutIdx, void* key, + word32 inSz); +/** Type for function that encodes a key from a wolfSSL key. */ +typedef int (*WP_MLDSA_ENCODE)(void* key, byte* output, word32 inLen); + +/** Function to create an ML-DSA key object preset to a level. */ +typedef wp_MlDsa* (*WP_MLDSA_NEW)(WOLFPROV_CTX* provCtx); + +/** + * Encode/decode ML-DSA public/private key. + */ +typedef struct wp_MlDsaEncDecCtx { + /** wolfSSL function to decode ML-DSA key from DER. */ + WP_MLDSA_DECODE decode; + /** wolfSSL function to encode ML-DSA key to DER. */ + WP_MLDSA_ENCODE encode; + /** Function to create the level-specific ML-DSA key object. */ + WP_MLDSA_NEW newKey; + + /** Provider context - used when creating ML-DSA key. */ + WOLFPROV_CTX* provCtx; + /** Parts of key to export. */ + int selection; + + /** Data type name passed to the data callback. */ + const char* dataType; + /** Supported format. */ + int format; + /** Data format. */ + int encoding; + + /** Cipher to use when encoding EncryptedPrivateKeyInfo. */ + int cipher; + /** Name of cipher to use when encoding EncryptedPrivateKeyInfo. */ + const char* cipherName; +} wp_MlDsaEncDecCtx; + +/** + * Create a new ML-DSA encoder/decoder context. + * + * @param [in] provCtx Provider context. + * @param [in] newKey Function to create level-specific ML-DSA key. + * @param [in] dataType Data type name passed to data callback. + * @param [in] format Supported key format. + * @param [in] encoding Data format. + * @param [in] decode Function to decode DER data to a key. + * @param [in] encode Function to encode key to DER data. + * @return New ML-DSA encoder/decoder context object on success. + * @return NULL on failure. + */ +static wp_MlDsaEncDecCtx* wp_mldsa_enc_dec_new(WOLFPROV_CTX* provCtx, + WP_MLDSA_NEW newKey, const char* dataType, int format, int encoding, + WP_MLDSA_DECODE decode, WP_MLDSA_ENCODE encode) +{ + wp_MlDsaEncDecCtx* ctx = NULL; + + if (wolfssl_prov_is_running()) { + ctx = (wp_MlDsaEncDecCtx*)OPENSSL_zalloc(sizeof(wp_MlDsaEncDecCtx)); + } + if (ctx != NULL) { + ctx->decode = decode; + ctx->encode = encode; + ctx->newKey = newKey; + ctx->provCtx = provCtx; + ctx->dataType = dataType; + ctx->format = format; + ctx->encoding = encoding; + } + return ctx; +} + +/** + * Dispose of ML-DSA encoder/decoder context object. + * + * @param [in, out] ctx ML-DSA encoder/decoder context object. + */ +static void wp_mldsa_enc_dec_free(wp_MlDsaEncDecCtx* ctx) +{ + OPENSSL_free(ctx); +} + +/** + * Return the settable parameters for the ML-DSA encoder/decoder context. + * + * @param [in] provCtx Provider context. Unused. + * @return Array of parameters with data type. + */ +static const OSSL_PARAM* wp_mldsa_enc_dec_settable_ctx_params( + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_mldsa_enc_dec_supported_settables[] = { + OSSL_PARAM_utf8_string(OSSL_ENCODER_PARAM_CIPHER, NULL, 0), + OSSL_PARAM_utf8_string(OSSL_ENCODER_PARAM_PROPERTIES, NULL, 0), + OSSL_PARAM_END, + }; + + (void)provCtx; + return wp_mldsa_enc_dec_supported_settables; +} + +/** + * Set the ML-DSA encoder/decoder context parameters. + * + * @param [in, out] ctx ML-DSA encoder/decoder context object. + * @param [in] params Array of parameters. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_mldsa_enc_dec_set_ctx_params(wp_MlDsaEncDecCtx* ctx, + const OSSL_PARAM params[]) +{ + int ok = 1; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_enc_dec_set_ctx_params"); + + if (!wp_cipher_from_params(params, &ctx->cipher, &ctx->cipherName)) { + ok = 0; + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Construct parameters from ML-DSA key and pass off to callback. + * + * @param [in] mldsa ML-DSA key object. + * @param [in] dataType Data type name passed to the callback. + * @param [in] dataCb Callback to pass ML-DSA key in parameters to. + * @param [in] dataCbArg Argument to pass to callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_mldsa_dec_send_params(wp_MlDsa* mldsa, const char* dataType, + OSSL_CALLBACK* dataCb, void* dataCbArg) +{ + int ok = 1; + OSSL_PARAM params[4]; + int object_type = OSSL_OBJECT_PKEY; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_dec_send_params"); + + params[0] = OSSL_PARAM_construct_int(OSSL_OBJECT_PARAM_TYPE, &object_type); + params[1] = OSSL_PARAM_construct_utf8_string(OSSL_OBJECT_PARAM_DATA_TYPE, + (char*)dataType, 0); + /* The address of the key object becomes the octet string pointer. */ + params[2] = OSSL_PARAM_construct_octet_string(OSSL_OBJECT_PARAM_REFERENCE, + &mldsa, sizeof(mldsa)); + params[3] = OSSL_PARAM_construct_end(); + + /* Callback to do something with ML-DSA key object. */ + if (!dataCb(params, dataCbArg)) { + ok = 0; + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Decode the data in the core BIO. + * + * The level of the key is preset on the created key object so decode only + * succeeds when the DER's algorithm OID matches this decoder's level. + * + * @param [in, out] ctx ML-DSA encoder/decoder context object. + * @param [in, out] cBio Core BIO to read data from. + * @param [in] selection Parts of key to export. + * @param [in] dataCb Callback to pass ML-DSA key in parameters to. + * @param [in] dataCbArg Argument to pass to callback. + * @param [in] pwCb Password callback. + * @param [in] pwCbArg Argument to pass to password callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_mldsa_decode(wp_MlDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, + int selection, OSSL_CALLBACK* dataCb, void* dataCbArg, + OSSL_PASSPHRASE_CALLBACK* pwCb, void* pwCbArg) +{ + int ok = 1; + int decoded = 1; + int rc; + unsigned char* data = NULL; + word32 len = 0; + word32 idx = 0; + wp_MlDsa* mldsa = NULL; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_decode"); + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + + (void)pwCb; + (void)pwCbArg; + + if (ok) { + ctx->selection = selection; + mldsa = ctx->newKey(ctx->provCtx); + if (mldsa == NULL) { + ok = 0; + } + } + + if (ok) { + ok = wp_read_der_bio(ctx->provCtx, cBio, &data, &len); + } + if (ok) { + rc = ctx->decode(data, &idx, (void*)&mldsa->key, len); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "decode", rc); + ok = 0; + decoded = 0; + } + } + if (ok && (ctx->format == WP_ENC_FORMAT_SPKI)) { + mldsa->hasPub = 1; + } + if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { + mldsa->hasPub = 1; + mldsa->hasPriv = 1; + } + + OPENSSL_clear_free(data, len); + + if (ok && (!wp_mldsa_dec_send_params(mldsa, ctx->dataType, dataCb, + dataCbArg))) { + ok = 0; + } + + if (!ok) { + wp_mldsa_free(mldsa); + if (!decoded) { + ok = 1; + } + } + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Encode the ML-DSA key. + * + * ML-DSA keys are large so the DER buffer is sized from a query-length call + * (output == NULL) and then allocated, rather than using a fixed buffer. + * + * @param [in] ctx ML-DSA encoder/decoder context object. + * @param [in, out] cBio Core BIO to write data to. + * @param [in] mldsa ML-DSA key object. + * @param [in] params Key parameters. Unused. + * @param [in] selection Parts of key to encode. Unused. + * @param [in] pwCb Password callback. + * @param [in] pwCbArg Argument to pass to password callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_mldsa_encode(wp_MlDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, + const wp_MlDsa* mldsa, const OSSL_PARAM* params, int selection, + OSSL_PASSPHRASE_CALLBACK* pwCb, void* pwCbArg) +{ + int ok = 1; + int rc; + BIO* out = wp_corebio_get_bio(ctx->provCtx, cBio); + unsigned char* keyData = NULL; + size_t keyLen = 0; + unsigned char* derData = NULL; + word32 derAllocLen = 0; + size_t derLen = 0; + unsigned char* pemData = NULL; + size_t pemLen = 0; + int pemType = (ctx->format == WP_ENC_FORMAT_SPKI) ? PUBLICKEY_TYPE : + PKCS8_PRIVATEKEY_TYPE; + int private = (ctx->format == WP_ENC_FORMAT_PKI) || + (ctx->format == WP_ENC_FORMAT_EPKI); + byte* cipherInfo = NULL; + + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_encode"); + + (void)params; + (void)selection; + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (out == NULL)) { + ok = 0; + } + + if (ok) { + rc = ctx->encode((void*)&mldsa->key, NULL, 0); + if (rc <= 0) { + ok = 0; + } + else { + derAllocLen = (word32)rc; + /* EPKI encrypts in place: round up to the AES block size so the + * buffer has room for the padded ciphertext. */ + if (ctx->format == WP_ENC_FORMAT_EPKI) { + derAllocLen = ((derAllocLen + 15) / 16) * 16; + } + else { + derAllocLen += WP_MLDSA_DER_SLACK; + } + } + } + if (ok) { + derData = (unsigned char*)OPENSSL_malloc(derAllocLen); + if (derData == NULL) { + ok = 0; + } + } + if (ok) { + rc = ctx->encode((void*)&mldsa->key, derData, derAllocLen); + if (rc <= 0) { + ok = 0; + } + else { + derLen = (size_t)rc; + } + } + if (ok && (ctx->format == WP_ENC_FORMAT_EPKI)) { + size_t encLen = derAllocLen; + if (!wp_encrypt_key(ctx->provCtx, ctx->cipherName, derData, &encLen, + (word32)derLen, pwCb, pwCbArg, &cipherInfo)) { + ok = 0; + } + else { + derLen = encLen; + } + } + + if (ok && (ctx->encoding == WP_FORMAT_DER)) { + keyData = derData; + keyLen = derLen; + } + else if (ok && (ctx->encoding == WP_FORMAT_PEM)) { + rc = wc_DerToPemEx(derData, (word32)derLen, NULL, 0, cipherInfo, + pemType); + if (rc <= 0) { + ok = 0; + } + if (ok) { + pemLen = (size_t)rc; + pemData = (unsigned char*)OPENSSL_malloc(pemLen); + if (pemData == NULL) { + ok = 0; + } + } + if (ok) { + rc = wc_DerToPemEx(derData, (word32)derLen, pemData, (word32)pemLen, + cipherInfo, pemType); + if (rc <= 0) { + ok = 0; + } + } + if (ok) { + keyLen = pemLen = (size_t)rc; + keyData = pemData; + } + } + if (ok) { + rc = BIO_write(out, keyData, (int)keyLen); + if (rc <= 0) { + ok = 0; + } + } + + if (private) { + if (derData != NULL) { + OPENSSL_clear_free(derData, derAllocLen); + } + OPENSSL_clear_free(pemData, pemLen); + } + else { + OPENSSL_free(derData); + OPENSSL_free(pemData); + } + OPENSSL_free(cipherInfo); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Export the ML-DSA key object. + * + * @param [in] ctx ML-DSA encoder/decoder context object. + * @param [in] mldsa ML-DSA key object. + * @param [in] size Size of key object. + * @param [in] exportCb Callback to export key. + * @param [in] exportCbArg Argument to pass to callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_mldsa_export_object(wp_MlDsaEncDecCtx* ctx, wp_MlDsa* mldsa, + size_t size, OSSL_CALLBACK* exportCb, void* exportCbArg) +{ + (void)size; + return wp_mldsa_export(mldsa, ctx->selection, exportCb, exportCbArg); +} + +/** + * Return whether the SPKI decoder/encoder handles this part of the key. + * + * @param [in] provCtx Provider context. Unused. + * @param [in] selection Parts of key to handle. + * @return 1 when supported. + * @return 0 when not supported. + */ +static int wp_mldsa_spki_does_selection(WOLFPROV_CTX* provCtx, int selection) +{ + int ok; + + WOLFPROV_ENTER_SILENT(WP_LOG_COMP_PQC, WOLFPROV_FUNC_NAME); + + (void)provCtx; + + if (selection == 0) { + ok = 1; + } + else { + ok = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; + } + + WOLFPROV_LEAVE_SILENT(WP_LOG_COMP_PQC, + __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Return whether the PKI decoder/encoder handles this part of the key. + * + * @param [in] provCtx Provider context. Unused. + * @param [in] selection Parts of key to handle. + * @return 1 when supported. + * @return 0 when not supported. + */ +static int wp_mldsa_pki_does_selection(WOLFPROV_CTX* provCtx, int selection) +{ + int ok; + + WOLFPROV_ENTER_SILENT(WP_LOG_COMP_PQC, WOLFPROV_FUNC_NAME); + + (void)provCtx; + + if (selection == 0) { + ok = 1; + } + else { + ok = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + } + + WOLFPROV_LEAVE_SILENT(WP_LOG_COMP_PQC, + __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Decode a public ML-DSA key from SPKI DER. + * + * @param [in] input Buffer holding SPKI DER data. + * @param [in, out] inOutIdx On in, index into buffer. On out, index after. + * @param [in, out] key ML-DSA key object. + * @param [in] inSz Length of buffer in bytes. + * @return 0 on success, negative on error. + */ +static int wp_mldsa_pub_decode(const byte* input, word32* inOutIdx, void* key, + word32 inSz) +{ + return wc_MlDsaKey_PublicKeyDecode((wc_MlDsaKey*)key, input, inSz, inOutIdx); +} + +/** + * Decode a private ML-DSA key from PKCS8 PrivateKeyInfo DER. + * + * @param [in] input Buffer holding PKCS8 DER data. + * @param [in, out] inOutIdx On in, index into buffer. On out, index after. + * @param [in, out] key ML-DSA key object. + * @param [in] inSz Length of buffer in bytes. + * @return 0 on success, negative on error. + */ +static int wp_mldsa_priv_decode(const byte* input, word32* inOutIdx, void* key, + word32 inSz) +{ + return wc_MlDsaKey_PrivateKeyDecode((wc_MlDsaKey*)key, input, inSz, + inOutIdx); +} + +/** + * Encode the public part of an ML-DSA key as SubjectPublicKeyInfo DER. + * + * Pass NULL for output to query the required length. + * + * @param [in] key ML-DSA key object. + * @param [out] output Buffer to put encoded data in. + * @param [in] inLen Size of buffer in bytes. + * @return Size of encoded data in bytes on success, negative on error. + */ +static int wp_mldsa_pub_encode(void* key, byte* output, word32 inLen) +{ + return wc_MlDsaKey_PublicKeyToDer((wc_MlDsaKey*)key, output, inLen, 1); +} + +/** + * Encode the private part of an ML-DSA key as PKCS8 PrivateKeyInfo DER. + * + * Pass NULL for output to query the required length. + * + * @param [in] key ML-DSA key object. + * @param [out] output Buffer to put encoded data in. + * @param [in] inLen Size of buffer in bytes. + * @return Size of encoded data in bytes on success, negative on error. + */ +static int wp_mldsa_priv_encode(void* key, byte* output, word32 inLen) +{ + int ret; + + /* Prefer the form that carries the public key so a reloaded key can be + * used to build a certificate; fall back to the private-only encoding when + * the public part is not available. */ + ret = wc_MlDsaKey_KeyToDer((wc_MlDsaKey*)key, output, inLen); + if (ret <= 0) { + ret = wc_MlDsaKey_PrivateKeyToDer((wc_MlDsaKey*)key, output, inLen); + } + return ret; +} + +/* + * Per-level encoder/decoder context constructors and dispatch tables. + */ + +#define IMPLEMENT_MLDSA_DECODER(alg, dataType) \ +static wp_MlDsaEncDecCtx* wp_##alg##_spki_dec_new(WOLFPROV_CTX* provCtx) \ +{ \ + return wp_mldsa_enc_dec_new(provCtx, wp_##alg##_new, dataType, \ + WP_ENC_FORMAT_SPKI, 0, wp_mldsa_pub_decode, NULL); \ +} \ +const OSSL_DISPATCH wp_##alg##_spki_decoder_functions[] = { \ + { OSSL_FUNC_DECODER_NEWCTX, (DFUNC)wp_##alg##_spki_dec_new },\ + { OSSL_FUNC_DECODER_FREECTX, (DFUNC)wp_mldsa_enc_dec_free },\ + { OSSL_FUNC_DECODER_DOES_SELECTION, \ + (DFUNC)wp_mldsa_spki_does_selection },\ + { OSSL_FUNC_DECODER_DECODE, (DFUNC)wp_mldsa_decode },\ + { OSSL_FUNC_DECODER_EXPORT_OBJECT, (DFUNC)wp_mldsa_export_object },\ + { 0, NULL } \ +}; \ +static wp_MlDsaEncDecCtx* wp_##alg##_pki_dec_new(WOLFPROV_CTX* provCtx) \ +{ \ + return wp_mldsa_enc_dec_new(provCtx, wp_##alg##_new, dataType, \ + WP_ENC_FORMAT_PKI, 0, wp_mldsa_priv_decode, NULL); \ +} \ +const OSSL_DISPATCH wp_##alg##_pki_decoder_functions[] = { \ + { OSSL_FUNC_DECODER_NEWCTX, (DFUNC)wp_##alg##_pki_dec_new },\ + { OSSL_FUNC_DECODER_FREECTX, (DFUNC)wp_mldsa_enc_dec_free },\ + { OSSL_FUNC_DECODER_DOES_SELECTION, \ + (DFUNC)wp_mldsa_pki_does_selection },\ + { OSSL_FUNC_DECODER_DECODE, (DFUNC)wp_mldsa_decode },\ + { OSSL_FUNC_DECODER_EXPORT_OBJECT, (DFUNC)wp_mldsa_export_object },\ + { 0, NULL } \ +}; + +#define IMPLEMENT_MLDSA_ENCODER_TABLE(alg, fmt, enc, dsel) \ +static wp_MlDsaEncDecCtx* wp_##alg##_##fmt##_##enc##_enc_new( \ + WOLFPROV_CTX* provCtx) \ +{ \ + return wp_mldsa_enc_dec_new(provCtx, wp_##alg##_new, NULL, \ + WP_ENC_FORMAT_##fmt##_VAL, WP_FORMAT_##enc##_VAL, NULL, \ + WP_ENC_##fmt##_ENCODE); \ +} \ +const OSSL_DISPATCH wp_##alg##_##fmt##_##enc##_encoder_functions[] = { \ + { OSSL_FUNC_ENCODER_NEWCTX, \ + (DFUNC)wp_##alg##_##fmt##_##enc##_enc_new }, \ + { OSSL_FUNC_ENCODER_FREECTX, (DFUNC)wp_mldsa_enc_dec_free },\ + { OSSL_FUNC_ENCODER_SETTABLE_CTX_PARAMS, \ + (DFUNC)wp_mldsa_enc_dec_settable_ctx_params },\ + { OSSL_FUNC_ENCODER_SET_CTX_PARAMS, \ + (DFUNC)wp_mldsa_enc_dec_set_ctx_params },\ + { OSSL_FUNC_ENCODER_DOES_SELECTION, (DFUNC)dsel }, \ + { OSSL_FUNC_ENCODER_ENCODE, (DFUNC)wp_mldsa_encode },\ + { OSSL_FUNC_ENCODER_IMPORT_OBJECT, (DFUNC)wp_mldsa_import },\ + { OSSL_FUNC_ENCODER_FREE_OBJECT, (DFUNC)wp_mldsa_free },\ + { 0, NULL } \ +}; + +/* Format/encoding value and encode-function selectors for the table macro. */ +#define WP_ENC_FORMAT_spki_VAL WP_ENC_FORMAT_SPKI +#define WP_ENC_FORMAT_pki_VAL WP_ENC_FORMAT_PKI +#define WP_ENC_FORMAT_epki_VAL WP_ENC_FORMAT_EPKI +#define WP_FORMAT_der_VAL WP_FORMAT_DER +#define WP_FORMAT_pem_VAL WP_FORMAT_PEM +#define WP_ENC_spki_ENCODE wp_mldsa_pub_encode +#define WP_ENC_pki_ENCODE wp_mldsa_priv_encode +#define WP_ENC_epki_ENCODE wp_mldsa_priv_encode + +#define IMPLEMENT_MLDSA_ENCODERS(alg) \ + IMPLEMENT_MLDSA_ENCODER_TABLE(alg, spki, der, wp_mldsa_spki_does_selection)\ + IMPLEMENT_MLDSA_ENCODER_TABLE(alg, spki, pem, wp_mldsa_spki_does_selection)\ + IMPLEMENT_MLDSA_ENCODER_TABLE(alg, pki, der, wp_mldsa_pki_does_selection) \ + IMPLEMENT_MLDSA_ENCODER_TABLE(alg, pki, pem, wp_mldsa_pki_does_selection) \ + IMPLEMENT_MLDSA_ENCODER_TABLE(alg, epki, der, wp_mldsa_pki_does_selection) \ + IMPLEMENT_MLDSA_ENCODER_TABLE(alg, epki, pem, wp_mldsa_pki_does_selection) + +IMPLEMENT_MLDSA_DECODER(mldsa44, "ML-DSA-44") +IMPLEMENT_MLDSA_DECODER(mldsa65, "ML-DSA-65") +IMPLEMENT_MLDSA_DECODER(mldsa87, "ML-DSA-87") + +IMPLEMENT_MLDSA_ENCODERS(mldsa44) +IMPLEMENT_MLDSA_ENCODERS(mldsa65) +IMPLEMENT_MLDSA_ENCODERS(mldsa87) + #endif /* WP_HAVE_MLDSA */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index ff77fec3..d92618ef 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -989,6 +989,63 @@ static const OSSL_ALGORITHM wolfprov_encoder[] = { "" }, #endif +#ifdef WP_HAVE_MLDSA + { WP_NAMES_ML_DSA_44, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_mldsa44_spki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_44, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_mldsa44_spki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_44, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_mldsa44_pki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_44, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_mldsa44_pki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_44, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_mldsa44_epki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_44, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_mldsa44_epki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_mldsa65_spki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_mldsa65_spki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_mldsa65_pki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_mldsa65_pki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_mldsa65_epki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_mldsa65_epki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, der), + wp_mldsa87_spki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(SubjectPublicKeyInfo, pem), + wp_mldsa87_spki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(PrivateKeyInfo, der), + wp_mldsa87_pki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(PrivateKeyInfo, pem), + wp_mldsa87_pki_pem_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, der), + wp_mldsa87_epki_der_encoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_ENCODER_PROPERTIES(EncryptedPrivateKeyInfo, pem), + wp_mldsa87_epki_pem_encoder_functions, + "" }, +#endif + { NULL, NULL, NULL, NULL } }; @@ -1149,6 +1206,27 @@ static const OSSL_ALGORITHM wolfprov_decoder[] = { "" }, #endif +#ifdef WP_HAVE_MLDSA + { WP_NAMES_ML_DSA_44, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_mldsa44_spki_decoder_functions, + "" }, + { WP_NAMES_ML_DSA_44, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_mldsa44_pki_decoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_mldsa65_spki_decoder_functions, + "" }, + { WP_NAMES_ML_DSA_65, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_mldsa65_pki_decoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_DECODER_PROPERTIES(SubjectPublicKeyInfo), + wp_mldsa87_spki_decoder_functions, + "" }, + { WP_NAMES_ML_DSA_87, WP_DECODER_PROPERTIES(PrivateKeyInfo), + wp_mldsa87_pki_decoder_functions, + "" }, +#endif + /* Dummy decoder added to match PKI bit not match EPKI from context. * Flag set to say context type checked even though it didn't match and * not checked again. From f19fb3067138cefe36a279c1f910a6f806743b2f Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 11:05:53 -0700 Subject: [PATCH 27/43] Register ML-DSA TLS signature algorithms (mldsa44/65/87) so ML-DSA certs authenticate in TLS 1.3 --- src/wp_tls_capa.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/wp_tls_capa.c b/src/wp_tls_capa.c index 18aaba72..1c7bd0f3 100644 --- a/src/wp_tls_capa.c +++ b/src/wp_tls_capa.c @@ -171,6 +171,60 @@ static const OSSL_PARAM wp_param_group_list[][11] = { #define WP_PARAM_GROUP_CNT \ (sizeof(wp_param_group_list) / sizeof(*wp_param_group_list)) +#ifdef WP_HAVE_MLDSA +/** Constants associated with a TLS signature algorithm. */ +typedef struct wp_tls_sigalg_consts { + unsigned int codePoint; /** IANA signature scheme code point. */ + unsigned int secBits; /** #Bits of security. */ + int minTls; /** Minimum TLS version, -1 not supported. */ + int maxTls; /** Maximum TLS version (or 0 for all). */ + int minDtls; /** Minimum DTLS version, -1 not supported. */ + int maxDtls; /** Maximum DTLS version (or 0 for all). */ +} wp_tls_sigalg_consts; + +/** List of ML-DSA signature algorithm constants, by IANA code point. */ +static const wp_tls_sigalg_consts wp_sigalg_const_list[3] = { + { 0x0904, 128, TLS1_3_VERSION, 0, -1, -1 }, + { 0x0905, 192, TLS1_3_VERSION, 0, -1, -1 }, + { 0x0906, 256, TLS1_3_VERSION, 0, -1, -1 }, +}; + +/** Parameters for a TLS signature algorithm. Index references constant list. */ +#define WP_TLS_SIGALG_ENTRY(tlsName, alg, oid, idx) \ + { \ + OSSL_PARAM_utf8_string(OSSL_CAPABILITY_TLS_SIGALG_IANA_NAME, \ + (char*)tlsName, sizeof(tlsName)), \ + OSSL_PARAM_utf8_string(OSSL_CAPABILITY_TLS_SIGALG_NAME, \ + (char*)alg, sizeof(alg)), \ + OSSL_PARAM_utf8_string(OSSL_CAPABILITY_TLS_SIGALG_OID, \ + (char*)oid, sizeof(oid)), \ + OSSL_PARAM_uint(OSSL_CAPABILITY_TLS_SIGALG_CODE_POINT, \ + (unsigned int *)&wp_sigalg_const_list[idx].codePoint), \ + OSSL_PARAM_uint(OSSL_CAPABILITY_TLS_SIGALG_SECURITY_BITS, \ + (unsigned int *)&wp_sigalg_const_list[idx].secBits), \ + OSSL_PARAM_int(OSSL_CAPABILITY_TLS_SIGALG_MIN_TLS, \ + (int *)&wp_sigalg_const_list[idx].minTls), \ + OSSL_PARAM_int(OSSL_CAPABILITY_TLS_SIGALG_MAX_TLS, \ + (int *)&wp_sigalg_const_list[idx].maxTls), \ + OSSL_PARAM_int(OSSL_CAPABILITY_TLS_SIGALG_MIN_DTLS, \ + (int *)&wp_sigalg_const_list[idx].minDtls), \ + OSSL_PARAM_int(OSSL_CAPABILITY_TLS_SIGALG_MAX_DTLS, \ + (int *)&wp_sigalg_const_list[idx].maxDtls), \ + OSSL_PARAM_END \ + } + +/** List of parameters for ML-DSA TLS signature algorithms. */ +static const OSSL_PARAM wp_param_sigalg_list[][10] = { + WP_TLS_SIGALG_ENTRY("mldsa44", "ML-DSA-44", "2.16.840.1.101.3.4.3.17", 0), + WP_TLS_SIGALG_ENTRY("mldsa65", "ML-DSA-65", "2.16.840.1.101.3.4.3.18", 1), + WP_TLS_SIGALG_ENTRY("mldsa87", "ML-DSA-87", "2.16.840.1.101.3.4.3.19", 2), +}; + +/** Count of supported TLS signature algorithms. */ +#define WP_PARAM_SIGALG_CNT \ + (sizeof(wp_param_sigalg_list) / sizeof(*wp_param_sigalg_list)) +#endif /* WP_HAVE_MLDSA */ + /** * Pass the list of parameters for TLS groups to the callback. * @@ -197,11 +251,40 @@ static int wp_tls_group_capability(OSSL_CALLBACK *cb, void *arg) return ok; } +#ifdef WP_HAVE_MLDSA +/** + * Pass the list of parameters for TLS signature algorithms to the callback. + * + * @param [in] cb Callback. + * @param [in] arg Argument for callback. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_tls_sigalg_capability(OSSL_CALLBACK *cb, void *arg) +{ + int ok = 1; + size_t i; + + WOLFPROV_ENTER(WP_LOG_COMP_PROVIDER, "wp_tls_sigalg_capability"); + + for (i = 0; i < WP_PARAM_SIGALG_CNT; i++) { + if (!cb(wp_param_sigalg_list[i], arg)) { + ok = 0; + break; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} +#endif /* WP_HAVE_MLDSA */ + /** * Get the capabilities of wolfSSL provider. * * Supports: * TLS-GROUP + * TLS-SIGALG (ML-DSA) * * @param [in] provCtx Provider context. Unused. * @param [in] cb Callback. @@ -221,6 +304,11 @@ int wolfssl_prov_get_capabilities(void *provCtx, const char *capability, if (strcasecmp(capability, "TLS-GROUP") == 0) { ok = wp_tls_group_capability(cb, arg); } +#ifdef WP_HAVE_MLDSA + else if (strcasecmp(capability, "TLS-SIGALG") == 0) { + ok = wp_tls_sigalg_capability(cb, arg); + } +#endif WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } From f2c1a1f156f09f3f0f4decbd4aed65019085e718 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 11:20:16 -0700 Subject: [PATCH 28/43] Provide ML-DSA X509 signature AlgorithmIdentifier so wolfProvider can sign ML-DSA certificates --- include/wolfprovider/alg_funcs.h | 1 + src/wp_mldsa_kmgmt.c | 16 ++++++++ src/wp_mldsa_sig.c | 69 +++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 9f6c0441..02501acd 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -299,6 +299,7 @@ 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_sig_size(const wp_MlDsa* mldsa); +int wp_mldsa_get_level(wp_MlDsa* mldsa); /* Internal DH types and functions. */ typedef struct wp_Dh wp_Dh; diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index d4e86de8..f30984df 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -176,6 +176,22 @@ void* wp_mldsa_get_key(wp_MlDsa* mldsa) return &mldsa->key; } +/** + * Get the ML-DSA parameter level (WC_ML_DSA_44/65/87) for the key. + * + * @param [in] mldsa ML-DSA key object. + * @return Level value, or 0 when not available. + */ +int wp_mldsa_get_level(wp_MlDsa* mldsa) +{ + int level = 0; + + if ((mldsa != NULL) && (mldsa->data != NULL)) { + level = mldsa->data->level; + } + return level; +} + /** * Get the maximum signature size for the key. * diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index d6332402..12633235 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -626,20 +626,79 @@ static int wp_mldsa_verify_message_final(wp_MlDsaSigCtx* ctx, return ok; } -/* No supported params; OSSL contract is unconditional success. */ +/* DER AlgorithmIdentifier (SEQUENCE { OID }) for each ML-DSA level. ML-DSA + * signature algorithms carry no parameters, so the encoding is a fixed + * 13-byte sequence differing only in the final OID arc (17/18/19). */ +static const byte wp_mldsa44_aid[] = { + 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, + 0x11 +}; +static const byte wp_mldsa65_aid[] = { + 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, + 0x12 +}; +static const byte wp_mldsa87_aid[] = { + 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, + 0x13 +}; + +/* Set the X.509 signature AlgorithmIdentifier for the key's ML-DSA level. */ +static int wp_mldsa_get_alg_id(wp_MlDsaSigCtx* ctx, OSSL_PARAM* p) +{ + int ok = 1; + int level = wp_mldsa_get_level(ctx->mldsa); + const byte* aid = NULL; + size_t aidLen = 0; + + if (level == WC_ML_DSA_44) { + aid = wp_mldsa44_aid; + aidLen = sizeof(wp_mldsa44_aid); + } + else if (level == WC_ML_DSA_65) { + aid = wp_mldsa65_aid; + aidLen = sizeof(wp_mldsa65_aid); + } + else if (level == WC_ML_DSA_87) { + aid = wp_mldsa87_aid; + aidLen = sizeof(wp_mldsa87_aid); + } + else { + ok = 0; + } + if (ok && !OSSL_PARAM_set_octet_string(p, aid, aidLen)) { + ok = 0; + } + return ok; +} + +/* Provides the X.509 signature AlgorithmIdentifier so certificate and other + * structure signing (ASN1_item_sign_ctx) can build the signatureAlgorithm. */ static int wp_mldsa_get_ctx_params(wp_MlDsaSigCtx* ctx, OSSL_PARAM* params) { + int ok = 1; + OSSL_PARAM* p; + WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_get_ctx_params"); - (void)ctx; - (void)params; - WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); - return 1; + + if (ctx == NULL) { + ok = 0; + } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_SIGNATURE_PARAM_ALGORITHM_ID); + if (p != NULL) { + ok = wp_mldsa_get_alg_id(ctx, p); + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; } static const OSSL_PARAM* wp_mldsa_gettable_ctx_params(wp_MlDsaSigCtx* ctx, WOLFPROV_CTX* provCtx) { static const OSSL_PARAM wp_mldsa_gettable[] = { + OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_ALGORITHM_ID, NULL, 0), OSSL_PARAM_END }; (void)ctx; From 2a43b3d607f7ee86e48c5c365a41b2dc3c313070 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 11:24:50 -0700 Subject: [PATCH 29/43] Register ML-DSA keymgmt OID/short-name aliases so ML-DSA CA certs validate as trust anchors (EVP_PKEY_is_a / check_sig_alg_match) --- include/wolfprovider/alg_funcs.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 02501acd..b4be2ebf 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -181,9 +181,9 @@ typedef void (*DFUNC)(void); #define WP_NAMES_SECP384R1MLKEM1024 "SecP384r1MLKEM1024" /* 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" +#define WP_NAMES_ML_DSA_44 "ML-DSA-44:MLDSA44:2.16.840.1.101.3.4.3.17:id-ml-dsa-44" +#define WP_NAMES_ML_DSA_65 "ML-DSA-65:MLDSA65:2.16.840.1.101.3.4.3.18:id-ml-dsa-65" +#define WP_NAMES_ML_DSA_87 "ML-DSA-87:MLDSA87:2.16.840.1.101.3.4.3.19:id-ml-dsa-87" /* DRBG names. */ #define WP_NAMES_SEED_SRC "SEED-SRC" From fe77e16d5824f59318be59b0e10183b2be1a752b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 11:56:06 -0700 Subject: [PATCH 30/43] Add nginx PQC OSP CI: ML-DSA cert auth + ML-KEM/hybrid KEX via nginx Test::Nginx harness (wolfSSL stable+master, latest OpenSSL, force-fail, pinned nginx/nginx-tests) --- .github/nginx/pqc_tls.t | 101 ++++++++++++++++++++++ .github/workflows/nginx-pqc.yml | 147 ++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 .github/nginx/pqc_tls.t create mode 100644 .github/workflows/nginx-pqc.yml diff --git a/.github/nginx/pqc_tls.t b/.github/nginx/pqc_tls.t new file mode 100644 index 00000000..37a7208e --- /dev/null +++ b/.github/nginx/pqc_tls.t @@ -0,0 +1,101 @@ +#!/usr/bin/perl + +# wolfProvider PQC test for the http ssl module. +# +# Exercises post-quantum TLS 1.3 through nginx backed by wolfProvider: an +# ML-DSA server certificate (FIPS 204) for authentication and ML-KEM / hybrid +# groups (FIPS 203) for key exchange. Mirrors the open-quantum-safe oqs-demos +# nginx test by connecting with each quantum-safe group and asserting the +# negotiated group, the ML-DSA peer signature, and a verified chain. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my @groups = qw/X25519MLKEM768 SecP256r1MLKEM768 SecP384r1MLKEM1024 MLKEM768/; +my $sig = 'ML-DSA-65'; + +my $t = Test::Nginx->new()->has(qw/http http_ssl/)->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + ssl_certificate_key server.key; + ssl_certificate server.crt; + + ssl_protocols TLSv1.3; + ssl_ecdh_curve X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024:MLKEM768; + + server { + listen 127.0.0.1:8443 ssl; + server_name localhost; + + return 200 "$ssl_curve"; + } +} + +EOF + +my $d = $t->testdir(); + +# ML-DSA certificate chain generated through wolfProvider (the default +# provider in a --replace-default build): an ML-DSA CA signing an ML-DSA +# server certificate, the oqs-demos SIG_ALG=mldsa65 arrangement. +system("openssl req -x509 -new -newkey $sig -nodes " + . "-keyout $d/ca.key -out $d/ca.crt -subj /CN=wolfProvider-PQC-CA " + . "-addext basicConstraints=critical,CA:TRUE " + . "-addext keyUsage=critical,keyCertSign " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create ML-DSA CA: $!\n"; + +system("openssl genpkey -algorithm $sig -out $d/server.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create ML-DSA server key: $!\n"; + +system("openssl req -new -key $d/server.key -subj /CN=localhost " + . "-addext subjectAltName=DNS:localhost -out $d/server.csr " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create server CSR: $!\n"; + +system("openssl x509 -req -in $d/server.csr -CA $d/ca.crt -CAkey $d/ca.key " + . "-CAcreateserial -days 30 -copy_extensions copy -out $d/server.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign server certificate: $!\n"; + +$t->run()->plan(scalar @groups * 3); + +############################################################################### + +my $p = port(8443); + +foreach my $g (@groups) { + my $out = `printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n' | openssl s_client -connect 127.0.0.1:$p -groups $g -CAfile $d/ca.crt -servername localhost 2>&1`; + + like($out, qr/Negotiated TLS1.3 group: \Q$g\E/, "$g: negotiated group"); + like($out, qr/Peer signature type: mldsa65/i, "$g: ML-DSA peer signature"); + like($out, qr/Verify return code: 0 \(ok\)/, "$g: ML-DSA chain verified"); +} + +############################################################################### diff --git a/.github/workflows/nginx-pqc.yml b/.github/workflows/nginx-pqc.yml new file mode 100644 index 00000000..edd61cb7 --- /dev/null +++ b/.github/workflows/nginx-pqc.yml @@ -0,0 +1,147 @@ +name: Nginx PQC Tests + +# Builds nginx against wolfProvider (--enable-pqc, --replace-default) and runs +# the post-quantum TLS path under nginx's own Test::Nginx harness: an ML-DSA +# (FIPS 204) certificate chain for authentication and ML-KEM / hybrid groups +# (FIPS 203) for key exchange, the open-quantum-safe oqs-demos nginx scenario. +# +# This intentionally differs from the classic nginx.yml in two ways: +# - nginx.yml installs debian-packaged OpenSSL (3.0.x), which has no ML-KEM / +# ML-DSA. PQC needs OpenSSL 3.6+, so the stack is built from source here, +# matching wolfssl-versions-pqc.yml / wolfssl-pqc-kat.yml. +# - Only the PQC path is exercised (pqc_tls.t); the rest of nginx-tests is +# out of scope for this workflow. +# +# Versioning mirrors the other PQC workflows: wolfSSL latest -stable (PQC floor +# is v5.9.2-stable) and master, against the latest OpenSSL 3.x release. The +# external OSP sources (nginx, nginx-tests) are pinned to fixed refs -- upstream +# master is unstable and would make the test non-reproducible. + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # Pinned external OSP sources (never track upstream master). + NGINX_REF: release-1.28.0 + NGINX_TESTS_REF: e0f3be50f7e701c4028acb9ed35004adc2b1db27 + +jobs: + discover-versions: + name: Resolve wolfSSL/OpenSSL versions + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + openssl-tag: ${{ steps.set-matrix.outputs.openssl-tag }} + steps: + - name: Resolve latest -stable wolfSSL tag and latest OpenSSL release + 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 + # PQC needs OpenSSL 3.6+, so always build against the latest release. + OSSL=$(git ls-remote --tags --refs \ + https://github.com/openssl/openssl.git 'openssl-3.*' \ + | awk -F/ '{print $NF}' | grep -E '^openssl-3\.[0-9.]+$' \ + | sort -V | tail -n 1) + if [ -z "${OSSL:-}" ]; then + echo "::error::Could not resolve latest OpenSSL release tag" + exit 1 + fi + echo "Latest stable wolfSSL: $LATEST" + echo "Latest OpenSSL: $OSSL" + echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT" + # Only PQC-eligible wolfSSL: the latest -stable (>= v5.9.2-stable) and + # master, each in normal and force-fail anti-test mode. The matrix + # owns the force-fail axis; the test step hands its raw result to + # check-workflow-result.sh which inverts the expectation. + MATRIX=$(jq -nc --arg latest "$LATEST" '{ + include: [ + {"name":("latest stable (" + $latest + ")"), + "wolfssl-ref":$latest,"force_fail":""}, + {"name":("latest stable (" + $latest + ") [force-fail]"), + "wolfssl-ref":$latest,"force_fail":"WOLFPROV_FORCE_FAIL=1"}, + {"name":"master","wolfssl-ref":"master","force_fail":""}, + {"name":"master [force-fail]", + "wolfssl-ref":"master","force_fail":"WOLFPROV_FORCE_FAIL=1"} + ] + }') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + + nginx-pqc: + name: ${{ matrix.name }} + needs: discover-versions + runs-on: ubuntu-22.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} + steps: + - name: Checkout wolfProvider + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build wolfProvider (PQC, replace-default) + run: | + OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ + WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ + ./scripts/build-wolfprovider.sh --enable-pqc --replace-default + + - name: Install nginx build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpcre3-dev zlib1g-dev \ + libtest-harness-perl libio-socket-ssl-perl libnet-ssleay-perl + + - name: Build nginx (${{ env.NGINX_REF }}) against wolfProvider OpenSSL + run: | + O="$GITHUB_WORKSPACE/openssl-install" + git clone --depth 1 --branch "$NGINX_REF" \ + https://github.com/nginx/nginx.git nginx + cd nginx + ./auto/configure --with-http_ssl_module \ + --with-cc-opt="-I$O/include" \ + --with-ld-opt="-L$O/lib -L$O/lib64 -Wl,-rpath,$O/lib -Wl,-rpath,$O/lib64" + make -j"$(nproc)" + + - name: Checkout nginx-tests (pinned ${{ env.NGINX_TESTS_REF }}) + run: | + git clone https://github.com/nginx/nginx-tests.git nginx-tests + git -C nginx-tests checkout "$NGINX_TESTS_REF" + + # Runs only the PQC path: ML-DSA cert auth + ML-KEM / hybrid key exchange. + # In normal mode every quantum-safe group must negotiate with an ML-DSA + # verified chain; in force-fail mode wolfProvider fails every operation so + # the handshakes break and check-workflow-result.sh inverts that to a + # pass, proving wolfProvider genuinely served the PQC crypto. + - name: Run nginx PQC test + shell: bash + run: | + set +e + source scripts/env-setup + # Use the wolfProvider-backed OpenSSL for the test client (s_client). + export PATH="$(dirname "$OPENSSL_BIN"):$PATH" + cp .github/nginx/pqc_tls.t nginx-tests/ + cd nginx-tests + export ${{ matrix.force_fail }} + TEST_NGINX_BINARY="$GITHUB_WORKSPACE/nginx/objs/nginx" \ + prove -v pqc_tls.t 2>&1 | tee nginx-pqc.log + TEST_RESULT=${PIPESTATUS[0]} + $GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \ + $TEST_RESULT "${{ matrix.force_fail }}" nginx-pqc From 6c9e4111b95b2f593340b190f74f08af1b850da7 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 12:14:49 -0700 Subject: [PATCH 31/43] nginx PQC OSP: run oqs-demos nginx in Docker with wolfProvider swapped for oqs-provider, exercising ML-DSA auth + ML-KEM/hybrid KEX via their connection test --- .dockerignore | 9 +++ .github/nginx/Dockerfile | 77 ++++++++++++++++++++++++ .github/nginx/nginx.conf | 38 ++++++++++++ .github/nginx/pqc_tls.t | 101 -------------------------------- .github/nginx/test.sh | 45 ++++++++++++++ .github/workflows/nginx-pqc.yml | 86 ++++++++------------------- 6 files changed, 194 insertions(+), 162 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/nginx/Dockerfile create mode 100644 .github/nginx/nginx.conf delete mode 100644 .github/nginx/pqc_tls.t create mode 100644 .github/nginx/test.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c92b3a6d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# Build artifacts from build-wolfprovider.sh are re-created inside the image; +# never ship them in the Docker build context (used by .github/nginx/Dockerfile). +openssl-source +openssl-install +wolfssl-source +wolfssl-install +wolfprov-install +**/*.bak +.git diff --git a/.github/nginx/Dockerfile b/.github/nginx/Dockerfile new file mode 100644 index 00000000..451d2634 --- /dev/null +++ b/.github/nginx/Dockerfile @@ -0,0 +1,77 @@ +# oqs-demos nginx, but with wolfProvider in place of liboqs + oqs-provider. +# +# Mirrors https://github.com/open-quantum-safe/oqs-demos/tree/main/nginx: build +# a quantum-safe OpenSSL provider, build nginx against that OpenSSL, generate an +# ML-DSA CA + server certificate, and serve TLS 1.3 with PQC groups. The only +# swap is the crypto provider: wolfProvider (backed by wolfSSL) instead of +# oqs-provider, restricted to the FIPS 203 / FIPS 204 algorithms wolfProvider +# implements. The default groups and SIG_ALG below are the wolfProvider- +# supported subset of the oqs-demos defaults. +# +# Build context is the wolfProvider repository root: +# docker build -f .github/nginx/Dockerfile -t wolfprov-nginx . + +ARG WOLFSSL_REF=master +ARG OPENSSL_REF=openssl-3.6.0 +ARG NGINX_VERSION=1.28.0 +ARG SIG_ALG=ML-DSA-65 +ARG DEFAULT_GROUPS=MLKEM512:MLKEM768:MLKEM1024:X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024 + +FROM ubuntu:22.04 AS build +ARG WOLFSSL_REF +ARG OPENSSL_REF +ARG NGINX_VERSION +ARG SIG_ALG +ARG DEFAULT_GROUPS +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git cmake autoconf automake libtool pkg-config \ + libpcre3-dev zlib1g-dev wget ca-certificates perl python3 \ + && rm -rf /var/lib/apt/lists/* + +# Build OpenSSL 3.6+, wolfSSL, and wolfProvider as the compile-time default +# provider (replace-default), so nginx uses wolfProvider with no app changes. +COPY . /opt/wolfProvider +WORKDIR /opt/wolfProvider +RUN OPENSSL_TAG=${OPENSSL_REF} WOLFSSL_TAG=${WOLFSSL_REF} \ + ./scripts/build-wolfprovider.sh --enable-pqc --replace-default + +ENV WOLFPROV_ROOT=/opt/wolfProvider +ENV O=/opt/wolfProvider/openssl-install +ENV LD_LIBRARY_PATH=/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:/opt/wolfProvider/openssl-install/lib:/opt/wolfProvider/openssl-install/lib64 + +# Build nginx against the wolfProvider-backed OpenSSL. +WORKDIR /opt/wolfProvider +RUN wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" \ + && tar -zxf "nginx-${NGINX_VERSION}.tar.gz" && rm "nginx-${NGINX_VERSION}.tar.gz" +WORKDIR /opt/wolfProvider/nginx-${NGINX_VERSION} +RUN ./configure --prefix=/opt/nginx --with-http_ssl_module \ + --with-cc-opt="-I${O}/include" \ + --with-ld-opt="-L${O}/lib -L${O}/lib64 -Wl,-rpath,${O}/lib -Wl,-rpath,${O}/lib64 -Wl,-rpath,/opt/wolfProvider/wolfprov-install/lib -Wl,-rpath,/opt/wolfProvider/wolfssl-install/lib" \ + && make -j"$(nproc)" && make install + +# Generate the ML-DSA CA + server certificate through wolfProvider (the oqs-demos +# SIG_ALG=mldsa65 arrangement) and lay down the PQC nginx config + test page. +WORKDIR /opt/nginx +COPY .github/nginx/nginx.conf /opt/nginx/conf/nginx.conf +RUN mkdir -p pki cacert logs html \ + && echo "wolfProvider quantum-safe nginx" > html/index.html \ + && "${O}/bin/openssl" req -x509 -new -newkey "${SIG_ALG}" -nodes \ + -keyout cacert/CA.key -out cacert/CA.crt -subj "/CN=wolfProvider PQC CA" \ + -days 365 -addext basicConstraints=critical,CA:TRUE \ + -addext keyUsage=critical,keyCertSign \ + && "${O}/bin/openssl" genpkey -algorithm "${SIG_ALG}" -out pki/server.key \ + && "${O}/bin/openssl" req -new -key pki/server.key -subj "/CN=oqs-nginx" \ + -addext subjectAltName=DNS:localhost -out server.csr \ + && "${O}/bin/openssl" x509 -req -in server.csr -CA cacert/CA.crt -CAkey cacert/CA.key \ + -CAcreateserial -days 365 -copy_extensions copy -out pki/server.crt + +COPY .github/nginx/test.sh /opt/nginx/test.sh +RUN chmod +x /opt/nginx/test.sh + +ENV DEFAULT_GROUPS=${DEFAULT_GROUPS} +EXPOSE 4433 + +# Default: serve. The CI test step overrides this with test.sh. +CMD ["/opt/nginx/sbin/nginx", "-c", "/opt/nginx/conf/nginx.conf", "-g", "daemon off;"] diff --git a/.github/nginx/nginx.conf b/.github/nginx/nginx.conf new file mode 100644 index 00000000..0a034243 --- /dev/null +++ b/.github/nginx/nginx.conf @@ -0,0 +1,38 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include ../conf/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + # Quantum-safe HTTPS server: ML-DSA certificate (FIPS 204) authentication + # and ML-KEM / hybrid groups (FIPS 203) for key exchange, served through + # wolfProvider. The group set is the wolfProvider-supported subset of the + # oqs-demos nginx DEFAULT_GROUPS. + server { + listen 0.0.0.0:4433 ssl; + server_name localhost; + + access_log /opt/nginx/logs/access.log; + error_log /opt/nginx/logs/error.log; + + ssl_certificate /opt/nginx/pki/server.crt; + ssl_certificate_key /opt/nginx/pki/server.key; + + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 5m; + + ssl_protocols TLSv1.3; + ssl_ecdh_curve MLKEM512:MLKEM768:MLKEM1024:X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024; + + location / { + root html; + index index.html index.htm; + } + } +} diff --git a/.github/nginx/pqc_tls.t b/.github/nginx/pqc_tls.t deleted file mode 100644 index 37a7208e..00000000 --- a/.github/nginx/pqc_tls.t +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/perl - -# wolfProvider PQC test for the http ssl module. -# -# Exercises post-quantum TLS 1.3 through nginx backed by wolfProvider: an -# ML-DSA server certificate (FIPS 204) for authentication and ML-KEM / hybrid -# groups (FIPS 203) for key exchange. Mirrors the open-quantum-safe oqs-demos -# nginx test by connecting with each quantum-safe group and asserting the -# negotiated group, the ML-DSA peer signature, and a verified chain. - -############################################################################### - -use warnings; -use strict; - -use Test::More; - -BEGIN { use FindBin; chdir($FindBin::Bin); } - -use lib 'lib'; -use Test::Nginx; - -############################################################################### - -select STDERR; $| = 1; -select STDOUT; $| = 1; - -my @groups = qw/X25519MLKEM768 SecP256r1MLKEM768 SecP384r1MLKEM1024 MLKEM768/; -my $sig = 'ML-DSA-65'; - -my $t = Test::Nginx->new()->has(qw/http http_ssl/)->has_daemon('openssl'); - -$t->write_file_expand('nginx.conf', <<'EOF'); - -%%TEST_GLOBALS%% - -daemon off; - -events { -} - -http { - %%TEST_GLOBALS_HTTP%% - - ssl_certificate_key server.key; - ssl_certificate server.crt; - - ssl_protocols TLSv1.3; - ssl_ecdh_curve X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024:MLKEM768; - - server { - listen 127.0.0.1:8443 ssl; - server_name localhost; - - return 200 "$ssl_curve"; - } -} - -EOF - -my $d = $t->testdir(); - -# ML-DSA certificate chain generated through wolfProvider (the default -# provider in a --replace-default build): an ML-DSA CA signing an ML-DSA -# server certificate, the oqs-demos SIG_ALG=mldsa65 arrangement. -system("openssl req -x509 -new -newkey $sig -nodes " - . "-keyout $d/ca.key -out $d/ca.crt -subj /CN=wolfProvider-PQC-CA " - . "-addext basicConstraints=critical,CA:TRUE " - . "-addext keyUsage=critical,keyCertSign " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create ML-DSA CA: $!\n"; - -system("openssl genpkey -algorithm $sig -out $d/server.key " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create ML-DSA server key: $!\n"; - -system("openssl req -new -key $d/server.key -subj /CN=localhost " - . "-addext subjectAltName=DNS:localhost -out $d/server.csr " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't create server CSR: $!\n"; - -system("openssl x509 -req -in $d/server.csr -CA $d/ca.crt -CAkey $d/ca.key " - . "-CAcreateserial -days 30 -copy_extensions copy -out $d/server.crt " - . ">>$d/openssl.out 2>&1") == 0 - or die "Can't sign server certificate: $!\n"; - -$t->run()->plan(scalar @groups * 3); - -############################################################################### - -my $p = port(8443); - -foreach my $g (@groups) { - my $out = `printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n' | openssl s_client -connect 127.0.0.1:$p -groups $g -CAfile $d/ca.crt -servername localhost 2>&1`; - - like($out, qr/Negotiated TLS1.3 group: \Q$g\E/, "$g: negotiated group"); - like($out, qr/Peer signature type: mldsa65/i, "$g: ML-DSA peer signature"); - like($out, qr/Verify return code: 0 \(ok\)/, "$g: ML-DSA chain verified"); -} - -############################################################################### diff --git a/.github/nginx/test.sh b/.github/nginx/test.sh new file mode 100644 index 00000000..929b2484 --- /dev/null +++ b/.github/nginx/test.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# +# oqs-demos nginx connection test, wolfProvider edition. Starts the quantum-safe +# nginx and connects once per supported group (the s_client / HTTP GET +# equivalent of oqs-demos testrun.py's "curl --cacert CA.crt --curves "), +# asserting the negotiated PQC group, the ML-DSA peer signature, a verified +# chain, and that the page is served. Exits non-zero if any group fails; the CI +# step inverts that under WOLFPROV_FORCE_FAIL=1. + +O=/opt/wolfProvider/openssl-install +CA=/opt/nginx/cacert/CA.crt +PORT=4433 +GROUPS="X25519MLKEM768 SecP256r1MLKEM768 SecP384r1MLKEM1024 MLKEM512 MLKEM768 MLKEM1024" + +export LD_LIBRARY_PATH="/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:${O}/lib:${O}/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +/opt/nginx/sbin/nginx -c /opt/nginx/conf/nginx.conf +sleep 2 + +fail=0 +for g in ${GROUPS}; do + out=$( (printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n'; sleep 0.5) \ + | "${O}/bin/openssl" s_client -connect "localhost:${PORT}" \ + -groups "${g}" -CAfile "${CA}" -servername localhost 2>&1) + + if echo "${out}" | grep -q "Negotiated TLS1.3 group: ${g}" \ + && echo "${out}" | grep -qi "Peer signature type: mldsa65" \ + && echo "${out}" | grep -q "Verify return code: 0 (ok)" \ + && echo "${out}" | grep -q "wolfProvider quantum-safe nginx"; then + echo "PASS: ${g} (ML-DSA auth + quantum-safe key exchange)" + else + echo "FAIL: ${g}" + echo "${out}" | grep -iE "group|signature|verify return|alert|error" | head -4 + fail=1 + fi +done + +/opt/nginx/sbin/nginx -c /opt/nginx/conf/nginx.conf -s stop 2>/dev/null + +if [ "${fail}" -eq 0 ]; then + echo "All quantum-safe groups served successfully." +else + echo "One or more quantum-safe groups failed." +fi +exit "${fail}" diff --git a/.github/workflows/nginx-pqc.yml b/.github/workflows/nginx-pqc.yml index edd61cb7..f0a2b718 100644 --- a/.github/workflows/nginx-pqc.yml +++ b/.github/workflows/nginx-pqc.yml @@ -1,21 +1,15 @@ name: Nginx PQC Tests -# Builds nginx against wolfProvider (--enable-pqc, --replace-default) and runs -# the post-quantum TLS path under nginx's own Test::Nginx harness: an ML-DSA -# (FIPS 204) certificate chain for authentication and ML-KEM / hybrid groups -# (FIPS 203) for key exchange, the open-quantum-safe oqs-demos nginx scenario. +# Runs the open-quantum-safe oqs-demos nginx demo with wolfProvider swapped in +# for liboqs + oqs-provider (.github/nginx/Dockerfile), then runs oqs-demos' +# own connection test (.github/nginx/test.sh): connect once per quantum-safe +# group and assert ML-DSA (FIPS 204) certificate authentication plus an +# ML-KEM / hybrid (FIPS 203) key exchange. # -# This intentionally differs from the classic nginx.yml in two ways: -# - nginx.yml installs debian-packaged OpenSSL (3.0.x), which has no ML-KEM / -# ML-DSA. PQC needs OpenSSL 3.6+, so the stack is built from source here, -# matching wolfssl-versions-pqc.yml / wolfssl-pqc-kat.yml. -# - Only the PQC path is exercised (pqc_tls.t); the rest of nginx-tests is -# out of scope for this workflow. -# -# Versioning mirrors the other PQC workflows: wolfSSL latest -stable (PQC floor -# is v5.9.2-stable) and master, against the latest OpenSSL 3.x release. The -# external OSP sources (nginx, nginx-tests) are pinned to fixed refs -- upstream -# master is unstable and would make the test non-reproducible. +# Only the PQC path is exercised. Versioning matches the other PQC workflows: +# wolfSSL latest -stable (PQC floor v5.9.2-stable) and master, against the +# latest OpenSSL 3.x release. The OSP source (nginx) is pinned to a fixed +# release in the Dockerfile -- upstream master is unstable and non-reproducible. on: push: @@ -30,11 +24,6 @@ concurrency: permissions: contents: read -env: - # Pinned external OSP sources (never track upstream master). - NGINX_REF: release-1.28.0 - NGINX_TESTS_REF: e0f3be50f7e701c4028acb9ed35004adc2b1db27 - jobs: discover-versions: name: Resolve wolfSSL/OpenSSL versions @@ -87,7 +76,7 @@ jobs: name: ${{ matrix.name }} needs: discover-versions runs-on: ubuntu-22.04 - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} @@ -97,51 +86,26 @@ jobs: with: fetch-depth: 1 - - name: Build wolfProvider (PQC, replace-default) - run: | - OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \ - WOLFSSL_TAG=${{ matrix.wolfssl-ref }} \ - ./scripts/build-wolfprovider.sh --enable-pqc --replace-default - - - name: Install nginx build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libpcre3-dev zlib1g-dev \ - libtest-harness-perl libio-socket-ssl-perl libnet-ssleay-perl - - - name: Build nginx (${{ env.NGINX_REF }}) against wolfProvider OpenSSL - run: | - O="$GITHUB_WORKSPACE/openssl-install" - git clone --depth 1 --branch "$NGINX_REF" \ - https://github.com/nginx/nginx.git nginx - cd nginx - ./auto/configure --with-http_ssl_module \ - --with-cc-opt="-I$O/include" \ - --with-ld-opt="-L$O/lib -L$O/lib64 -Wl,-rpath,$O/lib -Wl,-rpath,$O/lib64" - make -j"$(nproc)" - - - name: Checkout nginx-tests (pinned ${{ env.NGINX_TESTS_REF }}) + # Build the oqs-demos nginx image with wolfProvider in place of + # oqs-provider. nginx is pinned to release-1.28.0 in the Dockerfile. + - name: Build quantum-safe nginx image run: | - git clone https://github.com/nginx/nginx-tests.git nginx-tests - git -C nginx-tests checkout "$NGINX_TESTS_REF" + docker build \ + --build-arg WOLFSSL_REF=${{ matrix.wolfssl-ref }} \ + --build-arg OPENSSL_REF=${{ needs.discover-versions.outputs.openssl-tag }} \ + -f .github/nginx/Dockerfile -t wolfprov-nginx . - # Runs only the PQC path: ML-DSA cert auth + ML-KEM / hybrid key exchange. - # In normal mode every quantum-safe group must negotiate with an ML-DSA - # verified chain; in force-fail mode wolfProvider fails every operation so - # the handshakes break and check-workflow-result.sh inverts that to a - # pass, proving wolfProvider genuinely served the PQC crypto. - - name: Run nginx PQC test + # Run oqs-demos' connection test against the running server. In normal + # mode every quantum-safe group must negotiate with an ML-DSA verified + # chain; in force-fail mode wolfProvider fails every operation so the + # handshakes break and check-workflow-result.sh inverts that to a pass, + # proving wolfProvider genuinely served the PQC crypto. + - name: Run oqs-demos nginx PQC connection test shell: bash run: | set +e - source scripts/env-setup - # Use the wolfProvider-backed OpenSSL for the test client (s_client). - export PATH="$(dirname "$OPENSSL_BIN"):$PATH" - cp .github/nginx/pqc_tls.t nginx-tests/ - cd nginx-tests - export ${{ matrix.force_fail }} - TEST_NGINX_BINARY="$GITHUB_WORKSPACE/nginx/objs/nginx" \ - prove -v pqc_tls.t 2>&1 | tee nginx-pqc.log + docker run --rm -e ${{ matrix.force_fail || 'WOLFPROV_NOOP=1' }} \ + wolfprov-nginx /opt/nginx/test.sh 2>&1 | tee nginx-pqc.log TEST_RESULT=${PIPESTATUS[0]} $GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \ $TEST_RESULT "${{ matrix.force_fail }}" nginx-pqc From e4f18391c08969a54846a2d8afe9b40d3121c808 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 12:34:43 -0700 Subject: [PATCH 32/43] Add unit tests for ML-DSA encoder/decoder, ML-DSA X.509 signing, and hybrid-group KEM; run all PQC unit tests in CI --- .github/workflows/wolfssl-versions-pqc.yml | 20 +++ test/test_mldsa.c | 187 +++++++++++++++++++++ test/test_mlkem.c | 102 +++++++++++ test/unit.c | 3 + test/unit.h | 3 + 5 files changed, 315 insertions(+) diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml index 7245dcc8..ef2a97ad 100644 --- a/.github/workflows/wolfssl-versions-pqc.yml +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -135,6 +135,26 @@ jobs: *) false ;; esac || { echo "ERROR: PQC test families do not match expect=${{ matrix.expect }}"; exit 1; } + # Run the ML-KEM / ML-DSA / hybrid unit tests: keygen, sign/verify, + # encap/decap, PEM encoder/decoder round-trip, X.509 signing, and the + # hybrid-group KEM. Selected by index from --list so it tracks however + # many PQC tests are registered. LD_LIBRARY_PATH carries libwolfprov's + # deps; the provider module itself is found via the default .libs dir. + - name: Run PQC unit tests + if: matrix.expect != 'none' + run: | + export LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" + idxs=$(./test/unit.test --list | grep -iE 'mlkem|mldsa|mlx' \ + | grep -oE '^[0-9]+') + if [ -z "$idxs" ]; then + echo "::error::No PQC unit tests found" + exit 1 + fi + for i in $idxs; do + echo "== PQC unit test $i ==" + ./test/unit.test "$i" || exit 1 + done + # Three-way interop: wolfProvider <-> OpenSSL default <-> wolfSSL direct. # Only runs on PQC-enabled rows; the latest OpenSSL (3.6+, the PQC floor) # has native ML-KEM/ML-DSA in the default provider, so this proves diff --git a/test/test_mldsa.c b/test/test_mldsa.c index 1a125443..e863e403 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -22,6 +22,9 @@ #include #include +#include +#include +#include #ifdef WP_HAVE_MLDSA @@ -935,4 +938,188 @@ int test_mldsa_reinit_null_key(void* data) return err; } +/* Encode a key to PEM in wpLibCtx for the given selection/structure, then + * decode it back via the wolfProvider decoder. The decoded key signs a + * message that the original public key must verify. */ +int test_mldsa_encode_decode(void* data) +{ + static const unsigned char msg[24] = "mldsa-encode-decode-msg!"; + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + EVP_PKEY* privDec = NULL; + EVP_PKEY* pubDec = NULL; + OSSL_ENCODER_CTX* ectx = NULL; + OSSL_DECODER_CTX* dctx = NULL; + unsigned char* privPem = NULL; + unsigned char* pubPem = NULL; + const unsigned char* p = NULL; + size_t privPemLen = 0; + size_t pubPemLen = 0; + 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("Encode/decode %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k); + + /* Private-key PEM (PrivateKeyInfo) round-trip. */ + if (err == 0) { + ectx = OSSL_ENCODER_CTX_new_for_pkey(k, + EVP_PKEY_KEYPAIR, "PEM", "PrivateKeyInfo", NULL); + err = (ectx == NULL); + } + if (err == 0) { + err = OSSL_ENCODER_to_data(ectx, &privPem, &privPemLen) != 1; + if (err) PRINT_ERR_MSG("Private PEM encode failed"); + } + if (err == 0) { + p = privPem; + dctx = OSSL_DECODER_CTX_new_for_pkey(&privDec, "PEM", NULL, + lvl->name, EVP_PKEY_KEYPAIR, wpLibCtx, NULL); + err = (dctx == NULL); + } + if (err == 0) { + err = OSSL_DECODER_from_data(dctx, &p, &privPemLen) != 1; + if (err) PRINT_ERR_MSG("Private PEM decode failed"); + } + if (err == 0) { + err = (privDec == NULL); + } + if (err == 0) { + err = mldsa_dsign_short(privDec, msg, sizeof(msg), &sig, &sigLen); + if (err) PRINT_ERR_MSG("Sign with decoded private key failed"); + } + if (err == 0) { + err = mldsa_verify_msg(k, msg, sizeof(msg), sig, sigLen) != 1; + if (err) PRINT_ERR_MSG("Decoded private key sig did not verify"); + } + OSSL_ENCODER_CTX_free(ectx); ectx = NULL; + OSSL_DECODER_CTX_free(dctx); dctx = NULL; + OPENSSL_free(privPem); privPem = NULL; privPemLen = 0; + OPENSSL_free(sig); sig = NULL; sigLen = 0; + + /* Public-key PEM (SubjectPublicKeyInfo) round-trip. */ + if (err == 0) { + ectx = OSSL_ENCODER_CTX_new_for_pkey(k, + OSSL_KEYMGMT_SELECT_PUBLIC_KEY, "PEM", "SubjectPublicKeyInfo", + NULL); + err = (ectx == NULL); + } + if (err == 0) { + err = OSSL_ENCODER_to_data(ectx, &pubPem, &pubPemLen) != 1; + if (err) PRINT_ERR_MSG("Public PEM encode failed"); + } + if (err == 0) { + p = pubPem; + dctx = OSSL_DECODER_CTX_new_for_pkey(&pubDec, "PEM", NULL, + lvl->name, OSSL_KEYMGMT_SELECT_PUBLIC_KEY, wpLibCtx, NULL); + err = (dctx == NULL); + } + if (err == 0) { + err = OSSL_DECODER_from_data(dctx, &p, &pubPemLen) != 1; + if (err) PRINT_ERR_MSG("Public PEM decode failed"); + } + if (err == 0) { + err = (pubDec == NULL); + } + /* Sign with the original private key, verify with the decoded pub. */ + if (err == 0) { + err = mldsa_dsign_short(k, msg, sizeof(msg), &sig, &sigLen); + } + if (err == 0) { + err = mldsa_verify_msg(pubDec, msg, sizeof(msg), sig, sigLen) != 1; + if (err) PRINT_ERR_MSG("Decoded public key did not verify sig"); + } + + OSSL_ENCODER_CTX_free(ectx); ectx = NULL; + OSSL_DECODER_CTX_free(dctx); dctx = NULL; + OPENSSL_free(pubPem); pubPem = NULL; pubPemLen = 0; + OPENSSL_free(sig); sig = NULL; sigLen = 0; + EVP_PKEY_free(privDec); privDec = NULL; + EVP_PKEY_free(pubDec); pubDec = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + +/* Build a minimal self-signed X509 with an ML-DSA key, sign it via the + * provider (driving the signature AlgorithmIdentifier), and verify it. */ +int test_mldsa_x509_sign_verify(void* data) +{ + int err = 0; + size_t i; + EVP_PKEY* k = NULL; + X509* cert = NULL; + EVP_MD_CTX* mdctx = NULL; + X509_NAME* name = NULL; + ASN1_INTEGER* serial = NULL; + int rc; + + (void)data; + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + PRINT_MSG("X509 sign/verify %s", lvl->name); + + err = mldsa_keygen(lvl->name, &k); + if (err == 0) { + cert = X509_new(); + err = (cert == NULL); + } + if (err == 0) { + err = X509_set_version(cert, 2) != 1; + } + if (err == 0) { + serial = ASN1_INTEGER_new(); + err = (serial == NULL) || (ASN1_INTEGER_set(serial, 1) != 1) + || (X509_set_serialNumber(cert, serial) != 1); + } + if (err == 0) { + err = (X509_gmtime_adj(X509_getm_notBefore(cert), 0) == NULL); + } + if (err == 0) { + err = (X509_gmtime_adj(X509_getm_notAfter(cert), + 60L * 60L * 24L) == NULL); + } + if (err == 0) { + err = X509_set_pubkey(cert, k) != 1; + } + if (err == 0) { + name = X509_get_subject_name(cert); + err = (name == NULL) || (X509_NAME_add_entry_by_txt(name, "CN", + MBSTRING_ASC, (const unsigned char*)"mldsa-test", -1, -1, 0) + != 1); + } + if (err == 0) { + err = X509_set_issuer_name(cert, name) != 1; + } + if (err == 0) { + mdctx = EVP_MD_CTX_new(); + err = (mdctx == NULL); + } + if (err == 0) { + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, k, + NULL) != 1; + } + if (err == 0) { + rc = X509_sign_ctx(cert, mdctx); + err = (rc <= 0); + if (err) PRINT_ERR_MSG("X509_sign_ctx failed"); + } + if (err == 0) { + err = X509_verify(cert, k) != 1; + if (err) PRINT_ERR_MSG("X509_verify failed"); + } + + ASN1_INTEGER_free(serial); serial = NULL; + EVP_MD_CTX_free(mdctx); mdctx = NULL; + X509_free(cert); cert = NULL; + EVP_PKEY_free(k); k = NULL; + } + return err; +} + #endif /* WP_HAVE_MLDSA */ diff --git a/test/test_mlkem.c b/test/test_mlkem.c index 0793f59f..b30bad27 100644 --- a/test/test_mlkem.c +++ b/test/test_mlkem.c @@ -777,4 +777,106 @@ int test_mlkem_import_mismatched_pubpriv(void* data) return err; } +/* Hybrid (ML-KEM + classical) groups exercised as a KEM end to end. */ +static const char* const mlx_names[] = { + "X25519MLKEM768", + "SecP256r1MLKEM768", + "SecP384r1MLKEM1024", +}; +#define MLX_NAME_COUNT (sizeof(mlx_names) / sizeof(mlx_names[0])) + +/* Generate a hybrid key pair via wolfProvider. */ +static int wp_test_mlx_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; +} + +/* Encapsulate / decapsulate round trip for the hybrid groups. */ +int test_mlx_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 < MLX_NAME_COUNT); i++) { + const char* name = mlx_names[i]; + PRINT_MSG("Encap/Decap %s", name); + + err = wp_test_mlx_keygen(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 == 0) || (ss1Len == 0); + } + 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; + ctLen = 0; ss1Len = 0; ss2Len = 0; + } + + return err; +} + #endif /* WP_HAVE_MLKEM */ diff --git a/test/unit.c b/test/unit.c index 57e19879..e6d58243 100644 --- a/test/unit.c +++ b/test/unit.c @@ -490,6 +490,7 @@ TEST_CASE test_case[] = { TEST_DECL(test_mlkem_decap_size_query, NULL), TEST_DECL(test_mlkem_get_params, NULL), TEST_DECL(test_mlkem_import_mismatched_pubpriv, NULL), + TEST_DECL(test_mlx_encap_decap, NULL), #endif #ifdef WP_HAVE_MLDSA @@ -508,6 +509,8 @@ TEST_CASE test_case[] = { TEST_DECL(test_mldsa_import_mismatched_pubpriv, NULL), TEST_DECL(test_mldsa_empty_message, NULL), TEST_DECL(test_mldsa_reinit_null_key, NULL), + TEST_DECL(test_mldsa_encode_decode, NULL), + TEST_DECL(test_mldsa_x509_sign_verify, NULL), #endif }; #define TEST_CASE_CNT (int)(sizeof(test_case) / sizeof(*test_case)) diff --git a/test/unit.h b/test/unit.h index ad0d5ee6..c7cd5456 100644 --- a/test/unit.h +++ b/test/unit.h @@ -488,6 +488,7 @@ int test_mlkem_match(void *data); int test_mlkem_decap_size_query(void *data); int test_mlkem_get_params(void *data); int test_mlkem_import_mismatched_pubpriv(void *data); +int test_mlx_encap_decap(void *data); #endif #ifdef WP_HAVE_MLDSA @@ -506,6 +507,8 @@ int test_mldsa_digest_sign_init_rejects_md(void *data); int test_mldsa_import_mismatched_pubpriv(void *data); int test_mldsa_empty_message(void *data); int test_mldsa_reinit_null_key(void *data); +int test_mldsa_encode_decode(void *data); +int test_mldsa_x509_sign_verify(void *data); #endif #endif /* UNIT_H */ From 282e5ec7f5bdde7a124e0d7e3c5014db848f15b9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 12:38:53 -0700 Subject: [PATCH 33/43] PQC interop: add ML-DSA certificate TLS authentication test (sign+verify per group), validating the ML-DSA TLS signature algorithm end-to-end --- .../tests/pqc_interop/test_pqc_interop.c | 116 ++++++++++++++++-- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/test/standalone/tests/pqc_interop/test_pqc_interop.c b/test/standalone/tests/pqc_interop/test_pqc_interop.c index 3d2e56df..72fa246e 100644 --- a/test/standalone/tests/pqc_interop/test_pqc_interop.c +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -62,6 +62,7 @@ #include #include #include +#include #include @@ -88,6 +89,16 @@ static WC_RNG g_rng; * test. */ static X509* g_cert; static EVP_PKEY* g_key; +/* ML-DSA certificate + key on each context, for the TLS authentication test: + * proves ML-DSA is registered as a TLS signature algorithm so an ML-DSA + * certificate can sign (server) and verify (client) a TLS 1.3 handshake. */ +static X509* g_wp_mldsa_cert; +static EVP_PKEY* g_wp_mldsa_key; +static X509* g_oss_mldsa_cert; +static EVP_PKEY* g_oss_mldsa_key; + +static int make_mldsa_cert(OSSL_LIB_CTX* lib, X509** certOut, + EVP_PKEY** keyOut); #ifndef CERTS_DIR #define CERTS_DIR "certs" @@ -135,6 +146,14 @@ static int load_all(const char* wp_path) ok = 0; } } + if (ok && !make_mldsa_cert(wp_ctx, &g_wp_mldsa_cert, &g_wp_mldsa_key)) { + fprintf(stderr, "Failed to build wolfProvider ML-DSA certificate\n"); + ok = 0; + } + if (ok && !make_mldsa_cert(oss_ctx, &g_oss_mldsa_cert, &g_oss_mldsa_key)) { + fprintf(stderr, "Failed to build default-provider ML-DSA certificate\n"); + ok = 0; + } if (!ok) { if (wp_prov != NULL) { OSSL_PROVIDER_unload(wp_prov); @@ -149,6 +168,10 @@ static int load_all(const char* wp_path) static void unload_all(void) { wc_FreeRng(&g_rng); + if (g_wp_mldsa_key) EVP_PKEY_free(g_wp_mldsa_key); + if (g_wp_mldsa_cert) X509_free(g_wp_mldsa_cert); + if (g_oss_mldsa_key) EVP_PKEY_free(g_oss_mldsa_key); + if (g_oss_mldsa_cert) X509_free(g_oss_mldsa_cert); if (g_key) EVP_PKEY_free(g_key); if (g_cert) X509_free(g_cert); if (wp_prov) OSSL_PROVIDER_unload(wp_prov); @@ -670,9 +693,70 @@ static int tls_drive_handshake(SSL* client, SSL* server) return ok; } -/* One TLS 1.3 handshake for a group. wpServer != 0 puts wolfProvider on the - * server side and the default provider on the client; 0 swaps them. */ -static int test_tls_group(const char* group, int wpServer) +/* Build a self-signed ML-DSA-65 certificate on the given library context. The + * X509 signature drives the provider's ML-DSA signing AlgorithmIdentifier. */ +static int make_mldsa_cert(OSSL_LIB_CTX* lib, X509** certOut, EVP_PKEY** keyOut) +{ + int ok = 0; + EVP_PKEY* key = NULL; + EVP_PKEY_CTX* g = NULL; + X509* cert = NULL; + X509_NAME* name = NULL; + EVP_MD_CTX* mctx = NULL; + + g = EVP_PKEY_CTX_new_from_name(lib, "ML-DSA-65", NULL); + if ((g == NULL) || (EVP_PKEY_keygen_init(g) != 1) || + (EVP_PKEY_keygen(g, &key) != 1)) { + goto end; + } + cert = X509_new(); + if (cert == NULL) { + goto end; + } + if ((X509_set_version(cert, X509_VERSION_3) != 1) || + !ASN1_INTEGER_set(X509_get_serialNumber(cert), 1) || + (X509_gmtime_adj(X509_getm_notBefore(cert), 0) == NULL) || + (X509_gmtime_adj(X509_getm_notAfter(cert), 3600) == NULL) || + (X509_set_pubkey(cert, key) != 1)) { + goto end; + } + name = X509_get_subject_name(cert); + if ((X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char*)"mldsa-tls", -1, -1, 0) != 1) || + (X509_set_issuer_name(cert, name) != 1)) { + goto end; + } + mctx = EVP_MD_CTX_new(); + if ((mctx == NULL) || + (EVP_DigestSignInit_ex(mctx, NULL, NULL, lib, NULL, key, + NULL) != 1) || + (X509_sign_ctx(cert, mctx) <= 0)) { + goto end; + } + ok = 1; + +end: + if (!ok) { + ERR_print_errors_fp(stderr); + } + EVP_MD_CTX_free(mctx); + EVP_PKEY_CTX_free(g); + if (ok) { + *certOut = cert; + *keyOut = key; + } + else { + X509_free(cert); + EVP_PKEY_free(key); + } + return ok; +} + +/* One TLS 1.3 handshake for a group with the given certificate. wpServer != 0 + * puts wolfProvider on the server side and the default provider on the client; + * 0 swaps them. certName labels the output. */ +static int test_tls_group(const char* group, int wpServer, X509* cert, + EVP_PKEY* key, const char* certName) { int ok = 0; OSSL_LIB_CTX* serverLib = wpServer ? wp_ctx : oss_ctx; @@ -690,10 +774,10 @@ static int test_tls_group(const char* group, int wpServer) if ((sctx == NULL) || (cctx == NULL)) { goto end; } - if (SSL_CTX_use_certificate(sctx, g_cert) != 1) { + if (SSL_CTX_use_certificate(sctx, cert) != 1) { goto end; } - if (SSL_CTX_use_PrivateKey(sctx, g_key) != 1) { + if (SSL_CTX_use_PrivateKey(sctx, key) != 1) { goto end; } SSL_CTX_set_verify(cctx, SSL_VERIFY_NONE, NULL); @@ -731,9 +815,9 @@ static int test_tls_group(const char* group, int wpServer) if (!ok) { ERR_print_errors_fp(stderr); } - printf(" %-20s %-9s server -> %-9s client : %s\n", group, - wpServer ? "wolfProv" : "default", wpServer ? "default" : "wolfProv", - ok ? "PASS" : "FAIL"); + printf(" %-20s [%-6s cert] %-9s server -> %-9s client : %s\n", group, + certName, wpServer ? "wolfProv" : "default", + wpServer ? "default" : "wolfProv", ok ? "PASS" : "FAIL"); if (server != NULL) SSL_free(server); if (client != NULL) SSL_free(client); if (sctx != NULL) SSL_CTX_free(sctx); @@ -787,8 +871,20 @@ int main(int argc, char* argv[]) printf("\nTLS 1.3 group interop (handshake over BIO pair):\n"); printf(" (wolfProvider) <-> (OpenSSL default), both directions\n"); for (i = 0; i < sizeof(groups) / sizeof(groups[0]); i++) { - if (!test_tls_group(groups[i], 1)) fail++; - if (!test_tls_group(groups[i], 0)) fail++; + if (!test_tls_group(groups[i], 1, g_cert, g_key, "RSA")) fail++; + if (!test_tls_group(groups[i], 0, g_cert, g_key, "RSA")) fail++; + } + + printf("\nTLS 1.3 ML-DSA certificate authentication (TLS signature alg):\n"); + printf(" ML-DSA cert signs (server) and verifies (client) per group\n"); + for (i = 0; i < sizeof(groups) / sizeof(groups[0]); i++) { + /* wolfProvider server signs CertificateVerify with its ML-DSA cert; + * default client verifies. Then default server signs; wolfProvider + * client verifies. Both require ML-DSA as a registered TLS sigalg. */ + if (!test_tls_group(groups[i], 1, g_wp_mldsa_cert, g_wp_mldsa_key, + "ML-DSA")) fail++; + if (!test_tls_group(groups[i], 0, g_oss_mldsa_cert, g_oss_mldsa_key, + "ML-DSA")) fail++; } unload_all(); From dc2cd4fb6b7a1ba32a99e1c6bef47e740224f3f5 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 12:59:50 -0700 Subject: [PATCH 34/43] PQC security review fixes: zeroize seed/entropy/randomizer on free, apply FIPS 204 sig params in all init paths, reject wrong-length keygen seed, close hybrid match fail-open --- src/wp_mldsa_kmgmt.c | 10 +++++++++- src/wp_mldsa_sig.c | 20 +++++++++++++------- src/wp_mlkem_kem.c | 3 ++- src/wp_mlkem_kmgmt.c | 10 +++++++++- src/wp_mlx_kmgmt.c | 6 +++++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index f30984df..6eadd72f 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -1027,6 +1027,13 @@ static int wp_mldsa_gen_set_params(wp_MlDsaGenCtx* ctx, WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } + /* A seed shorter than the required size would silently fall back to + * RNG keygen, breaking the caller's reproducibility contract. Reject + * any length other than the exact FIPS 204 seed size. */ + if (ctx->seedLen != WP_MLDSA_SEED_SZ) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; @@ -1060,7 +1067,8 @@ static void wp_mldsa_gen_cleanup(wp_MlDsaGenCtx* ctx) { if (ctx != NULL) { wc_FreeRng(&ctx->rng); - OPENSSL_free(ctx); + /* ctx holds the deterministic keygen seed (FIPS 204 xi); cleanse it. */ + OPENSSL_clear_free(ctx, sizeof(*ctx)); } } diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 12633235..36c24d5d 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -193,7 +193,8 @@ static void wp_mldsa_freectx(wp_MlDsaSigCtx* ctx) wc_FreeRng(&ctx->rng); wp_mldsa_free(ctx->mldsa); OPENSSL_clear_free(ctx->mdBuf, ctx->mdCap); - OPENSSL_free(ctx); + /* ctx embeds the signing-randomizer override (testEntropy); cleanse. */ + OPENSSL_clear_free(ctx, sizeof(*ctx)); } } @@ -250,7 +251,6 @@ static int wp_mldsa_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_init"); - (void)params; if (ctx == NULL) { WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); @@ -274,6 +274,13 @@ static int wp_mldsa_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, /* Match OpenSSL: re-init clears external-mu but persists the context * string, deterministic flag and test-entropy until explicitly changed. */ ctx->mu = 0; + /* Apply the FIPS 204 signature params (context string, mu, message + * encoding, deterministic, test-entropy) so every init path -- not just + * the OpenSSL 3.5+ message API -- honors them. NULL params is a no-op. */ + if (!wp_mldsa_set_ctx_params(ctx, params)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } @@ -403,6 +410,8 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, if (ok) { *sigLen = outLen; } + /* The per-signature randomizer is sensitive; wipe it. */ + wc_ForceZero(rnd, sizeof(rnd)); } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; @@ -579,8 +588,8 @@ static int wp_mldsa_digest_verify_final(wp_MlDsaSigCtx* ctx, } /* OpenSSL 3.5+ ML-DSA signature message API. The init carries the FIPS 204 - * signature params (context, deterministic, mu, test-entropy); update/final - * reuse the digest-sign message accumulation. */ + * signature params (context, deterministic, mu, test-entropy); wp_mldsa_init + * applies them. update/final reuse the digest-sign message accumulation. */ static int wp_mldsa_message_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, const OSSL_PARAM params[]) { @@ -589,9 +598,6 @@ static int wp_mldsa_message_init(wp_MlDsaSigCtx* ctx, wp_MlDsa* mldsa, WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_message_init"); ok = wp_mldsa_init(ctx, mldsa, params); - if (ok) { - ok = wp_mldsa_set_ctx_params(ctx, params); - } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index 5d2ba9ab..be4e90bf 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -88,7 +88,8 @@ static void wp_mlkem_kem_freectx(wp_MlKemCtx* ctx) if (ctx != NULL) { wc_FreeRng(&ctx->rng); wp_mlkem_free(ctx->mlkem); - OPENSSL_free(ctx); + /* ctx embeds the encapsulation entropy (FIPS 203 ikme); cleanse. */ + OPENSSL_clear_free(ctx, sizeof(*ctx)); } } diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index 2c3e8199..10e40878 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -1048,6 +1048,13 @@ static int wp_mlkem_gen_set_params(wp_MlKemGenCtx* ctx, WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } + /* A seed shorter than the required size would silently fall back to + * RNG keygen, breaking the caller's reproducibility contract. Reject + * any length other than the exact FIPS 203 seed size. */ + if (ctx->seedLen != WP_MLKEM_SEED_SZ) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; @@ -1081,7 +1088,8 @@ static void wp_mlkem_gen_cleanup(wp_MlKemGenCtx* ctx) { if (ctx != NULL) { wc_FreeRng(&ctx->rng); - OPENSSL_free(ctx); + /* ctx holds the deterministic keygen seed (FIPS 203 d||z); cleanse. */ + OPENSSL_clear_free(ctx, sizeof(*ctx)); } } diff --git a/src/wp_mlx_kmgmt.c b/src/wp_mlx_kmgmt.c index 57f75629..7a83e312 100644 --- a/src/wp_mlx_kmgmt.c +++ b/src/wp_mlx_kmgmt.c @@ -619,7 +619,11 @@ static int wp_mlx_match(const wp_Mlx* a, const wp_Mlx* b, int selection) if (a->data != b->data) { return 0; } - if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { + /* Compare the public components for either a public- or private-key + * selection: the public uniquely identifies the key, so this avoids a + * fail-open where a private-only match would return equal without + * comparing anything. */ + if ((selection & OSSL_KEYMGMT_SELECT_KEYPAIR) != 0) { lenA = a->data->mlkemPubSize + a->data->classicalPubSize; bufA = (unsigned char*)OPENSSL_malloc(lenA); bufB = (unsigned char*)OPENSSL_malloc(lenA); From 31bd1df6bc2e3a3b9b97d6aa59d11eb2ccc29cb5 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 13:16:47 -0700 Subject: [PATCH 35/43] PQC security review round 2: apply+validate keygen seed params at init, reject wrong-length IKME/test-entropy, scrub hybrid shared secret on failure, fix fill_rnd log flag --- src/wp_mldsa_kmgmt.c | 14 +++++++++++--- src/wp_mldsa_sig.c | 9 ++++++++- src/wp_mlkem_kem.c | 7 +++++++ src/wp_mlkem_kmgmt.c | 14 +++++++++++--- src/wp_mlx_kem.c | 10 ++++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 6eadd72f..08286374 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -925,13 +925,14 @@ static int wp_mldsa_set_params(wp_MlDsa* mldsa, const OSSL_PARAM params[]) * @param [in] data Parameter set data. * @return New ML-DSA generation context on success, NULL on failure. */ +static int wp_mldsa_gen_set_params(wp_MlDsaGenCtx* ctx, + const OSSL_PARAM params[]); + 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)); @@ -948,9 +949,16 @@ static wp_MlDsaGenCtx* wp_mldsa_gen_init_base(WOLFPROV_CTX* provCtx, ctx->provCtx = provCtx; ctx->data = data; ctx->selection = selection; + /* Apply init-time params (e.g. the deterministic keygen seed) so + * the seed and its length validation are honored at init, not + * only via a later gen_set_params call. */ + if (!wp_mldsa_gen_set_params(ctx, params)) { + ok = 0; + } } if (!ok) { - OPENSSL_free(ctx); + wc_FreeRng(&ctx->rng); + OPENSSL_clear_free(ctx, sizeof(*ctx)); ctx = NULL; } } diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 36c24d5d..03bfb125 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -337,7 +337,8 @@ static int wp_mldsa_fill_rnd(wp_MlDsaSigCtx* ctx, unsigned char* rnd) else { rc = wc_RNG_GenerateBlock(&ctx->rng, rnd, WP_MLDSA_RND_SZ); } - WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), rc); + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + rc == 0); return rc; } @@ -773,6 +774,12 @@ static int wp_mldsa_set_ctx_params(wp_MlDsaSigCtx* ctx, &ctx->testEntropyLen)) { ok = 0; } + /* A short randomizer would be silently ignored; require the exact + * FIPS 204 size. */ + else if (ctx->testEntropyLen != WP_MLDSA_RND_SZ) { + ctx->testEntropyLen = 0; + ok = 0; + } } } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index be4e90bf..8a82a31a 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -363,6 +363,13 @@ static int wp_mlkem_kem_set_ctx_params(wp_MlKemCtx* ctx, WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; } + /* A short IKME would silently revert encapsulation from deterministic + * to RNG; require the exact FIPS 203 size. */ + if (ctx->ikmeLen != WP_MLKEM_IKME_SZ) { + ctx->ikmeLen = 0; + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; diff --git a/src/wp_mlkem_kmgmt.c b/src/wp_mlkem_kmgmt.c index 10e40878..d338efc4 100644 --- a/src/wp_mlkem_kmgmt.c +++ b/src/wp_mlkem_kmgmt.c @@ -945,13 +945,14 @@ static int wp_mlkem_set_params(wp_MlKem* mlkem, const OSSL_PARAM params[]) * @param [in] data Parameter set data. * @return New ML-KEM generation context on success, NULL on failure. */ +static int wp_mlkem_gen_set_params(wp_MlKemGenCtx* ctx, + const OSSL_PARAM params[]); + 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)); @@ -968,9 +969,16 @@ static wp_MlKemGenCtx* wp_mlkem_gen_init_base(WOLFPROV_CTX* provCtx, ctx->provCtx = provCtx; ctx->data = data; ctx->selection = selection; + /* Apply init-time params (e.g. the deterministic keygen seed) so + * the seed and its length validation are honored at init, not + * only via a later gen_set_params call. */ + if (!wp_mlkem_gen_set_params(ctx, params)) { + ok = 0; + } } if (!ok) { - OPENSSL_free(ctx); + wc_FreeRng(&ctx->rng); + OPENSSL_clear_free(ctx, sizeof(*ctx)); ctx = NULL; } } diff --git a/src/wp_mlx_kem.c b/src/wp_mlx_kem.c index 0f44fdd6..66945f4a 100644 --- a/src/wp_mlx_kem.c +++ b/src/wp_mlx_kem.c @@ -314,6 +314,11 @@ static int wp_mlx_kem_encapsulate(wp_MlxCtx* ctx, unsigned char* out, *outLen = ctSize; *secretLen = ssSize; } + else { + /* Scrub any component shared secret already written to the caller's + * buffer when a later component fails. */ + wc_ForceZero(secret, ssSize); + } return ok; } @@ -455,6 +460,11 @@ static int wp_mlx_kem_decapsulate(wp_MlxCtx* ctx, unsigned char* out, if (ok) { *outLen = ssSize; } + else { + /* Scrub any component shared secret already written to the caller's + * buffer when a later component fails. */ + wc_ForceZero(out, ssSize); + } return ok; } From 368f26b356cd8f9c8f2ffcba23673f5a98bd8428 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 13:37:28 -0700 Subject: [PATCH 36/43] PQC security review round 3: use sigSize as authoritative capacity, derive ECC public on hybrid private import, scrub ML-KEM shared secret on failure --- src/wp_mldsa_sig.c | 9 ++++++--- src/wp_mlkem_kem.c | 8 ++++++++ src/wp_mlx_kmgmt.c | 7 +++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index 03bfb125..afb3f5d6 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -355,8 +355,6 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_sign"); - (void)sigSize; - if ((ctx == NULL) || (ctx->mldsa == NULL) || (sigLen == NULL)) { WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); return 0; @@ -376,7 +374,12 @@ static int wp_mldsa_sign(wp_MlDsaSigCtx* ctx, unsigned char* sig, WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 1); return 1; } - if (*sigLen < sigSz) { + /* sigSize is the authoritative buffer capacity; fall back to *sigLen only + * when the dispatcher passes SIZE_MAX (matching wp_ecx_sig). */ + if (sigSize == (size_t)-1) { + sigSize = *sigLen; + } + if (sigSize < sigSz) { ok = 0; } /* wolfSSL's ML-DSA API takes a 32-bit message length. Reject >4 GiB diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index 8a82a31a..b98b152f 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -250,6 +250,10 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, *outLen = ctSize; *secretLen = ssSize; } + else if (secret != NULL) { + /* Scrub any shared secret wolfSSL may have written before failing. */ + wc_ForceZero(secret, ssSize); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -313,6 +317,10 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, if (ok) { *outLen = ssSize; } + else { + /* Scrub any shared secret wolfSSL may have written before failing. */ + wc_ForceZero(out, ssSize); + } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } diff --git a/src/wp_mlx_kmgmt.c b/src/wp_mlx_kmgmt.c index 7a83e312..31f1ac68 100644 --- a/src/wp_mlx_kmgmt.c +++ b/src/wp_mlx_kmgmt.c @@ -699,6 +699,13 @@ static int wp_mlx_load_keys(wp_Mlx* mlx, const unsigned char* pub, rc = wc_ecc_import_private_key_ex(priv + classicalOff, mlx->data->classicalPrivSize, NULL, 0, &mlx->classical.ecc, mlx->data->curveId); + if (rc == 0) { + /* A private-only ECC import leaves the key without a + * public point; derive it so the hybrid public can be + * exported (curve25519 derives its public lazily on + * export, so it needs no equivalent step). */ + rc = wc_ecc_make_pub(&mlx->classical.ecc, NULL); + } } if (rc != 0) { ok = 0; From 0c27517847f5883fbab22b8d5ca47cd55ed73e3d Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 13:49:13 -0700 Subject: [PATCH 37/43] PQC security review round 4: fix ML-KEM KEM out-of-bounds scrub on undersized buffer (early-return on size check), reject mismatched public on hybrid keypair import --- src/wp_mlkem_kem.c | 26 +++++++++++++++--------- src/wp_mlx_kmgmt.c | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index b98b152f..e76d4860 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -225,11 +225,16 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, return 0; } - if (ok && (*outLen < ctSize)) { - ok = 0; + /* Reject undersized buffers before the backend runs. Returning here (not + * falling through to the failure scrub) keeps any later wc_ForceZero in + * bounds, since the buffer is then proven at least ssSize. */ + if (*outLen < ctSize) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; } - if (ok && (*secretLen < ssSize)) { - ok = 0; + if (*secretLen < ssSize) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; } if (ok) { int rc; @@ -250,8 +255,8 @@ static int wp_mlkem_kem_encapsulate(wp_MlKemCtx* ctx, unsigned char* out, *outLen = ctSize; *secretLen = ssSize; } - else if (secret != NULL) { - /* Scrub any shared secret wolfSSL may have written before failing. */ + else { + /* Backend failed with a buffer proven >= ssSize; scrub stays in bounds. */ wc_ForceZero(secret, ssSize); } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); @@ -301,8 +306,11 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, return 0; } - if (ok && (*outLen < ssSize)) { - ok = 0; + /* Reject an undersized output buffer before the backend runs, so the + * later failure scrub of ssSize bytes is proven in bounds. */ + if (*outLen < ssSize) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; } if (ok && (inLen != ctSize)) { ok = 0; @@ -318,7 +326,7 @@ static int wp_mlkem_kem_decapsulate(wp_MlKemCtx* ctx, unsigned char* out, *outLen = ssSize; } else { - /* Scrub any shared secret wolfSSL may have written before failing. */ + /* Output buffer proven >= ssSize above; scrub stays in bounds. */ wc_ForceZero(out, ssSize); } WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); diff --git a/src/wp_mlx_kmgmt.c b/src/wp_mlx_kmgmt.c index 31f1ac68..2a916e93 100644 --- a/src/wp_mlx_kmgmt.c +++ b/src/wp_mlx_kmgmt.c @@ -664,6 +664,50 @@ static int wp_mlx_match(const wp_Mlx* a, const wp_Mlx* b, int selection) return ok; } +/** + * Verify a supplied concatenated public key matches the public derived from + * the imported private key (both ML-KEM and classical components). + * + * @param [in] mlx Hybrid key object (private already imported). + * @param [in] pub Concatenated public key in slot order. + * @return 1 on match, 0 on mismatch or error. + */ +static int wp_mlx_verify_pub(wp_Mlx* mlx, const unsigned char* pub) +{ + int ok = 1; + int rc; + int slot = mlx->data->mlkemSlot; + size_t mlkemOff = (size_t)slot * mlx->data->classicalPubSize; + size_t classicalOff = (size_t)(1 - slot) * mlx->data->mlkemPubSize; + unsigned char* buf = NULL; + word32 len; + + buf = (unsigned char*)OPENSSL_malloc((size_t)mlx->data->mlkemPubSize + + mlx->data->classicalPubSize); + if (buf == NULL) { + ok = 0; + } + if (ok) { + rc = wc_MlKemKey_EncodePublicKey(&mlx->mlkem, buf, + mlx->data->mlkemPubSize); + if ((rc != 0) || (XMEMCMP(buf, pub + mlkemOff, + mlx->data->mlkemPubSize) != 0)) { + ok = 0; + } + } + if (ok) { + len = mlx->data->classicalPubSize; + rc = wp_mlx_classical_export_pub(mlx, buf, &len); + if ((rc != 0) || (len != mlx->data->classicalPubSize) || + (XMEMCMP(buf, pub + classicalOff, + mlx->data->classicalPubSize) != 0)) { + ok = 0; + } + } + OPENSSL_free(buf); + return ok; +} + /** * Import hybrid key material from the concatenated (slot-order) encoding. * @@ -711,6 +755,11 @@ static int wp_mlx_load_keys(wp_Mlx* mlx, const unsigned char* pub, ok = 0; } } + if (ok && (pub != NULL) && !wp_mlx_verify_pub(mlx, pub)) { + /* Both halves supplied: reject a public that does not match the + * public derived from the imported private key. */ + ok = 0; + } if (ok) { mlx->hasPriv = 1; mlx->hasPub = 1; From 49260ae198adb6acc857f9b24155dd8089fa73d2 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 14:09:46 -0700 Subject: [PATCH 38/43] PQC security review round 5: copy IKME on KEM dupctx, advertise ML-DSA public only when actually decoded, give hybrid variant tables internal linkage --- src/wp_mldsa_kmgmt.c | 5 ++++- src/wp_mlkem_kem.c | 4 ++++ src/wp_mlx_kmgmt.c | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 08286374..e64220de 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -1405,8 +1405,11 @@ static int wp_mldsa_decode(wp_MlDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, mldsa->hasPub = 1; } if (ok && (ctx->format == WP_ENC_FORMAT_PKI)) { - mldsa->hasPub = 1; mldsa->hasPriv = 1; + /* Advertise the public only if the decoded private actually carried or + * derived it; a private-only PKCS8 (expanded key, no seed/public) does + * not, and must not claim a public it cannot export. */ + mldsa->hasPub = mldsa->key.pubKeySet ? 1 : 0; } OPENSSL_clear_free(data, len); diff --git a/src/wp_mlkem_kem.c b/src/wp_mlkem_kem.c index e76d4860..2a9c8fc7 100644 --- a/src/wp_mlkem_kem.c +++ b/src/wp_mlkem_kem.c @@ -118,6 +118,10 @@ static wp_MlKemCtx* wp_mlkem_kem_dupctx(wp_MlKemCtx* srcCtx) } dstCtx->mlkem = srcCtx->mlkem; } + /* Carry over deterministic encapsulation entropy so a duplicated context + * keeps reproducible (KAT/ACVP) behavior. */ + XMEMCPY(dstCtx->ikme, srcCtx->ikme, sizeof(dstCtx->ikme)); + dstCtx->ikmeLen = srcCtx->ikmeLen; return dstCtx; } diff --git a/src/wp_mlx_kmgmt.c b/src/wp_mlx_kmgmt.c index 2a916e93..10cb64ed 100644 --- a/src/wp_mlx_kmgmt.c +++ b/src/wp_mlx_kmgmt.c @@ -50,7 +50,7 @@ * * Matches OpenSSL's hybrid_vtable so the concatenated key_share interoperates. */ -const wp_MlxData mlxX25519Mlkem768Data = { +static const wp_MlxData mlxX25519Mlkem768Data = { WP_MLX_CLASSICAL_X25519, 0, WC_ML_KEM_768, @@ -65,7 +65,7 @@ const wp_MlxData mlxX25519Mlkem768Data = { "X25519MLKEM768" }; -const wp_MlxData mlxSecP256r1Mlkem768Data = { +static const wp_MlxData mlxSecP256r1Mlkem768Data = { WP_MLX_CLASSICAL_ECC, ECC_SECP256R1, WC_ML_KEM_768, @@ -80,7 +80,7 @@ const wp_MlxData mlxSecP256r1Mlkem768Data = { "SecP256r1MLKEM768" }; -const wp_MlxData mlxSecP384r1Mlkem1024Data = { +static const wp_MlxData mlxSecP384r1Mlkem1024Data = { WP_MLX_CLASSICAL_ECC, ECC_SECP384R1, WC_ML_KEM_1024, From 0f9d2a5bcdceac73c41152aaf30f8494772b23c0 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 14:25:22 -0700 Subject: [PATCH 39/43] PQC security review round 6: reject NULL ML-DSA message update with non-zero length --- src/wp_mldsa_sig.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wp_mldsa_sig.c b/src/wp_mldsa_sig.c index afb3f5d6..0eb6966c 100644 --- a/src/wp_mldsa_sig.c +++ b/src/wp_mldsa_sig.c @@ -97,6 +97,13 @@ static int wp_mldsa_buf_append(wp_MlDsaSigCtx* ctx, const unsigned char* data, WOLFPROV_ENTER(WP_LOG_COMP_PQC, "wp_mldsa_buf_append"); + /* A NULL buffer with a non-zero length is a caller error; reject it before + * the copy rather than dereferencing NULL. (NULL + 0 is a valid no-op.) */ + if ((data == NULL) && (dataLen != 0)) { + WOLFPROV_LEAVE(WP_LOG_COMP_PQC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), 0); + return 0; + } + needed = ctx->mdLen + dataLen; if (needed < ctx->mdLen) { ok = 0; From 32ff44de12c0a0f8b03f18ff7940869938f1ecbf Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 14:29:32 -0700 Subject: [PATCH 40/43] nginx PQC test: lengthen HTTP response wait to avoid handshake-timing flakes --- .github/nginx/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/nginx/test.sh b/.github/nginx/test.sh index 929b2484..683dea03 100644 --- a/.github/nginx/test.sh +++ b/.github/nginx/test.sh @@ -19,7 +19,7 @@ sleep 2 fail=0 for g in ${GROUPS}; do - out=$( (printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n'; sleep 0.5) \ + out=$( (printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n'; sleep 1) \ | "${O}/bin/openssl" s_client -connect "localhost:${PORT}" \ -groups "${g}" -CAfile "${CA}" -servername localhost 2>&1) From 009a00cf86d84dd70f5992493b81c8ed786495b6 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 19:48:30 -0700 Subject: [PATCH 41/43] nginx PQC CI: skip pre-PQC-floor wolfSSL stable (master-only until v5.9.2); fix wget TLS by setting LD_LIBRARY_PATH after nginx download --- .github/nginx/Dockerfile | 10 ++++++-- .github/workflows/nginx-pqc.yml | 45 +++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/nginx/Dockerfile b/.github/nginx/Dockerfile index 451d2634..4f6681eb 100644 --- a/.github/nginx/Dockerfile +++ b/.github/nginx/Dockerfile @@ -39,9 +39,11 @@ RUN OPENSSL_TAG=${OPENSSL_REF} WOLFSSL_TAG=${WOLFSSL_REF} \ ENV WOLFPROV_ROOT=/opt/wolfProvider ENV O=/opt/wolfProvider/openssl-install -ENV LD_LIBRARY_PATH=/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:/opt/wolfProvider/openssl-install/lib:/opt/wolfProvider/openssl-install/lib64 -# Build nginx against the wolfProvider-backed OpenSSL. +# Build nginx against the wolfProvider-backed OpenSSL. The download and build +# run BEFORE LD_LIBRARY_PATH points at the wolfProvider OpenSSL: otherwise +# wget's own HTTPS to nginx.org would route through wolfProvider and fail +# certificate verification. nginx finds the libs at runtime via -rpath. WORKDIR /opt/wolfProvider RUN wget -q "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" \ && tar -zxf "nginx-${NGINX_VERSION}.tar.gz" && rm "nginx-${NGINX_VERSION}.tar.gz" @@ -51,6 +53,10 @@ RUN ./configure --prefix=/opt/nginx --with-http_ssl_module \ --with-ld-opt="-L${O}/lib -L${O}/lib64 -Wl,-rpath,${O}/lib -Wl,-rpath,${O}/lib64 -Wl,-rpath,/opt/wolfProvider/wolfprov-install/lib -Wl,-rpath,/opt/wolfProvider/wolfssl-install/lib" \ && make -j"$(nproc)" && make install +# From here on the wolfProvider-backed OpenSSL is the default for cert +# generation and the runtime server. +ENV LD_LIBRARY_PATH=/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:/opt/wolfProvider/openssl-install/lib:/opt/wolfProvider/openssl-install/lib64 + # Generate the ML-DSA CA + server certificate through wolfProvider (the oqs-demos # SIG_ALG=mldsa65 arrangement) and lay down the PQC nginx config + test page. WORKDIR /opt/nginx diff --git a/.github/workflows/nginx-pqc.yml b/.github/workflows/nginx-pqc.yml index f0a2b718..0a974acd 100644 --- a/.github/workflows/nginx-pqc.yml +++ b/.github/workflows/nginx-pqc.yml @@ -55,20 +55,37 @@ jobs: echo "Latest stable wolfSSL: $LATEST" echo "Latest OpenSSL: $OSSL" echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT" - # Only PQC-eligible wolfSSL: the latest -stable (>= v5.9.2-stable) and - # master, each in normal and force-fail anti-test mode. The matrix - # owns the force-fail axis; the test step hands its raw result to - # check-workflow-result.sh which inverts the expectation. - MATRIX=$(jq -nc --arg latest "$LATEST" '{ - include: [ - {"name":("latest stable (" + $latest + ")"), - "wolfssl-ref":$latest,"force_fail":""}, - {"name":("latest stable (" + $latest + ") [force-fail]"), - "wolfssl-ref":$latest,"force_fail":"WOLFPROV_FORCE_FAIL=1"}, - {"name":"master","wolfssl-ref":"master","force_fail":""}, - {"name":"master [force-fail]", - "wolfssl-ref":"master","force_fail":"WOLFPROV_FORCE_FAIL=1"} - ] + # PQC needs the wc_MlDsaKey_* seed/message API that lands after + # v5.9.1-stable, so the floor is v5.9.2-stable. master is always + # eligible; the latest -stable row is only added once it is past the + # floor (i.e. v5.9.2-stable+ is released). Until then only master + # runs -- a pre-floor -stable would just fail the --enable-pqc gate. + 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" + # Each eligible wolfSSL ref runs in normal and force-fail anti-test + # mode. The matrix owns the force-fail axis; the test step hands its + # raw result to check-workflow-result.sh which inverts the + # expectation. + MATRIX=$(jq -nc --arg latest "$LATEST" \ + --argjson latest_pqc "$LATEST_PQC_ELIGIBLE" '{ + include: ( + [ {"name":"master","wolfssl-ref":"master","force_fail":""}, + {"name":"master [force-fail]","wolfssl-ref":"master", + "force_fail":"WOLFPROV_FORCE_FAIL=1"} ] + + (if $latest_pqc then + [ {"name":("latest stable (" + $latest + ")"), + "wolfssl-ref":$latest,"force_fail":""}, + {"name":("latest stable (" + $latest + ") [force-fail]"), + "wolfssl-ref":$latest, + "force_fail":"WOLFPROV_FORCE_FAIL=1"} ] + else [] end) + ) }') echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" From fd9083b362000a98f1b325ce983c3e34094c1053 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 20:25:56 -0700 Subject: [PATCH 42/43] nginx PQC test: rename GROUPS->KEX_GROUPS (GROUPS is a bash special array of GIDs, so the loop ran once with a GID); drop install-layout-dependent mime.types include; add startup debug --- .github/nginx/nginx.conf | 3 ++- .github/nginx/test.sh | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/nginx/nginx.conf b/.github/nginx/nginx.conf index 0a034243..1433bd9f 100644 --- a/.github/nginx/nginx.conf +++ b/.github/nginx/nginx.conf @@ -5,7 +5,8 @@ events { } http { - include ../conf/mime.types; + # No mime.types include: the relative path is install-layout dependent and + # the test serves a single plain page, so default_type is enough. default_type application/octet-stream; sendfile on; keepalive_timeout 65; diff --git a/.github/nginx/test.sh b/.github/nginx/test.sh index 683dea03..bddbe406 100644 --- a/.github/nginx/test.sh +++ b/.github/nginx/test.sh @@ -10,15 +10,22 @@ O=/opt/wolfProvider/openssl-install CA=/opt/nginx/cacert/CA.crt PORT=4433 -GROUPS="X25519MLKEM768 SecP256r1MLKEM768 SecP384r1MLKEM1024 MLKEM512 MLKEM768 MLKEM1024" +# NB: not "GROUPS" -- that is a bash special array (the user's group IDs). +KEX_GROUPS="X25519MLKEM768 SecP256r1MLKEM768 SecP384r1MLKEM1024 MLKEM512 MLKEM768 MLKEM1024" export LD_LIBRARY_PATH="/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:${O}/lib:${O}/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +echo "Quantum-safe groups under test: ${KEX_GROUPS}" /opt/nginx/sbin/nginx -c /opt/nginx/conf/nginx.conf sleep 2 +if ! "${O}/bin/openssl" s_client -connect "localhost:${PORT}" /dev/null 2>&1; then + echo "WARNING: nginx did not answer on ${PORT}; recent error log:" + tail -n 5 /opt/nginx/logs/error.log 2>/dev/null +fi fail=0 -for g in ${GROUPS}; do +for g in ${KEX_GROUPS}; do out=$( (printf 'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n'; sleep 1) \ | "${O}/bin/openssl" s_client -connect "localhost:${PORT}" \ -groups "${g}" -CAfile "${CA}" -servername localhost 2>&1) From 39fa5d2442b2852a5b6923606d3350dd462a8d0e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 5 Jun 2026 20:50:20 -0700 Subject: [PATCH 43/43] nginx PQC: add replace-default/non-replace as a matrix axis (4 modes per wolfSSL ref), loading wolfProvider via provider.conf in non-replace builds --- .github/nginx/Dockerfile | 20 +++++++++---- .github/workflows/nginx-pqc.yml | 51 +++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/.github/nginx/Dockerfile b/.github/nginx/Dockerfile index 4f6681eb..e5e2a6cc 100644 --- a/.github/nginx/Dockerfile +++ b/.github/nginx/Dockerfile @@ -16,6 +16,8 @@ ARG OPENSSL_REF=openssl-3.6.0 ARG NGINX_VERSION=1.28.0 ARG SIG_ALG=ML-DSA-65 ARG DEFAULT_GROUPS=MLKEM512:MLKEM768:MLKEM1024:X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024 +ARG REPLACE_DEFAULT_FLAG=--replace-default +ARG WP_OPENSSL_CONF=/opt/wolfProvider/openssl-install/ssl/openssl.cnf FROM ubuntu:22.04 AS build ARG WOLFSSL_REF @@ -23,6 +25,8 @@ ARG OPENSSL_REF ARG NGINX_VERSION ARG SIG_ALG ARG DEFAULT_GROUPS +ARG REPLACE_DEFAULT_FLAG +ARG WP_OPENSSL_CONF ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -30,12 +34,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libpcre3-dev zlib1g-dev wget ca-certificates perl python3 \ && rm -rf /var/lib/apt/lists/* -# Build OpenSSL 3.6+, wolfSSL, and wolfProvider as the compile-time default -# provider (replace-default), so nginx uses wolfProvider with no app changes. +# Build OpenSSL 3.6+, wolfSSL, and wolfProvider. REPLACE_DEFAULT_FLAG is +# --replace-default (wolfProvider is the compile-time default) or empty +# (wolfProvider is a loadable module selected at runtime via provider.conf). +# The matrix passes both REPLACE_DEFAULT_FLAG and the matching WP_OPENSSL_CONF. COPY . /opt/wolfProvider WORKDIR /opt/wolfProvider RUN OPENSSL_TAG=${OPENSSL_REF} WOLFSSL_TAG=${WOLFSSL_REF} \ - ./scripts/build-wolfprovider.sh --enable-pqc --replace-default + ./scripts/build-wolfprovider.sh --enable-pqc ${REPLACE_DEFAULT_FLAG} ENV WOLFPROV_ROOT=/opt/wolfProvider ENV O=/opt/wolfProvider/openssl-install @@ -53,9 +59,13 @@ RUN ./configure --prefix=/opt/nginx --with-http_ssl_module \ --with-ld-opt="-L${O}/lib -L${O}/lib64 -Wl,-rpath,${O}/lib -Wl,-rpath,${O}/lib64 -Wl,-rpath,/opt/wolfProvider/wolfprov-install/lib -Wl,-rpath,/opt/wolfProvider/wolfssl-install/lib" \ && make -j"$(nproc)" && make install -# From here on the wolfProvider-backed OpenSSL is the default for cert -# generation and the runtime server. +# From here on cert generation and the runtime server go through wolfProvider. +# In replace-default builds WP_OPENSSL_CONF is the stock openssl.cnf (a no-op); +# in non-replace builds it is provider.conf, which activates only wolfProvider +# so the PQC crypto is genuinely served by wolfSSL, not OpenSSL's native PQC. ENV LD_LIBRARY_PATH=/opt/wolfProvider/wolfprov-install/lib:/opt/wolfProvider/wolfssl-install/lib:/opt/wolfProvider/openssl-install/lib:/opt/wolfProvider/openssl-install/lib64 +ENV OPENSSL_CONF=${WP_OPENSSL_CONF} +ENV OPENSSL_MODULES=/opt/wolfProvider/wolfprov-install/lib # Generate the ML-DSA CA + server certificate through wolfProvider (the oqs-demos # SIG_ALG=mldsa65 arrangement) and lay down the PQC nginx config + test page. diff --git a/.github/workflows/nginx-pqc.yml b/.github/workflows/nginx-pqc.yml index 0a974acd..fd3adf2a 100644 --- a/.github/workflows/nginx-pqc.yml +++ b/.github/workflows/nginx-pqc.yml @@ -6,10 +6,12 @@ name: Nginx PQC Tests # group and assert ML-DSA (FIPS 204) certificate authentication plus an # ML-KEM / hybrid (FIPS 203) key exchange. # -# Only the PQC path is exercised. Versioning matches the other PQC workflows: -# wolfSSL latest -stable (PQC floor v5.9.2-stable) and master, against the -# latest OpenSSL 3.x release. The OSP source (nginx) is pinned to a fixed -# release in the Dockerfile -- upstream master is unstable and non-reproducible. +# Only the PQC path is exercised. Each wolfSSL ref runs in 4 modes: +# replace-default and non-replace (wolfProvider loaded via provider.conf), each +# in normal and force-fail. Versioning matches the other PQC workflows: wolfSSL +# latest -stable (PQC floor v5.9.2-stable) and master, against the latest +# OpenSSL 3.x release. The OSP source (nginx) is pinned to a fixed release in +# the Dockerfile -- upstream master is unstable and non-reproducible. on: push: @@ -68,25 +70,28 @@ jobs: LATEST_PQC_ELIGIBLE=false fi echo "latest-stable PQC eligible: $LATEST_PQC_ELIGIBLE" - # Each eligible wolfSSL ref runs in normal and force-fail anti-test - # mode. The matrix owns the force-fail axis; the test step hands its - # raw result to check-workflow-result.sh which inverts the - # expectation. + # Each eligible wolfSSL ref expands to 4 rows: replace-default and + # non-replace, each in normal and force-fail anti-test mode. The + # matrix owns the replace-default and force-fail axes; the test step + # hands its raw result to check-workflow-result.sh which inverts the + # force-fail expectation. MATRIX=$(jq -nc --arg latest "$LATEST" \ - --argjson latest_pqc "$LATEST_PQC_ELIGIBLE" '{ - include: ( - [ {"name":"master","wolfssl-ref":"master","force_fail":""}, - {"name":"master [force-fail]","wolfssl-ref":"master", - "force_fail":"WOLFPROV_FORCE_FAIL=1"} ] - + (if $latest_pqc then - [ {"name":("latest stable (" + $latest + ")"), - "wolfssl-ref":$latest,"force_fail":""}, - {"name":("latest stable (" + $latest + ") [force-fail]"), - "wolfssl-ref":$latest, - "force_fail":"WOLFPROV_FORCE_FAIL=1"} ] - else [] end) - ) - }') + --argjson latest_pqc "$LATEST_PQC_ELIGIBLE" ' + def rows($ref; $lbl): + [ {"replace":true, "ff":"WOLFPROV_FORCE_FAIL=1", + "sfx":" [replace-default] [force-fail]"}, + {"replace":true, "ff":"", "sfx":" [replace-default]"}, + {"replace":false, "ff":"WOLFPROV_FORCE_FAIL=1", + "sfx":" [non-replace] [force-fail]"}, + {"replace":false, "ff":"", "sfx":" [non-replace]"} ] + | map({"name":($lbl+.sfx), "wolfssl-ref":$ref, + "replace":.replace, "force_fail":.ff}); + { include: ( + rows("master"; "master") + + (if $latest_pqc + then rows($latest; ("latest stable (" + $latest + ")")) + else [] end) + ) }') echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" nginx-pqc: @@ -110,6 +115,8 @@ jobs: docker build \ --build-arg WOLFSSL_REF=${{ matrix.wolfssl-ref }} \ --build-arg OPENSSL_REF=${{ needs.discover-versions.outputs.openssl-tag }} \ + --build-arg REPLACE_DEFAULT_FLAG=${{ matrix.replace && '--replace-default' || '' }} \ + --build-arg WP_OPENSSL_CONF=${{ matrix.replace && '/opt/wolfProvider/openssl-install/ssl/openssl.cnf' || '/opt/wolfProvider/provider.conf' }} \ -f .github/nginx/Dockerfile -t wolfprov-nginx . # Run oqs-demos' connection test against the running server. In normal