diff --git a/test/test_mldsa.c b/test/test_mldsa.c index 8a651d41..65c9e512 100644 --- a/test/test_mldsa.c +++ b/test/test_mldsa.c @@ -57,12 +57,13 @@ static const unsigned char mldsa_test_msg[] = * @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) +static int mldsa_keygen_ex(OSSL_LIB_CTX* libCtx, const char* prop, + const char* name, EVP_PKEY** pkey) { int err = 0; EVP_PKEY_CTX* ctx = NULL; - ctx = EVP_PKEY_CTX_new_from_name(wpLibCtx, name, NULL); + ctx = EVP_PKEY_CTX_new_from_name(libCtx, name, prop); err = (ctx == NULL); if (err == 0) { err = EVP_PKEY_keygen_init(ctx) != 1; @@ -74,6 +75,11 @@ static int mldsa_keygen(const char* name, EVP_PKEY** pkey) return err; } +static int mldsa_keygen(const char* name, EVP_PKEY** pkey) +{ + return mldsa_keygen_ex(wpLibCtx, NULL, name, pkey); +} + /** * Extract the raw public key bytes from an ML-DSA EVP_PKEY. */ @@ -103,8 +109,9 @@ static int mldsa_get_pub(EVP_PKEY* pkey, unsigned char** out, size_t* len) * 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) +static int mldsa_sign_msg_ex(OSSL_LIB_CTX* libCtx, const char* prop, + EVP_PKEY* pkey, const unsigned char* msg, size_t msgLen, + unsigned char** sigOut, size_t* sigLenOut) { int err = 0; EVP_MD_CTX* mdctx = NULL; @@ -114,7 +121,7 @@ static int mldsa_sign_msg(EVP_PKEY* pkey, const unsigned char* msg, mdctx = EVP_MD_CTX_new(); err = (mdctx == NULL); if (err == 0) { - err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, pkey, + err = EVP_DigestSignInit_ex(mdctx, NULL, NULL, libCtx, prop, pkey, NULL) != 1; } if (err == 0) { @@ -138,13 +145,21 @@ static int mldsa_sign_msg(EVP_PKEY* pkey, const unsigned char* msg, return err; } +static int mldsa_sign_msg(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, unsigned char** sigOut, size_t* sigLenOut) +{ + return mldsa_sign_msg_ex(wpLibCtx, NULL, pkey, msg, msgLen, sigOut, + sigLenOut); +} + /** * 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) +static int mldsa_verify_msg_ex(OSSL_LIB_CTX* libCtx, const char* prop, + EVP_PKEY* pkey, const unsigned char* msg, size_t msgLen, + const unsigned char* sig, size_t sigLen) { int ok = 0; int rc; @@ -154,7 +169,8 @@ static int mldsa_verify_msg(EVP_PKEY* pkey, const unsigned char* msg, if (mdctx == NULL) { return 0; } - rc = EVP_DigestVerifyInit_ex(mdctx, NULL, NULL, wpLibCtx, NULL, pkey, NULL); + rc = EVP_DigestVerifyInit_ex(mdctx, NULL, NULL, libCtx, prop, pkey, + NULL); if (rc == 1) { rc = EVP_DigestVerify(mdctx, sig, sigLen, msg, msgLen); if (rc == 1) { @@ -165,6 +181,13 @@ static int mldsa_verify_msg(EVP_PKEY* pkey, const unsigned char* msg, return ok; } +static int mldsa_verify_msg(EVP_PKEY* pkey, const unsigned char* msg, + size_t msgLen, const unsigned char* sig, size_t sigLen) +{ + return mldsa_verify_msg_ex(wpLibCtx, NULL, pkey, msg, msgLen, sig, + sigLen); +} + /** * Test ML-DSA key generation; verify pub-key size and that two keys differ. */ @@ -1262,4 +1285,192 @@ int test_mldsa_encode_epki(void* data) } #endif /* WP_HAVE_EPKI_TEST */ +/** + * Export the raw public and private key through the key management export. + * + * EVP_PKEY_todata reaches the provider's export function, which the + * parameter getters do not. + * + * @param [in] pkey ML-DSA key. + * @param [out] pub Public key bytes (caller frees). + * @param [out] pubLen Length of public key. + * @param [out] priv Private key bytes (caller frees). + * @param [out] privLen Length of private key. + * @return 0 on success, non-zero on failure. + */ +static int mldsa_get_raw_pair(EVP_PKEY* pkey, unsigned char** pub, + size_t* pubLen, unsigned char** priv, size_t* privLen) +{ + int err; + OSSL_PARAM* params = NULL; + const OSSL_PARAM* p; + + *pub = NULL; + *priv = NULL; + + err = EVP_PKEY_todata(pkey, EVP_PKEY_KEYPAIR, ¶ms) != 1; + if (err) { + PRINT_ERR_MSG("Export of raw key from provider failed"); + } + if (err == 0) { + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_PUB_KEY); + err = (p == NULL) + || (OSSL_PARAM_get_octet_string(p, (void**)pub, 0, pubLen) != 1); + } + if (err == 0) { + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_PRIV_KEY); + err = (p == NULL) + || (OSSL_PARAM_get_octet_string(p, (void**)priv, 0, privLen) != 1); + } + if (err) { + OPENSSL_free(*pub); + *pub = NULL; + } + + OSSL_PARAM_free(params); + return err; +} + +/** + * Move a raw ML-DSA key from one provider to the other and use it. + * + * The imported key signs and the original verifies, and the other way + * round, so the transfer is shown to be usable rather than byte equal. + * + * @param [in] lvl Parameter set being tested. + * @param [in] srcCtx Library context to generate the key in. + * @param [in] srcProp Property query for the source provider. + * @param [in] dstCtx Library context to import the key into. + * @param [in] dstProp Property query for the destination provider. + * @return 0 on success, non-zero on failure. + */ +static int mldsa_cross_transfer(const mldsa_test_level* lvl, + OSSL_LIB_CTX* srcCtx, const char* srcProp, OSSL_LIB_CTX* dstCtx, + const char* dstProp) +{ + int err = 0; + EVP_PKEY* src = NULL; + EVP_PKEY* dst = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + unsigned char* pub = NULL; + unsigned char* pub2 = NULL; + unsigned char* priv = NULL; + unsigned char* sig = NULL; + size_t pubLen = 0; + size_t pub2Len = 0; + size_t privLen = 0; + size_t sigLen = 0; + + err = mldsa_keygen_ex(srcCtx, srcProp, lvl->name, &src); + if (err == 0) { + err = mldsa_get_raw_pair(src, &pub, &pubLen, &priv, &privLen); + } + if (err == 0) { + err = (pubLen != lvl->pubKeySize); + if (err) { + PRINT_ERR_MSG("Raw public key size does not match the level"); + } + } + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(dstCtx, lvl->name, dstProp); + 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, pub, pubLen) != 1 + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen) != 1; + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + OSSL_PARAM_BLD_free(bld); + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &dst, EVP_PKEY_KEYPAIR, params) != 1; + if (err) { + PRINT_ERR_MSG("Import of raw key into other provider failed"); + } + } + if (err == 0) { + err = mldsa_get_pub(dst, &pub2, &pub2Len); + } + if (err == 0) { + err = (pubLen != pub2Len) || (memcmp(pub, pub2, pubLen) != 0); + if (err) { + PRINT_ERR_MSG("Public key changed crossing providers"); + } + } + /* The imported key signs, the original verifies. */ + if (err == 0) { + err = mldsa_sign_msg_ex(dstCtx, dstProp, dst, mldsa_test_msg, + MLDSA_TEST_MSG_LEN, &sig, &sigLen); + } + if (err == 0) { + err = mldsa_verify_msg_ex(srcCtx, srcProp, src, mldsa_test_msg, + MLDSA_TEST_MSG_LEN, sig, sigLen) != 1; + if (err) { + PRINT_ERR_MSG("Signature from imported key did not verify"); + } + } + + /* And the other way round, so the imported public key is used too. */ + if (err == 0) { + OPENSSL_free(sig); + sig = NULL; + sigLen = 0; + err = mldsa_sign_msg_ex(srcCtx, srcProp, src, mldsa_test_msg, + MLDSA_TEST_MSG_LEN, &sig, &sigLen); + } + if (err == 0) { + err = mldsa_verify_msg_ex(dstCtx, dstProp, dst, mldsa_test_msg, + MLDSA_TEST_MSG_LEN, sig, sigLen) != 1; + if (err) { + PRINT_ERR_MSG("Imported key did not verify the original's " + "signature"); + } + } + + OPENSSL_free(pub); + OPENSSL_free(pub2); + OPENSSL_free(sig); + OPENSSL_clear_free(priv, privLen); + OSSL_PARAM_free(params); + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(src); + EVP_PKEY_free(dst); + return err; +} + +/** + * Test raw ML-DSA keys moving between wolfProvider and OpenSSL. + */ +int test_mldsa_cross_provider_raw(void* data) +{ + int err = 0; + size_t i; + + (void)data; + + for (i = 0; (err == 0) && (i < MLDSA_LEVEL_COUNT); i++) { + const mldsa_test_level* lvl = &mldsa_levels[i]; + + PRINT_MSG("Cross-provider raw key OpenSSL to wolfProvider %s", + lvl->name); + err = mldsa_cross_transfer(lvl, osslLibCtx, "provider=default", + wpLibCtx, NULL); + if (err == 0) { + PRINT_MSG("Cross-provider raw key wolfProvider to OpenSSL %s", + lvl->name); + err = mldsa_cross_transfer(lvl, wpLibCtx, NULL, osslLibCtx, + "provider=default"); + } + } + return err; +} + #endif /* WP_HAVE_MLDSA */ diff --git a/test/test_mlkem.c b/test/test_mlkem.c index 8000285b..aa714df6 100644 --- a/test/test_mlkem.c +++ b/test/test_mlkem.c @@ -162,7 +162,7 @@ int test_mlkem_keygen(void* data) /** * Test ML-KEM raw key import/export round-trip. * - * For each level: keygen, export both pub and priv via EVP_PKEY_todata, + * For each level: keygen, read both pub and priv as key parameters, * 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. @@ -988,4 +988,252 @@ int test_mlx_encap_decap(void* data) return err; } +/** + * Encapsulate with one key and decapsulate with the other. + * + * @param [in] lvl Parameter set being tested. + * @param [in] encKey Key to encapsulate with. + * @param [in] encCtx Library context owning the encapsulating key. + * @param [in] encProp Property query for the encapsulating provider. + * @param [in] decKey Key to decapsulate with. + * @param [in] decCtx Library context owning the decapsulating key. + * @param [in] decProp Property query for the decapsulating provider. + * @return 0 on success, non-zero on failure. + */ +static int mlkem_cross_use(const mlkem_test_level* lvl, EVP_PKEY* encKey, + OSSL_LIB_CTX* encCtx, const char* encProp, EVP_PKEY* decKey, + OSSL_LIB_CTX* decCtx, const char* decProp) +{ + int err = 0; + 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; + + ectx = EVP_PKEY_CTX_new_from_pkey(encCtx, encKey, encProp); + err = (ectx == NULL) || (EVP_PKEY_encapsulate_init(ectx, NULL) != 1); + if (err == 0) { + err = EVP_PKEY_encapsulate(ectx, NULL, &ctLen, NULL, &ss1Len) != 1; + } + if (err == 0) { + ct = (unsigned char*)OPENSSL_malloc(ctLen); + ss1 = (unsigned char*)OPENSSL_malloc(ss1Len); + err = (ct == NULL) || (ss1 == NULL); + } + if (err == 0) { + err = EVP_PKEY_encapsulate(ectx, ct, &ctLen, ss1, &ss1Len) != 1; + } + if (err == 0) { + err = (ctLen != lvl->ctSize); + if (err) { + PRINT_ERR_MSG("Unexpected ciphertext size crossing providers"); + } + } + if (err == 0) { + dctx = EVP_PKEY_CTX_new_from_pkey(decCtx, decKey, decProp); + err = (dctx == NULL) || (EVP_PKEY_decapsulate_init(dctx, NULL) != 1); + } + if (err == 0) { + ss2Len = ss1Len; + ss2 = (unsigned char*)OPENSSL_malloc(ss2Len); + err = (ss2 == NULL); + } + if (err == 0) { + err = EVP_PKEY_decapsulate(dctx, ss2, &ss2Len, ct, ctLen) != 1; + if (err) { + PRINT_ERR_MSG("Decapsulate of other provider's ciphertext failed"); + } + } + if (err == 0) { + err = (ss1Len != ss2Len) || (memcmp(ss1, ss2, ss1Len) != 0); + if (err) { + PRINT_ERR_MSG("Shared secret differs across providers"); + } + } + + OPENSSL_free(ct); + OPENSSL_clear_free(ss1, ss1Len); + OPENSSL_clear_free(ss2, ss2Len); + EVP_PKEY_CTX_free(ectx); + EVP_PKEY_CTX_free(dctx); + return err; +} + +/** + * Export the raw public and private key through the key management export. + * + * EVP_PKEY_todata reaches the provider's export function, which the + * parameter getters do not. + * + * @param [in] pkey ML-KEM key. + * @param [out] pub Public key bytes (caller frees). + * @param [out] pubLen Length of public key. + * @param [out] priv Private key bytes (caller frees). + * @param [out] privLen Length of private key. + * @return 0 on success, non-zero on failure. + */ +static int mlkem_export_raw_pair(EVP_PKEY* pkey, unsigned char** pub, + size_t* pubLen, unsigned char** priv, size_t* privLen) +{ + int err; + OSSL_PARAM* params = NULL; + const OSSL_PARAM* p; + + *pub = NULL; + *priv = NULL; + + err = EVP_PKEY_todata(pkey, EVP_PKEY_KEYPAIR, ¶ms) != 1; + if (err) { + PRINT_ERR_MSG("Export of raw key from provider failed"); + } + if (err == 0) { + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_PUB_KEY); + err = (p == NULL) + || (OSSL_PARAM_get_octet_string(p, (void**)pub, 0, pubLen) != 1); + } + if (err == 0) { + p = OSSL_PARAM_locate_const(params, OSSL_PKEY_PARAM_PRIV_KEY); + err = (p == NULL) + || (OSSL_PARAM_get_octet_string(p, (void**)priv, 0, privLen) != 1); + } + if (err) { + OPENSSL_free(*pub); + *pub = NULL; + } + + OSSL_PARAM_free(params); + return err; +} + +/** + * Move a raw ML-KEM key from one provider to the other and use it. + * + * Exports the raw public and private keys from a key generated in the + * source provider through the key management export, imports them into the + * destination provider and checks the public key read back is unchanged. + * Each key then decapsulates a ciphertext made for the other, so both + * imported halves are shown to be usable rather than merely byte equal. + * + * @param [in] lvl Parameter set being tested. + * @param [in] srcCtx Library context to generate the key in. + * @param [in] srcProp Property query for the source provider. + * @param [in] dstCtx Library context to import the key into. + * @param [in] dstProp Property query for the destination provider. + * @return 0 on success, non-zero on failure. + */ +static int mlkem_cross_transfer(const mlkem_test_level* lvl, + OSSL_LIB_CTX* srcCtx, const char* srcProp, OSSL_LIB_CTX* dstCtx, + const char* dstProp) +{ + int err = 0; + EVP_PKEY* src = NULL; + EVP_PKEY* dst = NULL; + EVP_PKEY_CTX* ctx = NULL; + OSSL_PARAM* params = NULL; + unsigned char* pub = NULL; + unsigned char* pub2 = NULL; + unsigned char* priv = NULL; + size_t pubLen = 0; + size_t pub2Len = 0; + size_t privLen = 0; + + err = wp_test_mlkem_keygen_ex(srcCtx, srcProp, lvl->name, &src); + if (err == 0) { + err = mlkem_export_raw_pair(src, &pub, &pubLen, &priv, &privLen); + } + if (err == 0) { + err = (pubLen != lvl->pubKeySize) || (privLen != lvl->privKeySize); + if (err) { + PRINT_ERR_MSG("Raw key sizes do not match the parameter set"); + } + } + + if (err == 0) { + ctx = EVP_PKEY_CTX_new_from_name(dstCtx, lvl->name, dstProp); + 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, pub, pubLen) != 1 + || OSSL_PARAM_BLD_push_octet_string(bld, + OSSL_PKEY_PARAM_PRIV_KEY, priv, privLen) != 1; + if (err == 0) { + params = OSSL_PARAM_BLD_to_param(bld); + err = (params == NULL); + } + OSSL_PARAM_BLD_free(bld); + } + if (err == 0) { + err = EVP_PKEY_fromdata(ctx, &dst, EVP_PKEY_KEYPAIR, params) != 1; + if (err) { + PRINT_ERR_MSG("Import of raw key into other provider failed"); + } + } + if (err == 0) { + err = mlkem_get_pub(dst, &pub2, &pub2Len); + } + if (err == 0) { + err = (pubLen != pub2Len) || (memcmp(pub, pub2, pubLen) != 0); + if (err) { + PRINT_ERR_MSG("Public key changed crossing providers"); + } + } + if (err == 0) { + err = mlkem_cross_use(lvl, dst, dstCtx, dstProp, src, srcCtx, + srcProp); + } + /* And the other way round, so the imported private key decapsulates. */ + if (err == 0) { + err = mlkem_cross_use(lvl, src, srcCtx, srcProp, dst, dstCtx, + dstProp); + } + + OPENSSL_free(pub); + OPENSSL_free(pub2); + OPENSSL_clear_free(priv, privLen); + OSSL_PARAM_free(params); + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(src); + EVP_PKEY_free(dst); + return err; +} + +/** + * Test raw ML-KEM keys moving between wolfProvider and OpenSSL. + * + * Covers both directions for every parameter set. wolfProvider has no + * ASN.1 encoder for ML-KEM, so the raw parameter interface is the only + * way a key can cross between providers. + */ +int test_mlkem_cross_provider_raw(void* data) +{ + int err = 0; + size_t i; + + (void)data; + + for (i = 0; (err == 0) && (i < MLKEM_LEVEL_COUNT); i++) { + const mlkem_test_level* lvl = &mlkem_levels[i]; + + PRINT_MSG("Cross-provider raw key OpenSSL to wolfProvider %s", + lvl->name); + err = mlkem_cross_transfer(lvl, osslLibCtx, "provider=default", + wpLibCtx, NULL); + if (err == 0) { + PRINT_MSG("Cross-provider raw key wolfProvider to OpenSSL %s", + lvl->name); + err = mlkem_cross_transfer(lvl, wpLibCtx, NULL, osslLibCtx, + "provider=default"); + } + } + return err; +} + #endif /* WP_HAVE_MLKEM */ diff --git a/test/unit.c b/test/unit.c index da5d820d..75b512c0 100644 --- a/test/unit.c +++ b/test/unit.c @@ -1,6 +1,6 @@ /* unit.c * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -613,6 +613,7 @@ TEST_CASE test_case[] = { TEST_DECL(test_mlkem_decap_wrong_key, NULL), TEST_DECL(test_mlkem_dup, NULL), TEST_DECL(test_mlkem_match, NULL), + TEST_DECL(test_mlkem_cross_provider_raw, NULL), TEST_DECL(test_mlkem_decap_size_query, NULL), TEST_DECL(test_mlkem_get_params, NULL), TEST_DECL(test_mlkem_import_mismatched_pubpriv, NULL), @@ -641,6 +642,7 @@ TEST_CASE test_case[] = { TEST_DECL(test_mldsa_reinit_null_key, NULL), TEST_DECL(test_mldsa_encode_decode, NULL), TEST_DECL(test_mldsa_x509_sign_verify, NULL), + TEST_DECL(test_mldsa_cross_provider_raw, NULL), #endif #if defined(WP_HAVE_SLHDSA) && defined(WP_SLHDSA_TEST_SETS) diff --git a/test/unit.h b/test/unit.h index 86c255ca..bdbe79c1 100644 --- a/test/unit.h +++ b/test/unit.h @@ -1,6 +1,6 @@ /* unit.h * - * Copyright (C) 2006-2025 wolfSSL Inc. + * Copyright (C) 2006-2026 wolfSSL Inc. * * This file is part of wolfProvider. * @@ -648,6 +648,7 @@ int test_mlkem_decap_tampered_ct(void *data); int test_mlkem_decap_wrong_key(void *data); int test_mlkem_dup(void *data); int test_mlkem_match(void *data); +int test_mlkem_cross_provider_raw(void *data); int test_mlkem_decap_size_query(void *data); int test_mlkem_get_params(void *data); int test_mlkem_import_mismatched_pubpriv(void *data); @@ -676,6 +677,7 @@ int test_mldsa_empty_message(void *data); int test_mldsa_reinit_null_key(void *data); int test_mldsa_encode_decode(void *data); int test_mldsa_x509_sign_verify(void *data); +int test_mldsa_cross_provider_raw(void *data); #endif #if defined(WP_HAVE_SLHDSA) && defined(WP_SLHDSA_TEST_SETS)