diff --git a/.github/workflows/wolfssl-versions-pqc.yml b/.github/workflows/wolfssl-versions-pqc.yml new file mode 100644 index 00000000..1fb6d5cc --- /dev/null +++ b/.github/workflows/wolfssl-versions-pqc.yml @@ -0,0 +1,141 @@ +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 +# 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 +# (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: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + 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 + 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 + + # 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 + 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 + + # 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: | + ./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 + # 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 + run: | + LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64" \ + ./test/pqc_interop.test + + - name: Print errors on failure + if: ${{ failure() }} + run: | + if [ -f test-suite.log ]; then + cat test-suite.log + fi 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 diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 6e9bd1af..264d6ac8 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" @@ -222,6 +232,28 @@ 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); +/* 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; + +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; @@ -325,12 +357,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 +378,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..c1b7d8b6 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -169,6 +169,42 @@ #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 + #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 + #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 + #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 #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} diff --git a/src/include.am b/src/include.am index 5d8db01b..21db6007 100644 --- a/src/include.am +++ b/src/include.am @@ -36,6 +36,10 @@ 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_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..21c13a92 --- /dev/null +++ b/src/wp_mldsa_kmgmt.c @@ -0,0 +1,1055 @@ +/* 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; + 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 (dupPub) { + 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 && dupPriv) { + 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); + 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; +} + +/** + * 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; + unsigned char* derivedPub = NULL; + word32 derivedPubLen = 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; + /* 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; + } + } + } + } + } + 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; + } + /* 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); + if (rc != 0) { + ok = 0; + } + if (ok) { + mldsa->hasPub = 1; + } + } + } + if (ok && (privData == NULL) && (pubData == NULL)) { + ok = 0; + } + OPENSSL_free(derivedPub); + 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; + + 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)) { + 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) { + if (p->data_size < outLen) { + 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; + } + } + } + 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) { + p->return_size = outLen; + } + else if (mldsa->hasPriv) { + if (p->data_size < outLen) { + 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; + } + } + } + else { + /* Buffer supplied but no private key available. */ + p->return_size = 0; + } + } + } + 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); + } +} + +/* 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 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[]) +{ + 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_##alg##_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..9cd4f4f7 --- /dev/null +++ b/src/wp_mldsa_sig.c @@ -0,0 +1,497 @@ +/* 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. + */ +/* 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) +{ + int ok = 1; + size_t needed; + unsigned char* tmp; + + needed = ctx->mdLen + dataLen; + 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) { + 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()) || (srcCtx == NULL)) { + 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; + } + if (srcCtx->mdLen > 0) { + if (!wp_mldsa_buf_append(dstCtx, srcCtx->mdBuf, srcCtx->mdLen)) { + wp_mldsa_freectx(dstCtx); + return NULL; + } + } + 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; + } + /* 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, + * 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; + } + 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; + } + /* 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, + (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) +{ + if (ctx == NULL) { + return 0; + } + 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) +{ + if (ctx == NULL) { + return 0; + } + 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. + */ +static int wp_mldsa_get_ctx_params(wp_MlDsaSigCtx* ctx, OSSL_PARAM* params) +{ + (void)params; + return ctx != NULL; +} + +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; 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)params; + return ctx != NULL; +} + +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_mlkem_kem.c b/src/wp_mlkem_kem.c new file mode 100644 index 00000000..07e3288a --- /dev/null +++ b/src/wp_mlkem_kem.c @@ -0,0 +1,344 @@ +/* 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()) || (srcCtx == NULL)) { + 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_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) { + if (outLen != NULL) { + *outLen = ctSize; + } + if (secretLen != NULL) { + *secretLen = ssSize; + } + return 1; + } + if (secret == NULL) { + return 0; + } + + 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_SS_SIZE; + 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; 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)params; + return ctx != NULL; +} + +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; 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)params; + return ctx != NULL; +} + +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..6892450f --- /dev/null +++ b/src/wp_mlkem_kmgmt.c @@ -0,0 +1,1034 @@ +/* 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 + +/** 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; +} + +/** + * 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; + 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; + } + + dst = wp_mlkem_new(src->provCtx, src->data); + if (dst == NULL) { + return NULL; + } + + if (dupPub) { + pubLen = src->data->pubKeySize; + 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; + } + OPENSSL_free(pubBuf); + pubBuf = NULL; + } + + if (ok && dupPriv) { + privLen = src->data->privKeySize; + 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; + } + 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; +} + +/** + * 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; + unsigned char* derivedPub = NULL; + word32 derivedPubLen = 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; + /* 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; + } + } + } + } + } + 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; + } + /* 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); + if (rc != 0) { + ok = 0; + } + if (ok) { + mlkem->hasPub = 1; + } + } + } + if (ok && (privData == NULL) && (pubData == NULL)) { + ok = 0; + } + OPENSSL_free(derivedPub); + 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; + + 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; + } + 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) { + if (p->data_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; + } + } + } + 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) { + p->return_size = outLen; + } + else if (mlkem->hasPriv) { + if (p->data_size < outLen) { + ok = 0; + } + else { + rc = wc_MlKemKey_EncodePrivateKey(&mlkem->key, + (unsigned char*)p->data, outLen); + if (rc != 0) { + ok = 0; + } + else { + p->return_size = outLen; + } + } + } + else { + /* Buffer supplied but no private key available. */ + p->return_size = 0; + } + } + } + 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. + * + * 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) +{ + (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..f9f4a708 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -663,6 +663,23 @@ 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, "" }, + { WP_NAMES_ML_KEM_768, WOLFPROV_PROPERTIES, + wp_mlkem768_keymgmt_functions, "" }, + { WP_NAMES_ML_KEM_1024, WOLFPROV_PROPERTIES, + wp_mlkem1024_keymgmt_functions, "" }, +#endif +#ifdef WP_HAVE_MLDSA + { WP_NAMES_ML_DSA_44, WOLFPROV_PROPERTIES, + wp_mldsa44_keymgmt_functions, "" }, + { WP_NAMES_ML_DSA_65, WOLFPROV_PROPERTIES, + wp_mldsa65_keymgmt_functions, "" }, + { WP_NAMES_ML_DSA_87, WOLFPROV_PROPERTIES, + wp_mldsa87_keymgmt_functions, "" }, +#endif + { NULL, NULL, NULL, NULL } }; @@ -720,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 } }; @@ -741,6 +766,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 } }; 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/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..b4e0df95 --- /dev/null +++ b/test/standalone/tests/pqc_interop/test_pqc_interop.c @@ -0,0 +1,627 @@ +/* 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 + +#define WP_NAME "libwolfprov" + +static OSSL_LIB_CTX* wp_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 WC_RNG g_rng; + +static int load_all(const char* wp_path) +{ + 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; + } + /* 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) { + 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 (wp_ctx) OSSL_LIB_CTX_free(wp_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 (*pub == NULL) { + return 0; + } + if (EVP_PKEY_get_octet_string_param(src, OSSL_PKEY_PARAM_PUB_KEY, *pub, + *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; + } + } + } + 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); + 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; +} + +/* 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; } + 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; + rc = wc_MlKemKey_Encapsulate(&key, *ct, ss, &g_rng); + wc_MlKemKey_Free(&key); + if (rc != 0) { + OPENSSL_free(*ct); + *ct = NULL; + return 0; + } + return 1; +} + +/* 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 (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; +} + +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 (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); +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); + 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); + 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 */ diff --git a/test/test_mldsa.c b/test/test_mldsa.c new file mode 100644 index 00000000..d094d3f9 --- /dev/null +++ b/test/test_mldsa.c @@ -0,0 +1,472 @@ +/* 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 +#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 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. + */ +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..d336f2f0 --- /dev/null +++ b/test/test_mlkem.c @@ -0,0 +1,482 @@ +/* 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 wp_test_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 = wp_test_mlkem_keygen(lvl->name, &pkey1); + if (err == 0) { + err = wp_test_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 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 = wp_test_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. + */ +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 = wp_test_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 = wp_test_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 = wp_test_mlkem_keygen(lvl->name, &keyA); + if (err == 0) { + err = wp_test_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..1656962b 100644 --- a/test/unit.c +++ b/test/unit.c @@ -478,6 +478,23 @@ 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_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), +#endif + +#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), + 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..2616f9cb 100644 --- a/test/unit.h +++ b/test/unit.h @@ -477,4 +477,21 @@ int test_des3_tls_cbc_bad_pad(void *data); #endif #endif +#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); +#endif + +#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); +int test_mldsa_verify_wrong_key(void *data); +#endif + #endif /* UNIT_H */