From 01474df524be8a4bfec6698bfb4cb2e97df21baf Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 30 Sep 2026 15:16:45 -0600 Subject: [PATCH 1/4] mldsa: derive the public key for a private-only host key --- README.md | 11 +++++ configure.ac | 12 ++++++ src/internal.c | 103 +++++++++++++++++++++++++++++++++++++++------ tests/auth.c | 15 ++++--- tests/unit.c | 38 +++++++++++------ wolfssh/internal.h | 13 ++++++ 6 files changed, 161 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 15b3f2935..8d11adbb0 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,17 @@ After that, configure and build wolfSSH as usual: $ ./configure $ make all +A private-only ML-DSA key, used as a host key or read as a client key with +`wolfSSH_ReadKey_buffer()`/`wolfSSH_ReadKey_file()`, loads when wolfSSL +provides `wc_MlDsaKey_MakePublicKey()`; wolfSSH derives the public key. This +is on when `./configure` detects the function, or in any build when wolfSSL +defines `WC_MLDSA_HAVE_MAKE_PUBLIC_KEY` alongside it. A build without +`./configure` against a wolfSSL that has the function but not that macro +must define `WOLFSSH_HAVE_MLDSA_DERIVE_PUB`. Without it, such keys are +rejected with `WS_CRYPTO_FAILED`. +An ML-DSA private key of a level disabled in wolfSSH +(`WOLFSSH_NO_MLDSA44/65/87`) is rejected at load with `WS_UNIMPLEMENTED_E`. + The wolfSSH client and server will automatically negotiate using ML-KEM-768 hybridized with ECDHE over the P-256 ECC curve and ML-DSA for host keys/client public key authentication. diff --git a/configure.ac b/configure.ac index b747b39b6..d1d88d75a 100644 --- a/configure.ac +++ b/configure.ac @@ -89,6 +89,18 @@ AC_ARG_WITH(wolfssl, AC_CHECK_LIB([wolfssl],[wolfCrypt_Init],,[AC_MSG_ERROR([libwolfssl is required for ${PACKAGE}. It can be obtained from https://www.wolfssl.com/download.html/ .])]) AC_CHECK_FUNCS([gethostbyname getaddrinfo gettimeofday inet_ntoa memset socket wc_ecc_set_rng]) +# Check if the wc_MlDsaKey_MakePublicKey API is available. +# The declaration is ML-DSA config-gated, so probe through the real header. +AC_MSG_CHECKING([for wc_MlDsaKey_MakePublicKey]) +AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[#include +#include ]], + [[(void)wc_MlDsaKey_MakePublicKey(NULL);]])], + [AC_DEFINE([WOLFSSH_HAVE_MLDSA_DERIVE_PUB], [1], + [wc_MlDsaKey_MakePublicKey() available and declared]) + AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no])]) + # futimens()/utimensat() declarations are feature-test-macro gated, so a plain # AC_CHECK_FUNCS link test can pass while the prototype stays hidden at compile # time. Probe through the real header so a positive result is build-safe. diff --git a/src/internal.c b/src/internal.c index 9759ca5f1..091dd940c 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2001,6 +2001,43 @@ static int IsCompositeMlDsaId(byte id) #endif +#ifndef WOLFSSH_NO_MLDSA +/* Nonzero when keyId is an ML-DSA level wolfSSH was built without. */ +static int MlDsaIdDisabled(byte keyId) +{ + WOLFSSH_UNUSED(keyId); + return + #ifdef WOLFSSH_NO_MLDSA44 + keyId == ID_MLDSA44 || + #endif + #ifdef WOLFSSH_NO_MLDSA65 + keyId == ID_MLDSA65 || + #endif + #ifdef WOLFSSH_NO_MLDSA87 + keyId == ID_MLDSA87 || + #endif + 0; +} + + +/* MlDsaIdDisabled() for a decoded key. */ +static int MlDsaLevelDisabled(MlDsaKey* key) +{ + byte level = 0; + + if (wc_MlDsaKey_GetParams(key, &level) != 0) + return 0; + if (level == WC_ML_DSA_44) + return MlDsaIdDisabled(ID_MLDSA44); + if (level == WC_ML_DSA_65) + return MlDsaIdDisabled(ID_MLDSA65); + if (level == WC_ML_DSA_87) + return MlDsaIdDisabled(ID_MLDSA87); + return 0; +} +#endif + + void wolfSSH_KEY_clean(WS_KeySignature* key) { if (key != NULL) { @@ -2070,10 +2107,14 @@ void wolfSSH_KEY_clean(WS_KeySignature* key) * fails try to load it as if ECDSA. Both public and private keys can be * decoded. For RSA keys, the key format is described as "ssh-rsa". * - * Private-only ML-DSA keys are rejected (WS_CRYPTO_FAILED) as public keys - * cannot be derived. ECDSA derives and validates the public key here. + * Private-only ML-DSA keys have their public key derived here when + * WOLFSSH_HAVE_MLDSA_DERIVE_PUB is set; otherwise, or when derivation + * fails, they are rejected (WS_CRYPTO_FAILED, or WS_MEMORY_E if it ran + * out of memory). A private ML-DSA key of a level disabled in wolfSSH is + * rejected with WS_UNIMPLEMENTED_E. ECDSA derives and validates the public + * key here. * Ed25519 allows missing public keys if HAVE_ED25519_MAKE_KEY is defined - * (derived later at KEX); otherwise rejected like ML-DSA. + * (derived later at KEX); otherwise rejected. * * @param in key to identify * @param inSz size of key @@ -2090,8 +2131,9 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, word32 idx; int ret; int dynType = isPrivate ? DYNTYPE_PRIVKEY : DYNTYPE_PUBKEY; - /* Set to WS_CRYPTO_FAILED if ML-DSA key lacks derivable public key. - * Prevents Ed25519 fallback decode. */ + /* Set to the rejection code when a private key lacks a derivable + * public key or is of a disabled ML-DSA level. Prevents a fallback + * decode as another key type. */ int noPubKeyRet = 0; #ifndef WOLFSSH_NO_MLDSA byte mlDsaLevel = 0; @@ -2208,14 +2250,32 @@ int IdentifyAsn1Key(const byte* in, word32 inSz, int isPrivate, void* heap, if (isPrivate) { ret = wc_MlDsaKey_PrivateKeyDecode(&key->ks.mldsa.key, in, inSz, &idx); - if (ret == 0) { - /* Priv-only decode can succeed with no derivable - * public key; reject here instead of at first - * handshake. */ - if (!key->ks.mldsa.key.pubKeySet) { - WLOG(WS_LOG_ERROR, - "ML-DSA priv-only key rejected; no derivable pubkey"); - ret = WS_CRYPTO_FAILED; + if (ret == 0 && MlDsaLevelDisabled(&key->ks.mldsa.key)) { + /* Either DER form; before paying for derivation. */ + WLOG(WS_LOG_ERROR, "ML-DSA level not enabled in " + "this build"); + ret = WS_UNIMPLEMENTED_E; + noPubKeyRet = ret; + } + else if (ret == 0 && !key->ks.mldsa.key.pubKeySet) { + /* Derive the public key now so underivable keys + * are rejected at load time instead of handshake. */ + #ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + int makeRet = wc_MlDsaKey_MakePublicKey( + &key->ks.mldsa.key); + #else + int makeRet = WC_NO_ERR_TRACE(NOT_COMPILED_IN); + #endif + if (makeRet != 0) { + WLOG(WS_LOG_ERROR, "ML-DSA priv-only key " + "rejected; no derivable pubkey (%d)", + makeRet); + if (makeRet == WC_NO_ERR_TRACE(MEMORY_E)) { + ret = WS_MEMORY_E; + } + else { + ret = WS_CRYPTO_FAILED; + } noPubKeyRet = ret; } } @@ -2950,6 +3010,16 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, WFREE(der, ctx->heap, dynamicType); ret = WS_BAD_ARGUMENT; } +#endif +#ifndef WOLFSSH_NO_MLDSA + /* Defensive: IdentifyAsn1Key() already refuses a disabled level. */ + else if (MlDsaIdDisabled(keyId)) { + WLOG(WS_LOG_ERROR, "SetHostPrivateKey: ML-DSA level not enabled " + "in this build"); + WS_FORCEZERO(der, derSz); + WFREE(der, ctx->heap, dynamicType); + ret = WS_UNIMPLEMENTED_E; + } #endif else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; @@ -16177,6 +16247,13 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, &sigKeyBlock_ptr->sk.mldsa.key, ssh->ctx->privateKey[keyIdx].key, ssh->ctx->privateKey[keyIdx].keySz, &scratch); + #ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + /* The exporters below are pure accessors; a private-only host + * key needs its public half derived first. */ + if (ret == 0 && !sigKeyBlock_ptr->sk.mldsa.key.pubKeySet) + ret = wc_MlDsaKey_MakePublicKey( + &sigKeyBlock_ptr->sk.mldsa.key); + #endif if (ret == 0) ret = wc_MlDsaKey_ExportPubRaw( &sigKeyBlock_ptr->sk.mldsa.key, diff --git a/tests/auth.c b/tests/auth.c index 01ee216b7..8f3e7d5ae 100644 --- a/tests/auth.c +++ b/tests/auth.c @@ -1415,9 +1415,9 @@ static void test_pubkey_auth_wrong_key(void) #if !defined(WOLFSSH_NO_MLDSA) && !defined(WOLFSSH_NO_MLDSA44) && \ defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) -/* Confirms a private-only ML-DSA host key is rejected through the real - * load path (wolfSSH_CTX_UsePrivateKey_buffer), not just IdentifyAsn1Key - * called directly as in the unit test. */ +/* Covers the private-only ML-DSA host key through the real load path + * (wolfSSH_CTX_UsePrivateKey_buffer), not just IdentifyAsn1Key called + * directly as in the unit test. */ static void test_pubkey_load_mldsa_privonly_hostkey(void) { WOLFSSH_CTX* ctx; @@ -1425,8 +1425,13 @@ static void test_pubkey_load_mldsa_privonly_hostkey(void) WC_RNG mlRng; byte* mlDer; int mlDerSz; +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + const int expect = WS_SUCCESS; +#else + const int expect = WS_CRYPTO_FAILED; +#endif - printf("Testing ML-DSA private-only host key load rejection\n"); + printf("Testing ML-DSA private-only host key load\n"); WMEMSET(&mlKey, 0, sizeof(mlKey)); AssertIntEQ(wc_MlDsaKey_Init(&mlKey, NULL, INVALID_DEVID), 0); @@ -1445,7 +1450,7 @@ static void test_pubkey_load_mldsa_privonly_hostkey(void) ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); AssertNotNull(ctx); AssertIntEQ(wolfSSH_CTX_UsePrivateKey_buffer(ctx, mlDer, (word32)mlDerSz, - WOLFSSH_FORMAT_ASN1), WS_CRYPTO_FAILED); + WOLFSSH_FORMAT_ASN1), expect); wolfSSH_CTX_free(ctx); WFREE(mlDer, NULL, 0); diff --git a/tests/unit.c b/tests/unit.c index 8dfa18669..74871deff 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -16849,17 +16849,26 @@ static int test_ECCKexDeriveFallbackFailure(void) !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ (!defined(WOLFSSH_NO_MLDSA44) || !defined(WOLFSSH_NO_MLDSA65) || \ !defined(WOLFSSH_NO_MLDSA87)) -/* Private-only DER: rejected at decode time. Shared across 44/65/87 levels. +/* Private-only DER: the public key is derived at decode time where wolfSSL + * supports it, otherwise rejected there. Shared across 44/65/87 levels. * Return codes -692..-699 */ static int test_IdentifyAsn1Key_MlDsaPrivOnlyDer(byte level, - word32 derBufSz, const char* levelName) + word32 derBufSz, int expectedKeyId, const char* levelName) { +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + const int expect = expectedKeyId; +#else + const int expect = WS_CRYPTO_FAILED; +#endif int ret; MlDsaKey mlKey; WC_RNG mlRng; byte* mlDer = NULL; int mlDerSz; +#ifndef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + WOLFSSH_UNUSED(expectedKeyId); +#endif WMEMSET(&mlKey, 0, sizeof(mlKey)); if (wc_MlDsaKey_Init(&mlKey, NULL, INVALID_DEVID) != 0) { return -692; @@ -16892,27 +16901,30 @@ static int test_IdentifyAsn1Key_MlDsaPrivOnlyDer(byte level, } ret = IdentifyAsn1Key(mlDer, (word32)mlDerSz, 1, NULL, NULL); - if (ret != WS_CRYPTO_FAILED) { + if (ret != expect) { WFREE(mlDer, NULL, 0); printf("IdentifyAsn1Key: private-only MlDsa %s DER expected " - "WS_CRYPTO_FAILED, got %d\n", levelName, ret); + "%d, got %d\n", levelName, expect, ret); return -698; } - /* Confirms *pkey stays NULL on rejection path. */ + /* On the derive path *pkey comes back set; on the reject path it + * stays NULL. */ { WS_KeySignature* mlKeySig = NULL; + int bad; ret = IdentifyAsn1Key(mlDer, (word32)mlDerSz, 1, NULL, &mlKeySig); WFREE(mlDer, NULL, 0); - if (ret != WS_CRYPTO_FAILED || mlKeySig != NULL) { + bad = (ret != expect) || ((mlKeySig != NULL) != (expect > 0)); + if (mlKeySig != NULL) { + wolfSSH_KEY_clean(mlKeySig); + WFREE(mlKeySig, NULL, DYNTYPE_PRIVKEY); + } + if (bad) { printf("IdentifyAsn1Key: private-only MlDsa %s DER pkey-out " "variant failed, ret=%d\n", levelName, ret); - if (mlKeySig != NULL) { - wolfSSH_KEY_clean(mlKeySig); - WFREE(mlKeySig, NULL, DYNTYPE_PRIVKEY); - } return -699; } } @@ -17584,7 +17596,7 @@ static int test_IdentifyAsn1Key(void) #if defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) ret = test_IdentifyAsn1Key_MlDsaPrivOnlyDer(WC_ML_DSA_44, - WC_MLDSA_44_PRV_KEY_DER_SIZE, "44"); + WC_MLDSA_44_PRV_KEY_DER_SIZE, ID_MLDSA44, "44"); if (ret != 0) { result = ret; goto done; } @@ -17595,7 +17607,7 @@ static int test_IdentifyAsn1Key(void) defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) ret = test_IdentifyAsn1Key_MlDsaPrivOnlyDer(WC_ML_DSA_65, - WC_MLDSA_65_PRV_KEY_DER_SIZE, "65"); + WC_MLDSA_65_PRV_KEY_DER_SIZE, ID_MLDSA65, "65"); if (ret != 0) { result = ret; goto done; } @@ -17605,7 +17617,7 @@ static int test_IdentifyAsn1Key(void) defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) ret = test_IdentifyAsn1Key_MlDsaPrivOnlyDer(WC_ML_DSA_87, - WC_MLDSA_87_PRV_KEY_DER_SIZE, "87"); + WC_MLDSA_87_PRV_KEY_DER_SIZE, ID_MLDSA87, "87"); if (ret != 0) { result = ret; goto done; } diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 7044912cd..10c32efa0 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -147,6 +147,19 @@ extern "C" { #define WOLFSSH_NO_MLDSA87 #endif +/* Check if the wc_MlDsaKey_MakePublicKey API is available. + * A private-only key needs this call made explicitly before its + * public half is read. Set by configure, by wolfSSL's own + * WC_MLDSA_HAVE_MAKE_PUBLIC_KEY, or predefined. */ +#if defined(WC_MLDSA_HAVE_MAKE_PUBLIC_KEY) && \ + !defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) + #define WOLFSSH_HAVE_MLDSA_DERIVE_PUB +#endif +#if defined(WOLFSSH_NO_MLDSA) || defined(WOLFSSL_MLDSA_ASSIGN_KEY) || \ + defined(WOLFSSL_MLDSA_NO_MAKE_KEY) || defined(WOLFSSL_MLDSA_VERIFY_ONLY) + #undef WOLFSSH_HAVE_MLDSA_DERIVE_PUB +#endif + #ifdef NO_SHA #undef WOLFSSH_NO_SHA1 #define WOLFSSH_NO_SHA1 From 02143e4e6812590fed93ac4454af12aba3dd0b9b Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 30 Sep 2026 15:16:56 -0600 Subject: [PATCH 2/4] tests: cover the derived ML-DSA public key --- tests/auth.c | 179 ++++++++++++++++++++++-- tests/unit.c | 388 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 549 insertions(+), 18 deletions(-) diff --git a/tests/auth.c b/tests/auth.c index 8f3e7d5ae..2d2174e4a 100644 --- a/tests/auth.c +++ b/tests/auth.c @@ -1415,23 +1415,13 @@ static void test_pubkey_auth_wrong_key(void) #if !defined(WOLFSSH_NO_MLDSA) && !defined(WOLFSSH_NO_MLDSA44) && \ defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) -/* Covers the private-only ML-DSA host key through the real load path - * (wolfSSH_CTX_UsePrivateKey_buffer), not just IdentifyAsn1Key called - * directly as in the unit test. */ -static void test_pubkey_load_mldsa_privonly_hostkey(void) +/* Generate a private-only ML-DSA-44 host key in PKCS#8 DER. Caller frees. */ +static byte* mldsa_privonly_hostkey_der(int* derSz) { - WOLFSSH_CTX* ctx; MlDsaKey mlKey; WC_RNG mlRng; byte* mlDer; int mlDerSz; -#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB - const int expect = WS_SUCCESS; -#else - const int expect = WS_CRYPTO_FAILED; -#endif - - printf("Testing ML-DSA private-only host key load\n"); WMEMSET(&mlKey, 0, sizeof(mlKey)); AssertIntEQ(wc_MlDsaKey_Init(&mlKey, NULL, INVALID_DEVID), 0); @@ -1447,16 +1437,167 @@ static void test_pubkey_load_mldsa_privonly_hostkey(void) wc_MlDsaKey_Free(&mlKey); AssertIntGT(mlDerSz, 0); + *derSz = mlDerSz; + return mlDer; +} + +/* Covers the private-only ML-DSA host key through the real load path + * (wolfSSH_CTX_UsePrivateKey_buffer), not just IdentifyAsn1Key called + * directly as in the unit test. */ +static void test_pubkey_load_mldsa_privonly_hostkey(void) +{ + WOLFSSH_CTX* ctx; + byte* mlDer; + int mlDerSz; +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + const int expect = WS_SUCCESS; +#else + const int expect = WS_CRYPTO_FAILED; +#endif + + printf("Testing ML-DSA private-only host key load\n"); + + mlDer = mldsa_privonly_hostkey_der(&mlDerSz); + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); AssertNotNull(ctx); AssertIntEQ(wolfSSH_CTX_UsePrivateKey_buffer(ctx, mlDer, (word32)mlDerSz, WOLFSSH_FORMAT_ASN1), expect); wolfSSH_CTX_free(ctx); + WMEMSET(mlDer, 0, mlDerSz); WFREE(mlDer, NULL, 0); } -#endif /* !WOLFSSH_NO_MLDSA && !WOLFSSH_NO_MLDSA44 && WOLFSSL_MLDSA_PRIVATE_KEY - * && !WOLFSSL_MLDSA_NO_ASN1 && !WOLFSSL_MLDSA_NO_MAKE_KEY */ + +#if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && !defined(WOLFSSH_NO_ECDSA) +/* ML-DSA counterpart to test_pubkey_auth_ed25519_privonly_hostkey: a real + * handshake with a private-only host key, exercising the derived public + * key all the way through SendKexGetSigningKey. */ +static void test_pubkey_auth_mldsa_privonly_hostkey(void) +{ + PubkeyServerCtx sCtx = {0}; + PubkeyClientCtx cCtx; + byte pubKeyBuf[512]; + byte* p = pubKeyBuf; + word32 pubKeySz = sizeof(pubKeyBuf); + const byte* pubKeyType = NULL; + word32 pubKeyTypeSz = 0; + byte privKeyBuf[1300]; + byte* privKeyPtr = privKeyBuf; + word32 privKeySz = sizeof(privKeyBuf); + const byte* privKeyType = NULL; + word32 privKeyTypeSz = 0; + byte* hostKeyDer; + int hostKeyDerSz; + + printf("Testing ML-DSA private-only host key at KEX (derived pubkey)\n"); + + hostKeyDer = mldsa_privonly_hostkey_der(&hostKeyDerSz); + + AssertIntEQ(wolfSSH_ReadKey_buffer((const byte*)hanselPublicEcc, + (word32)WSTRLEN(hanselPublicEcc), WOLFSSH_FORMAT_SSH, + &p, &pubKeySz, &pubKeyType, &pubKeyTypeSz, NULL), WS_SUCCESS); + + AssertIntEQ(wc_Sha256Hash(pubKeyBuf, pubKeySz, sCtx.hash), 0); + + AssertIntEQ(wolfSSH_ReadKey_buffer(hanselPrivateEcc, hanselPrivateEccSz, + WOLFSSH_FORMAT_ASN1, + &privKeyPtr, &privKeySz, &privKeyType, &privKeyTypeSz, NULL), + WS_SUCCESS); + + cCtx.publicKeyType = pubKeyType; + cCtx.publicKeyTypeSz = pubKeyTypeSz; + cCtx.publicKey = pubKeyBuf; + cCtx.publicKeySz = pubKeySz; + cCtx.privateKey = privKeyBuf; + cCtx.privateKeySz = privKeySz; + + run_pubkey_test_ex(&sCtx, &cCtx, WS_SUCCESS, hostKeyDer, + (word32)hostKeyDerSz); + + WMEMSET(privKeyBuf, 0, sizeof(privKeyBuf)); + WMEMSET(hostKeyDer, 0, hostKeyDerSz); + WFREE(hostKeyDer, NULL, 0); +} +#endif /* WOLFSSH_HAVE_MLDSA_DERIVE_PUB && !WOLFSSH_NO_ECDSA */ + +#if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && \ + defined(WOLFSSL_MLDSA_PUBLIC_KEY) && \ + (!defined(WOLFSSH_NO_ECDSA) || !defined(WOLFSSH_NO_RSA)) +/* User auth with a private-only ML-DSA-44 client key, read the way an + * application would, through wolfSSH_ReadKey_buffer(). */ +static void test_pubkey_auth_mldsa_privonly_userkey(void) +{ + PubkeyServerCtx sCtx = {0}; + PubkeyClientCtx cCtx; + MlDsaKey mlKey; + WC_RNG rng; + byte* pubDer; + int pubDerSz; + byte* privDer; + int privDerSz; + byte* pubBlob = NULL; + word32 pubBlobSz = 0; + const byte* pubType = NULL; + word32 pubTypeSz = 0; + byte* privKey = NULL; + word32 privKeySz = 0; + const byte* privType = NULL; + word32 privTypeSz = 0; + + printf("Testing ML-DSA private-only client key for user auth\n"); + + WMEMSET(&mlKey, 0, sizeof(mlKey)); + AssertIntEQ(wc_MlDsaKey_Init(&mlKey, NULL, INVALID_DEVID), 0); + AssertIntEQ(wc_MlDsaKey_SetParams(&mlKey, WC_ML_DSA_44), 0); + AssertIntEQ(wc_InitRng(&rng), 0); + AssertIntEQ(wc_MlDsaKey_MakeKey(&mlKey, &rng), 0); + wc_FreeRng(&rng); + + pubDer = (byte*)WMALLOC(WC_MLDSA_44_PUB_KEY_DER_SIZE, NULL, 0); + AssertNotNull(pubDer); + pubDerSz = wc_MlDsaKey_PublicKeyToDer(&mlKey, pubDer, + WC_MLDSA_44_PUB_KEY_DER_SIZE, 1); + AssertIntGT(pubDerSz, 0); + privDer = (byte*)WMALLOC(WC_MLDSA_44_PRV_KEY_DER_SIZE, NULL, 0); + AssertNotNull(privDer); + privDerSz = wc_MlDsaKey_PrivateKeyToDer(&mlKey, privDer, + WC_MLDSA_44_PRV_KEY_DER_SIZE); + wc_MlDsaKey_Free(&mlKey); + AssertIntGT(privDerSz, 0); + + AssertIntEQ(wolfSSH_ReadKey_buffer_ex(pubDer, (word32)pubDerSz, + WOLFSSH_FORMAT_ASN1, &pubBlob, &pubBlobSz, &pubType, &pubTypeSz, + 0, NULL), WS_SUCCESS); + AssertIntEQ(wolfSSH_ReadKey_buffer(privDer, (word32)privDerSz, + WOLFSSH_FORMAT_ASN1, &privKey, &privKeySz, &privType, + &privTypeSz, NULL), WS_SUCCESS); + AssertIntEQ(privTypeSz, pubTypeSz); + AssertIntEQ(WMEMCMP(privType, pubType, pubTypeSz), 0); + + AssertIntEQ(wc_Sha256Hash(pubBlob, pubBlobSz, sCtx.hash), 0); + + cCtx.publicKeyType = pubType; + cCtx.publicKeyTypeSz = pubTypeSz; + cCtx.publicKey = pubBlob; + cCtx.publicKeySz = pubBlobSz; + cCtx.privateKey = privKey; + cCtx.privateKeySz = privKeySz; + + run_pubkey_test_ex(&sCtx, &cCtx, WS_SUCCESS, NULL, 0); + + WMEMSET(privKey, 0, privKeySz); + WFREE(privKey, NULL, DYNTYPE_PRIVKEY); + WFREE(pubBlob, NULL, DYNTYPE_PRIVKEY); + WMEMSET(privDer, 0, privDerSz); + WFREE(privDer, NULL, 0); + WFREE(pubDer, NULL, 0); +} +#endif /* WOLFSSH_HAVE_MLDSA_DERIVE_PUB && WOLFSSL_MLDSA_PUBLIC_KEY && + * (!WOLFSSH_NO_ECDSA || !WOLFSSH_NO_RSA) */ +#endif /* !WOLFSSH_NO_MLDSA && !WOLFSSH_NO_MLDSA44 + * && WOLFSSL_MLDSA_PRIVATE_KEY && !WOLFSSL_MLDSA_NO_ASN1 + * && !WOLFSSL_MLDSA_NO_MAKE_KEY */ #if !defined(WOLFSSH_NO_ED25519) && defined(HAVE_ED25519) && \ defined(HAVE_ED25519_MAKE_KEY) && defined(HAVE_ED25519_KEY_EXPORT) @@ -1591,6 +1732,8 @@ static void test_pubkey_auth_ecdsa_privonly_hostkey(void) run_pubkey_test_ex(&sCtx, &cCtx, WS_SUCCESS, hostKeyDer, (word32)hostKeyDerSz); + WMEMSET(privKeyBuf, 0, sizeof(privKeyBuf)); + WMEMSET(hostKeyDer, 0, hostKeyDerSz); WFREE(hostKeyDer, NULL, 0); } #endif /* !WOLFSSH_NO_ECDSA && !WOLFSSH_NO_RSA */ @@ -2504,6 +2647,14 @@ int wolfSSH_AuthTest(int argc, char** argv) defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) test_pubkey_load_mldsa_privonly_hostkey(); + #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && !defined(WOLFSSH_NO_ECDSA) + test_pubkey_auth_mldsa_privonly_hostkey(); + #endif + #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && \ + defined(WOLFSSL_MLDSA_PUBLIC_KEY) && \ + (!defined(WOLFSSH_NO_ECDSA) || !defined(WOLFSSH_NO_RSA)) + test_pubkey_auth_mldsa_privonly_userkey(); + #endif #endif #endif /* !NO_SHA256 */ diff --git a/tests/unit.c b/tests/unit.c index 74871deff..c8df28487 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -16849,9 +16849,14 @@ static int test_ECCKexDeriveFallbackFailure(void) !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ (!defined(WOLFSSH_NO_MLDSA44) || !defined(WOLFSSH_NO_MLDSA65) || \ !defined(WOLFSSH_NO_MLDSA87)) +/* FIPS 204 expanded private key: rho || K || tr || ... */ +#define MLDSA_TEST_RHO_SZ 32 +#define MLDSA_TEST_K_SZ 32 +#define MLDSA_TEST_TR_SZ 64 + /* Private-only DER: the public key is derived at decode time where wolfSSL * supports it, otherwise rejected there. Shared across 44/65/87 levels. - * Return codes -692..-699 */ + * Return codes -692..-703 */ static int test_IdentifyAsn1Key_MlDsaPrivOnlyDer(byte level, word32 derBufSz, int expectedKeyId, const char* levelName) { @@ -16865,6 +16870,10 @@ static int test_IdentifyAsn1Key_MlDsaPrivOnlyDer(byte level, WC_RNG mlRng; byte* mlDer = NULL; int mlDerSz; +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + byte origPub[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; + word32 origPubSz = sizeof(origPub); +#endif #ifndef WOLFSSH_HAVE_MLDSA_DERIVE_PUB WOLFSSH_UNUSED(expectedKeyId); @@ -16894,30 +16903,139 @@ static int test_IdentifyAsn1Key_MlDsaPrivOnlyDer(byte level, return -696; } mlDerSz = wc_MlDsaKey_PrivateKeyToDer(&mlKey, mlDer, derBufSz); +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + /* Keep the generated public key so the derived one can be compared + * against it; deriving the wrong key would still set pubKeySet. */ + if (wc_MlDsaKey_ExportPubRaw(&mlKey, origPub, &origPubSz) != 0) { + wc_MlDsaKey_Free(&mlKey); + WMEMSET(mlDer, 0, derBufSz); + WFREE(mlDer, NULL, 0); + return -700; + } +#endif wc_MlDsaKey_Free(&mlKey); if (mlDerSz <= 0) { + WMEMSET(mlDer, 0, derBufSz); WFREE(mlDer, NULL, 0); return -697; } ret = IdentifyAsn1Key(mlDer, (word32)mlDerSz, 1, NULL, NULL); if (ret != expect) { + WMEMSET(mlDer, 0, mlDerSz); WFREE(mlDer, NULL, 0); printf("IdentifyAsn1Key: private-only MlDsa %s DER expected " "%d, got %d\n", levelName, expect, ret); return -698; } - /* On the derive path *pkey comes back set; on the reject path it - * stays NULL. */ +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + /* A key whose stored tr does not match the derived public key is + * rejected. rho also opens the public key, so find it in the DER rather + * than assume the layout. */ + { + word32 trIdx = 0, i; + int rhoHits = 0; + WS_KeySignature* badSig = NULL; + + for (i = 0; i + MLDSA_TEST_RHO_SZ + MLDSA_TEST_K_SZ + + MLDSA_TEST_TR_SZ <= (word32)mlDerSz; i++) { + if (WMEMCMP(mlDer + i, origPub, MLDSA_TEST_RHO_SZ) == 0) { + trIdx = i + MLDSA_TEST_RHO_SZ + MLDSA_TEST_K_SZ; + rhoHits++; + } + } + /* A changed encoding would otherwise hollow out this test. */ + if (rhoHits != 1) { + WMEMSET(mlDer, 0, mlDerSz); + WFREE(mlDer, NULL, 0); + printf("IdentifyAsn1Key: private-only MlDsa %s DER has no " + "expanded key (%d rho matches)\n", levelName, rhoHits); + return -702; + } + mlDer[trIdx] ^= 0x5A; + ret = IdentifyAsn1Key(mlDer, (word32)mlDerSz, 1, NULL, &badSig); + mlDer[trIdx] ^= 0x5A; + if (ret != WS_CRYPTO_FAILED || badSig != NULL) { + if (badSig != NULL) { + wolfSSH_KEY_clean(badSig); + WFREE(badSig, NULL, DYNTYPE_PRIVKEY); + } + WMEMSET(mlDer, 0, mlDerSz); + WFREE(mlDer, NULL, 0); + printf("IdentifyAsn1Key: private-only MlDsa %s DER with a bad " + "tr expected WS_CRYPTO_FAILED, got %d\n", levelName, ret); + return -701; + } + } +#endif + + /* wolfSSH_ReadKey_buffer() takes the same path for a client key. */ + { + byte* out = NULL; + word32 outSz = 0; + const byte* outType = NULL; + word32 outTypeSz = 0; + char name[16]; + int bad; + + WSNPRINTF(name, sizeof(name), "ssh-mldsa-%s", levelName); + ret = wolfSSH_ReadKey_buffer(mlDer, (word32)mlDerSz, + WOLFSSH_FORMAT_ASN1, &out, &outSz, &outType, &outTypeSz, + NULL); +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + bad = ret != WS_SUCCESS || out == NULL + || outSz != (word32)mlDerSz + || WMEMCMP(out, mlDer, mlDerSz) != 0 + || outTypeSz != (word32)WSTRLEN(name) + || WMEMCMP(outType, name, outTypeSz) != 0; +#else + bad = ret != WS_CRYPTO_FAILED || out != NULL; +#endif + if (out != NULL) { + WMEMSET(out, 0, outSz); + WFREE(out, NULL, DYNTYPE_PRIVKEY); + } + if (bad) { + WMEMSET(mlDer, 0, mlDerSz); + WFREE(mlDer, NULL, 0); + printf("wolfSSH_ReadKey_buffer: private-only MlDsa %s DER " + "failed, ret=%d\n", levelName, ret); + return -703; + } + } + + /* On the derive path *pkey comes back with the public key set; on the + * reject path it stays NULL. */ { WS_KeySignature* mlKeySig = NULL; int bad; ret = IdentifyAsn1Key(mlDer, (word32)mlDerSz, 1, NULL, &mlKeySig); + WMEMSET(mlDer, 0, mlDerSz); WFREE(mlDer, NULL, 0); - bad = (ret != expect) || ((mlKeySig != NULL) != (expect > 0)); +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + bad = (ret != expect) || mlKeySig == NULL || + mlKeySig->keyId != expectedKeyId || + !mlKeySig->ks.mldsa.key.pubKeySet; + if (!bad) { + byte derivedPub[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; + word32 derivedPubSz = sizeof(derivedPub); + + bad = wc_MlDsaKey_ExportPubRaw(&mlKeySig->ks.mldsa.key, + derivedPub, &derivedPubSz) != 0 + || derivedPubSz != origPubSz + || WMEMCMP(derivedPub, origPub, origPubSz) != 0; + if (bad) { + printf("IdentifyAsn1Key: private-only MlDsa %s derived " + "public key does not match the original\n", + levelName); + } + } +#else + bad = (ret != expect) || mlKeySig != NULL; +#endif if (mlKeySig != NULL) { wolfSSH_KEY_clean(mlKeySig); WFREE(mlKeySig, NULL, DYNTYPE_PRIVKEY); @@ -22719,6 +22837,247 @@ static int test_ShutdownPeerChannelId(void) #endif /* WOLFSSH_TEST_INTERNAL && !NO_WOLFSSH_SERVER */ +#if defined(WOLFSSH_TEST_INTERNAL) && \ + !defined(WOLFSSH_NO_MLDSA) && !defined(WOLFSSH_NO_MLDSA44) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + defined(WOLFSSH_TEST_CAPTURING_ALLOCATOR) && \ + defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) +/* Make an ML-DSA-44 DER (private-only when the load can derive) and its + * public key. Caller frees *derOut. */ +static int mldsa44_der_and_pub(byte** derOut, word32* derSzOut, + byte* pubOut, word32* pubSzOut) +{ + MlDsaKey key; + WC_RNG rng; + byte* der; + int derSz; +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + const word32 derBufSz = WC_MLDSA_44_PRV_KEY_DER_SIZE; +#else + const word32 derBufSz = WC_MLDSA_44_BOTH_KEY_DER_SIZE; +#endif + int ret = 0; + + if (wc_InitRng(&rng) != 0) + return -1; + WMEMSET(&key, 0, sizeof(key)); + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) { + wc_FreeRng(&rng); + return -1; + } + if (wc_MlDsaKey_SetParams(&key, WC_ML_DSA_44) != 0 + || wc_MlDsaKey_MakeKey(&key, &rng) != 0 + || wc_MlDsaKey_ExportPubRaw(&key, pubOut, pubSzOut) != 0) + ret = -1; + wc_FreeRng(&rng); + + der = NULL; + if (ret == 0) { + der = (byte*)WMALLOC(derBufSz, NULL, 0); + if (der == NULL) + ret = -1; + } + if (ret != 0) { + wc_MlDsaKey_Free(&key); + return ret; + } +#ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB + derSz = wc_MlDsaKey_PrivateKeyToDer(&key, der, derBufSz); +#else + derSz = wc_MlDsaKey_KeyToDer(&key, der, derBufSz); +#endif + wc_MlDsaKey_Free(&key); + if (derSz <= 0) { + WMEMSET(der, 0, derBufSz); + WFREE(der, NULL, 0); + return -1; + } + + *derOut = der; + *derSzOut = (word32)derSz; + return 0; +} + + +/* Counts allocations of exactly failAllocSz bytes (any size when 0) and + * fails the failAllocNth one (1-based); 0 only counts. */ +static size_t failAllocSz = 0; +static int failAllocNth = 0; +static int failAllocHits = 0; + +static void* FailSizeMalloc(size_t size) +{ + if ((failAllocSz == 0 || size == failAllocSz) && + ++failAllocHits == failAllocNth) { + return NULL; + } + return malloc(size); +} + + +/* An out-of-memory derivation of a private-only key is WS_MEMORY_E, not + * WS_CRYPTO_FAILED. Fails each allocation in turn: only the derivation's + * can yield WS_MEMORY_E after the first (the WS_KeySignature itself). + * Return codes -740..-743 */ +static int test_MlDsaDeriveOutOfMemory(void) +{ + wolfSSL_Malloc_cb prevMf = NULL; + wolfSSL_Free_cb prevFf = NULL; + wolfSSL_Realloc_cb prevRf = NULL; + byte* der = NULL; + word32 derSz = 0; + byte pub[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; + word32 pubSz = sizeof(pub); + WS_KeySignature* sig = NULL; + int sawMemory = 0; + int nth; + int ret = 0; + int result = 0; + + if (mldsa44_der_and_pub(&der, &derSz, pub, &pubSz) != 0) + return -740; + + wolfSSL_GetAllocators(&prevMf, &prevFf, &prevRf); + failAllocSz = 0; + for (nth = 2; result == 0 && nth < 1000; nth++) { + failAllocNth = nth; + failAllocHits = 0; + if (wolfSSL_SetAllocators(FailSizeMalloc, prevFf, prevRf) != 0) { + result = -741; + break; + } + ret = IdentifyAsn1Key(der, derSz, 1, NULL, &sig); + wolfSSL_SetAllocators(prevMf, prevFf, prevRf); + + if (ret == WS_MEMORY_E) + sawMemory = 1; + if (ret != ID_MLDSA44 && sig != NULL) + result = -742; + if (sig != NULL) { + wolfSSH_KEY_clean(sig); + WFREE(sig, NULL, DYNTYPE_PRIVKEY); + sig = NULL; + } + if (ret == ID_MLDSA44 && failAllocHits < nth) + break; /* nothing left to fail */ + } + if (result == 0 && (ret != ID_MLDSA44 || !sawMemory)) + result = -743; + + WMEMSET(der, 0, derSz); + WFREE(der, NULL, 0); + + return result; +} +#endif /* ML-DSA-44 derive out-of-memory test */ + + +#if defined(WOLFSSH_TEST_INTERNAL) && !defined(WOLFSSH_NO_MLDSA) && \ + defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSL_NO_ML_DSA_87) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) +/* A private key of an ML-DSA level this build disabled is refused, with or + * without the public half in the DER: IdentifyAsn1Key() returns no key, + * wolfSSH_ReadKey_buffer() fails, and the load leaves the CTX unchanged. + * Return codes -730..-739 */ +static int test_MlDsaDisabledLevelRefused(void) +{ + WOLFSSH_CTX* ctx = NULL; + WS_KeySignature* sig = NULL; + MlDsaKey key; + WC_RNG rng; + byte* der = NULL; + int derSz = 0; + int privOnly; + int result = 0; + + if (wc_InitRng(&rng) != 0) + return -730; + WMEMSET(&key, 0, sizeof(key)); + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) { + wc_FreeRng(&rng); + return -730; + } + if (wc_MlDsaKey_SetParams(&key, WC_ML_DSA_87) != 0 + || wc_MlDsaKey_MakeKey(&key, &rng) != 0) + result = -731; + wc_FreeRng(&rng); + + if (result == 0) { + der = (byte*)WMALLOC(WC_MLDSA_87_BOTH_KEY_DER_SIZE, NULL, 0); + if (der == NULL) + result = -732; + } + + for (privOnly = 0; result == 0 && privOnly <= 1; privOnly++) { + if (privOnly) + derSz = wc_MlDsaKey_PrivateKeyToDer(&key, der, + WC_MLDSA_87_BOTH_KEY_DER_SIZE); + else + derSz = wc_MlDsaKey_KeyToDer(&key, der, + WC_MLDSA_87_BOTH_KEY_DER_SIZE); + if (derSz <= 0) + result = -733; + + /* Every IdentifyAsn1Key() caller, not just the host key load. */ + if (result == 0 && IdentifyAsn1Key(der, (word32)derSz, 1, NULL, + &sig) != WS_UNIMPLEMENTED_E) + result = -734; + if (sig != NULL) { + wolfSSH_KEY_clean(sig); + WFREE(sig, NULL, DYNTYPE_PRIVKEY); + sig = NULL; + if (result == 0) + result = -735; + } + + if (result == 0) { + byte* out = NULL; + word32 outSz = 0; + const byte* outType = NULL; + word32 outTypeSz = 0; + + if (wolfSSH_ReadKey_buffer(der, (word32)derSz, + WOLFSSH_FORMAT_ASN1, &out, &outSz, &outType, + &outTypeSz, NULL) != WS_UNIMPLEMENTED_E) + result = -736; + if (out != NULL) { + WMEMSET(out, 0, outSz); + WFREE(out, NULL, DYNTYPE_PRIVKEY); + if (result == 0) + result = -736; + } + } + + if (result == 0) { + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + result = -737; + } + if (result == 0 && wolfSSH_CTX_UsePrivateKey_buffer(ctx, der, + (word32)derSz, WOLFSSH_FORMAT_ASN1) != WS_UNIMPLEMENTED_E) + result = -738; + if (result == 0 && ctx->privateKeyCount != 0) + result = -739; + + if (ctx != NULL) { + wolfSSH_CTX_free(ctx); + ctx = NULL; + } + } + wc_MlDsaKey_Free(&key); + + if (der != NULL) { + WMEMSET(der, 0, WC_MLDSA_87_BOTH_KEY_DER_SIZE); + WFREE(der, NULL, 0); + } + + return result; +} +#endif /* disabled ML-DSA level test */ + + int wolfSSH_UnitTest(int argc, char** argv) { int testResult = 0, unitResult = 0; @@ -23520,6 +23879,27 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("IdentifyAsn1Key: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#if !defined(WOLFSSH_NO_MLDSA) && !defined(WOLFSSH_NO_MLDSA44) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ + defined(WOLFSSH_TEST_CAPTURING_ALLOCATOR) && \ + defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) + unitResult = test_MlDsaDeriveOutOfMemory(); + printf("MlDsaDeriveOutOfMemory: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + +#if !defined(WOLFSSH_NO_MLDSA) && \ + defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSL_NO_ML_DSA_87) && \ + defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) + unitResult = test_MlDsaDisabledLevelRefused(); + printf("MlDsaDisabledLevelRefused: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + unitResult = test_ReadPublicKeyAsn1(); printf("ReadPublicKeyAsn1: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; From 68f870c56415f1daa32f176d24b6d97aa1fcaa20 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 30 Sep 2026 15:16:56 -0600 Subject: [PATCH 3/4] mldsa: cache the host public key on the CTX slot --- src/internal.c | 154 ++++++++++++++++++++++++++++++++++++++++----- src/ssh.c | 3 + wolfssh/internal.h | 10 +++ 3 files changed, 152 insertions(+), 15 deletions(-) diff --git a/src/internal.c b/src/internal.c index 091dd940c..5583ef9c9 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1487,6 +1487,9 @@ void CtxResourceFree(WOLFSSH_CTX* ctx) ctx->privateKey[i].key = NULL; ctx->privateKey[i].keySz = 0; } + #ifndef WOLFSSH_NO_MLDSA + ClearMlDsaHostPubKey(&ctx->privateKey[i], ctx->heap); + #endif #ifdef WOLFSSH_CERTS if (ctx->privateKey[i].cert != NULL) { WFREE(ctx->privateKey[i].cert, ctx->heap, DYNTYPE_CERT); @@ -2001,6 +2004,16 @@ static int IsCompositeMlDsaId(byte id) #endif +/* Free a private-key IdentifyAsn1Key() result. NULL safe. */ +static void FreeKeySignature(WS_KeySignature* key, void* heap) +{ + if (key != NULL) { + wolfSSH_KEY_clean(key); + WFREE(key, heap, DYNTYPE_PRIVKEY); + } +} + + #ifndef WOLFSSH_NO_MLDSA /* Nonzero when keyId is an ML-DSA level wolfSSH was built without. */ static int MlDsaIdDisabled(byte keyId) @@ -2652,6 +2665,76 @@ WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) } +#ifndef WOLFSSH_NO_MLDSA +static INLINE int KeyIdToMlDsaLevel(byte id); + +/* Also used by CommitCertStoreSlot() in ssh.c. */ +WOLFSSH_LOCAL void ClearMlDsaHostPubKey(WOLFSSH_PVT_KEY* pvtKey, void* heap) +{ + if (pvtKey->mldsaPub != NULL) { + WFREE(pvtKey->mldsaPub, heap, DYNTYPE_PUBKEY); + pvtKey->mldsaPub = NULL; + pvtKey->mldsaPubSz = 0; + } +} + + +/* Export the raw public key of the key IdentifyAsn1Key() decoded, for the + * slot cache. Caller owns *pubOut. No-op for a non-ML-DSA keyId. */ +static int ExportMlDsaHostPubKey(byte keyId, WS_KeySignature* keySig, + void* heap, byte** pubOut, word32* pubOutSz) +{ + MlDsaKey* key; + int ret; + int pubLen = 0; + word32 pubSz; + byte* pub; + + if (KeyIdToMlDsaLevel(keyId) < 0) { + return WS_SUCCESS; + } + /* KEX needs the cache. Defensive: the public API always passes a + * matching one. */ + if (keySig == NULL || keySig->keyId != keyId) { + WLOG(WS_LOG_ERROR, "ExportMlDsaHostPubKey: ML-DSA decoded key " + "missing or does not match"); + return WS_BAD_ARGUMENT; + } + key = &keySig->ks.mldsa.key; + + ret = wc_MlDsaKey_GetPubLen(key, &pubLen); + if (ret != 0 || pubLen <= 0) { + WLOG(WS_LOG_ERROR, + "ExportMlDsaHostPubKey: ML-DSA public key length failed %d", + ret); + return WS_CRYPTO_FAILED; + } + pubSz = (word32)pubLen; + + pub = (byte*)WMALLOC(pubSz, heap, DYNTYPE_PUBKEY); + if (pub == NULL) { + return WS_MEMORY_E; + } + + ret = wc_MlDsaKey_ExportPubRaw(key, pub, &pubSz); + if (ret != 0) { + WLOG(WS_LOG_ERROR, + "ExportMlDsaHostPubKey: ML-DSA public key export failed %d", + ret); + WFREE(pub, heap, DYNTYPE_PUBKEY); + return WS_CRYPTO_FAILED; + } + + *pubOut = pub; + *pubOutSz = pubSz; + + return WS_SUCCESS; +} + + +#endif /* !WOLFSSH_NO_MLDSA */ + + #ifdef WOLFSSH_CERTS WOLFSSH_LOCAL byte CertTypeForId(byte id) @@ -2976,10 +3059,15 @@ static int CertStoreSlotConflict(const WOLFSSH_CTX* ctx, word32 destIdx, static int SetHostPrivateKey(WOLFSSH_CTX* ctx, - byte keyId, byte* der, word32 derSz, int dynamicType) + byte keyId, byte* der, word32 derSz, int dynamicType, + WS_KeySignature* keySig) { word32 destIdx = 0; int ret = WS_SUCCESS; +#ifndef WOLFSSH_NO_MLDSA + byte* mldsaPub = NULL; + word32 mldsaPubSz = 0; +#endif /* Look for the specified keyId. Add it if not present, * replace it if present. Call UpdateHostCertificate(). @@ -3020,6 +3108,13 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, WFREE(der, ctx->heap, dynamicType); ret = WS_UNIMPLEMENTED_E; } + /* Before the slot is touched, so a failure leaves the CTX as it was. */ + else if ((ret = ExportMlDsaHostPubKey(keyId, keySig, ctx->heap, + &mldsaPub, &mldsaPubSz)) != WS_SUCCESS) { + /* der not taken on this path; free it to avoid a leak */ + WS_FORCEZERO(der, derSz); + WFREE(der, ctx->heap, dynamicType); + } #endif else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; @@ -3043,6 +3138,12 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, pvtKey->isTpm = 0; #endif + #ifndef WOLFSSH_NO_MLDSA + ClearMlDsaHostPubKey(pvtKey, ctx->heap); + pvtKey->mldsaPub = mldsaPub; + pvtKey->mldsaPubSz = mldsaPubSz; + #endif + #ifdef WOLFSSH_CERTS if (ret == WS_SUCCESS) { ret = UpdateHostCertificates(ctx, destIdx, WOLFSSH_MAX_PVT_KEYS); @@ -3054,6 +3155,7 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, } WOLFSSH_UNUSED(dynamicType); + WOLFSSH_UNUSED(keySig); return ret; } @@ -3106,6 +3208,9 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) pvtKey->key = NULL; pvtKey->keySz = 0; pvtKey->isTpm = 1; + #ifndef WOLFSSH_NO_MLDSA + ClearMlDsaHostPubKey(pvtKey, ctx->heap); + #endif #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Defensive only: the else-if above already rejects a cert-store * slot, so this can only clear a slot in a state no writer @@ -3482,10 +3587,13 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, /* Maybe decrypt */ if (type == BUFTYPE_PRIVKEY) { + /* Decoded ML-DSA key for SetHostPrivateKey()'s cache; else NULL. */ + WS_KeySignature* keySig = NULL; + if (format == WOLFSSH_FORMAT_OPENSSH) ret = IdentifyOpenSshKey(der, derSz, ctx->heap); else - ret = IdentifyAsn1Key(der, derSz, 1, ctx->heap, NULL); + ret = IdentifyAsn1Key(der, derSz, 1, ctx->heap, &keySig); if (ret < 0) { if (der != NULL) { WS_FORCEZERO(der, derSz); @@ -3494,6 +3602,14 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, return ret; } keyId = (byte)ret; +#ifndef WOLFSSH_NO_MLDSA + /* Only the ML-DSA slot cache needs the decoded key. */ + if (KeyIdToMlDsaLevel(keyId) < 0) +#endif + { + FreeKeySignature(keySig, ctx->heap); + keySig = NULL; + } /* Only composite parsers can walk the stored openssh-key-v1 * envelope; reject other key types now instead of at handshake. */ if (format == WOLFSSH_FORMAT_OPENSSH @@ -3505,7 +3621,8 @@ int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, WFREE(der, heap, dynamicType); return WS_UNIMPLEMENTED_E; } - ret = SetHostPrivateKey(ctx, keyId, der, derSz, dynamicType); + ret = SetHostPrivateKey(ctx, keyId, der, derSz, dynamicType, keySig); + FreeKeySignature(keySig, ctx->heap); } #ifdef WOLFSSH_CERTS else if (type == BUFTYPE_CERT) { @@ -16247,18 +16364,25 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, &sigKeyBlock_ptr->sk.mldsa.key, ssh->ctx->privateKey[keyIdx].key, ssh->ctx->privateKey[keyIdx].keySz, &scratch); - #ifdef WOLFSSH_HAVE_MLDSA_DERIVE_PUB - /* The exporters below are pure accessors; a private-only host - * key needs its public half derived first. */ - if (ret == 0 && !sigKeyBlock_ptr->sk.mldsa.key.pubKeySet) - ret = wc_MlDsaKey_MakePublicKey( - &sigKeyBlock_ptr->sk.mldsa.key); - #endif - if (ret == 0) - ret = wc_MlDsaKey_ExportPubRaw( - &sigKeyBlock_ptr->sk.mldsa.key, - sigKeyBlock_ptr->sk.mldsa.q, - &sigKeyBlock_ptr->sk.mldsa.qSz); + /* Use the cached public key; deriving it is a keygen per KEX. + * x509v3 sends the certificate instead. A missing cache is + * defensive: the load refuses a key it cannot cache. */ + if (ret == 0 && !isCert) { + const WOLFSSH_PVT_KEY* pvtKey = + &ssh->ctx->privateKey[keyIdx]; + + if (pvtKey->mldsaPub != NULL && + pvtKey->mldsaPubSz <= sigKeyBlock_ptr->sk.mldsa.qSz) { + WMEMCPY(sigKeyBlock_ptr->sk.mldsa.q, pvtKey->mldsaPub, + pvtKey->mldsaPubSz); + sigKeyBlock_ptr->sk.mldsa.qSz = pvtKey->mldsaPubSz; + } + else { + WLOG(WS_LOG_ERROR, "SendKexGetSigningKey: ML-DSA host " + "key has no cached public key"); + ret = WS_INVALID_STATE_E; + } + } /* Hash in raw public key only for non-cert path. */ if (!isCert) { diff --git a/src/ssh.c b/src/ssh.c index 20c69fd14..32ee703bb 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -4099,6 +4099,9 @@ static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot) pvtKey->key = NULL; pvtKey->keySz = 0; } +#ifndef WOLFSSH_NO_MLDSA + ClearMlDsaHostPubKey(pvtKey, heap); +#endif if (pvtKey->cert != NULL) { WFREE(pvtKey->cert, heap, DYNTYPE_CERT); } diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 10c32efa0..0e09a06dd 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -875,6 +875,11 @@ typedef struct WOLFSSH_PVT_KEY { byte publicKeyFmt; /* Public key format for the private key. Note, some public key * formats are used with multiple public key signing algorithms. */ +#ifndef WOLFSSH_NO_MLDSA + byte* mldsaPub; + /* Raw ML-DSA public key, exported once at load time. Owned by CTX. */ + word32 mldsaPubSz; +#endif /* !WOLFSSH_NO_MLDSA */ #ifdef WOLFSSH_TPM byte isTpm; /* When set, the host key material lives in the TPM and key/keySz are @@ -889,6 +894,11 @@ typedef struct WOLFSSH_PVT_KEY { #endif /* WOLFSSH_WINDOWS_CERT_STORE */ } WOLFSSH_PVT_KEY; +#ifndef WOLFSSH_NO_MLDSA +/* Free a slot's cached ML-DSA public key. Every slot writer calls it. */ +WOLFSSH_LOCAL void ClearMlDsaHostPubKey(WOLFSSH_PVT_KEY* pvtKey, void* heap); +#endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Returns 1 when the value is exactly one assigned CERT_SYSTEM_STORE_* * location with no control flags set. Defined in certman.c. */ From 1739412f5004821e5a4c3a9f1e72a6c6fcf36102 Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 30 Sep 2026 15:16:56 -0600 Subject: [PATCH 4/4] tests: cover the ML-DSA host public key cache --- tests/auth.c | 206 +++++++++++++++++++++++++++++++++++++++++++-------- tests/auth.h | 2 + tests/unit.c | 161 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 334 insertions(+), 35 deletions(-) diff --git a/tests/auth.c b/tests/auth.c index 2d2174e4a..3b72f7664 100644 --- a/tests/auth.c +++ b/tests/auth.c @@ -39,7 +39,9 @@ #if (!defined(WOLFSSH_NO_ED25519) && defined(HAVE_ED25519) && \ defined(HAVE_ED25519_MAKE_KEY) && defined(HAVE_ED25519_KEY_EXPORT)) || \ (!defined(WOLFSSH_NO_ECDSA) && !defined(WOLFSSH_NO_RSA) && \ - defined(HAVE_ECC_KEY_EXPORT)) + defined(HAVE_ECC_KEY_EXPORT)) || \ + (!defined(WOLFSSH_NO_MLDSA) && defined(WOLFSSH_CERTS) && \ + defined(WOLFSSL_CERT_GEN)) #include #endif #ifdef NO_FILESYSTEM @@ -596,6 +598,15 @@ static THREAD_RETURN WOLFSSH_THREAD pubkey_server_thread(void* args) } } +#ifdef WOLFSSH_CERTS + if (serverArgs->hostCertBuf != NULL && + wolfSSH_CTX_UseCert_buffer(ctx, serverArgs->hostCertBuf, + serverArgs->hostCertBufSz, WOLFSSH_FORMAT_ASN1) < 0) { + serverArgs->return_code = WS_BAD_FILE_E; + goto cleanup; + } +#endif /* WOLFSSH_CERTS */ + clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &clientAddrSz); if (clientFd == WOLFSSH_SOCKET_INVALID) { serverArgs->return_code = WS_SOCKET_ERROR_E; @@ -638,10 +649,15 @@ static int AcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz, * WS_FATAL_ERROR for a reject test * hostKeyBuf - server host key DER; NULL uses the default fixture key * via load_key() (what every existing caller wants) - * hostKeyBufSz - size of hostKeyBuf; ignored when hostKeyBuf is NULL */ -static int run_pubkey_test_ex(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, + * hostKeyBufSz - size of hostKeyBuf; ignored when hostKeyBuf is NULL + * hostCertBuf - server host cert DER; NULL = none + * rootCertBuf - client root CA for hostCertBuf + * hostKeyAlgo - client host key algo list; NULL keeps the default */ +static int run_pubkey_test_host(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, int expect, const byte* hostKeyBuf, - word32 hostKeyBufSz) + word32 hostKeyBufSz, const byte* hostCertBuf, + word32 hostCertBufSz, const byte* rootCertBuf, + word32 rootCertBufSz, const char* hostKeyAlgo) { thread_args serverArgs; tcp_ready ready; @@ -662,6 +678,8 @@ static int run_pubkey_test_ex(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, serverArgs.caCertSz = sCtx->caCertSz; serverArgs.hostKeyBuf = hostKeyBuf; serverArgs.hostKeyBufSz = hostKeyBufSz; + serverArgs.hostCertBuf = hostCertBuf; + serverArgs.hostCertBufSz = hostCertBufSz; InitTcpReady(serverArgs.signal); ThreadStart(pubkey_server_thread, (void*)&serverArgs, &serThread); @@ -671,6 +689,17 @@ static int run_pubkey_test_ex(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, AssertNotNull(clientCtx); wolfSSH_CTX_SetPublicKeyCheck(clientCtx, AcceptAnyServerHostKey); wolfSSH_SetUserAuth(clientCtx, clientPubkeyUserAuth); +#ifdef WOLFSSH_CERTS + if (rootCertBuf != NULL) + AssertIntEQ(wolfSSH_CTX_AddRootCert_buffer(clientCtx, rootCertBuf, + rootCertBufSz, WOLFSSH_FORMAT_ASN1), WS_SUCCESS); +#else + (void)rootCertBuf; + (void)rootCertBufSz; +#endif + if (hostKeyAlgo != NULL) + AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(clientCtx, hostKeyAlgo), + WS_SUCCESS); clientSsh = wolfSSH_new(clientCtx); AssertNotNull(clientSsh); @@ -708,6 +737,14 @@ static int run_pubkey_test_ex(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, return WS_SUCCESS; } +static int run_pubkey_test_ex(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, + int expect, const byte* hostKeyBuf, + word32 hostKeyBufSz) +{ + return run_pubkey_test_host(sCtx, cCtx, expect, hostKeyBuf, hostKeyBufSz, + NULL, 0, NULL, 0, NULL); +} + /* Existing callers all want the default fixture host key, and every one of * them is an RSA or ECDSA test. */ #if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ECDSA) @@ -1470,6 +1507,37 @@ static void test_pubkey_load_mldsa_privonly_hostkey(void) } #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && !defined(WOLFSSH_NO_ECDSA) +/* Authorize hansel's ECC key on the server and have the client present + * it. cCtx points into pubBuf and privBuf. */ +static void load_hansel_ecc_client(PubkeyServerCtx* sCtx, + PubkeyClientCtx* cCtx, byte* pubBuf, word32 pubBufSz, + byte* privBuf, word32 privBufSz) +{ + const byte* pubKeyType = NULL; + word32 pubKeyTypeSz = 0; + const byte* privKeyType = NULL; + word32 privKeyTypeSz = 0; + + AssertIntEQ(wolfSSH_ReadKey_buffer((const byte*)hanselPublicEcc, + (word32)WSTRLEN(hanselPublicEcc), WOLFSSH_FORMAT_SSH, + &pubBuf, &pubBufSz, &pubKeyType, &pubKeyTypeSz, NULL), + WS_SUCCESS); + + AssertIntEQ(wc_Sha256Hash(pubBuf, pubBufSz, sCtx->hash), 0); + + AssertIntEQ(wolfSSH_ReadKey_buffer(hanselPrivateEcc, hanselPrivateEccSz, + WOLFSSH_FORMAT_ASN1, + &privBuf, &privBufSz, &privKeyType, &privKeyTypeSz, NULL), + WS_SUCCESS); + + cCtx->publicKeyType = pubKeyType; + cCtx->publicKeyTypeSz = pubKeyTypeSz; + cCtx->publicKey = pubBuf; + cCtx->publicKeySz = pubBufSz; + cCtx->privateKey = privBuf; + cCtx->privateKeySz = privBufSz; +} + /* ML-DSA counterpart to test_pubkey_auth_ed25519_privonly_hostkey: a real * handshake with a private-only host key, exercising the derived public * key all the way through SendKexGetSigningKey. */ @@ -1478,15 +1546,7 @@ static void test_pubkey_auth_mldsa_privonly_hostkey(void) PubkeyServerCtx sCtx = {0}; PubkeyClientCtx cCtx; byte pubKeyBuf[512]; - byte* p = pubKeyBuf; - word32 pubKeySz = sizeof(pubKeyBuf); - const byte* pubKeyType = NULL; - word32 pubKeyTypeSz = 0; byte privKeyBuf[1300]; - byte* privKeyPtr = privKeyBuf; - word32 privKeySz = sizeof(privKeyBuf); - const byte* privKeyType = NULL; - word32 privKeyTypeSz = 0; byte* hostKeyDer; int hostKeyDerSz; @@ -1494,23 +1554,8 @@ static void test_pubkey_auth_mldsa_privonly_hostkey(void) hostKeyDer = mldsa_privonly_hostkey_der(&hostKeyDerSz); - AssertIntEQ(wolfSSH_ReadKey_buffer((const byte*)hanselPublicEcc, - (word32)WSTRLEN(hanselPublicEcc), WOLFSSH_FORMAT_SSH, - &p, &pubKeySz, &pubKeyType, &pubKeyTypeSz, NULL), WS_SUCCESS); - - AssertIntEQ(wc_Sha256Hash(pubKeyBuf, pubKeySz, sCtx.hash), 0); - - AssertIntEQ(wolfSSH_ReadKey_buffer(hanselPrivateEcc, hanselPrivateEccSz, - WOLFSSH_FORMAT_ASN1, - &privKeyPtr, &privKeySz, &privKeyType, &privKeyTypeSz, NULL), - WS_SUCCESS); - - cCtx.publicKeyType = pubKeyType; - cCtx.publicKeyTypeSz = pubKeyTypeSz; - cCtx.publicKey = pubKeyBuf; - cCtx.publicKeySz = pubKeySz; - cCtx.privateKey = privKeyBuf; - cCtx.privateKeySz = privKeySz; + load_hansel_ecc_client(&sCtx, &cCtx, pubKeyBuf, sizeof(pubKeyBuf), + privKeyBuf, sizeof(privKeyBuf)); run_pubkey_test_ex(&sCtx, &cCtx, WS_SUCCESS, hostKeyDer, (word32)hostKeyDerSz); @@ -1521,6 +1566,104 @@ static void test_pubkey_auth_mldsa_privonly_hostkey(void) } #endif /* WOLFSSH_HAVE_MLDSA_DERIVE_PUB && !WOLFSSH_NO_ECDSA */ +/* A generated cert can't meet the FPKI profile the client enforces. */ +#if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && !defined(WOLFSSH_NO_ECDSA) && \ + defined(WOLFSSH_CERTS) && defined(WOLFSSL_CERT_GEN) && \ + defined(WOLFSSH_NO_FPKI) +/* Make an ML-DSA-44 cert for key, signed by signer (self-signed when + * issuerDer is NULL). Caller frees. */ +static byte* mldsa44_cert_der(MlDsaKey* key, MlDsaKey* signer, + const byte* issuerDer, int issuerDerSz, WC_RNG* rng, int* derSz) +{ + Cert cert; + byte* der; + int sz; + + der = (byte*)WMALLOC(16384, NULL, 0); + AssertNotNull(der); + wc_InitCert(&cert); + WSTRNCPY(cert.subject.commonName, issuerDer == NULL ? + "wolfSSH-mldsa-ca" : "wolfSSH-mldsa-host", CTC_NAME_SIZE - 1); + WSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE - 1); + cert.daysValid = 365; + cert.sigType = CTC_ML_DSA_44; + if (issuerDer == NULL) { + cert.selfSigned = 1; + cert.isCA = 1; + } + else { + AssertIntEQ(wc_SetIssuerBuffer(&cert, issuerDer, issuerDerSz), 0); + } + sz = wc_MakeCert_ex(&cert, der, 16384, ML_DSA_44_TYPE, key, rng); + AssertIntGT(sz, 0); + sz = wc_SignCert_ex(sz, CTC_ML_DSA_44, der, 16384, ML_DSA_44_TYPE, + signer, rng); + AssertIntGT(sz, 0); + + *derSz = sz; + return der; +} + +/* x509v3-ssh-mldsa-44 handshake with a private-only host key: the cert + * path signs without the cached public key and sends the certificate. */ +static void test_pubkey_auth_mldsa_privonly_hostcert(void) +{ + PubkeyServerCtx sCtx = {0}; + PubkeyClientCtx cCtx; + byte pubKeyBuf[512]; + byte privKeyBuf[1300]; + MlDsaKey caKey; + MlDsaKey hostKey; + WC_RNG rng; + byte* caDer; + int caDerSz; + byte* certDer; + int certDerSz; + byte* hostKeyDer; + int hostKeyDerSz; + + printf("Testing ML-DSA private-only host key with x509v3 cert\n"); + + AssertIntEQ(wc_InitRng(&rng), 0); + WMEMSET(&caKey, 0, sizeof(caKey)); + WMEMSET(&hostKey, 0, sizeof(hostKey)); + AssertIntEQ(wc_MlDsaKey_Init(&caKey, NULL, INVALID_DEVID), 0); + AssertIntEQ(wc_MlDsaKey_SetParams(&caKey, WC_ML_DSA_44), 0); + AssertIntEQ(wc_MlDsaKey_MakeKey(&caKey, &rng), 0); + AssertIntEQ(wc_MlDsaKey_Init(&hostKey, NULL, INVALID_DEVID), 0); + AssertIntEQ(wc_MlDsaKey_SetParams(&hostKey, WC_ML_DSA_44), 0); + AssertIntEQ(wc_MlDsaKey_MakeKey(&hostKey, &rng), 0); + + /* The client refuses a CA as the leaf, so issue the host cert. */ + caDer = mldsa44_cert_der(&caKey, &caKey, NULL, 0, &rng, &caDerSz); + certDer = mldsa44_cert_der(&hostKey, &caKey, caDer, caDerSz, &rng, + &certDerSz); + wc_MlDsaKey_Free(&caKey); + wc_FreeRng(&rng); + + hostKeyDer = (byte*)WMALLOC(WC_MLDSA_44_PRV_KEY_DER_SIZE, NULL, 0); + AssertNotNull(hostKeyDer); + hostKeyDerSz = wc_MlDsaKey_PrivateKeyToDer(&hostKey, hostKeyDer, + WC_MLDSA_44_PRV_KEY_DER_SIZE); + wc_MlDsaKey_Free(&hostKey); + AssertIntGT(hostKeyDerSz, 0); + + load_hansel_ecc_client(&sCtx, &cCtx, pubKeyBuf, sizeof(pubKeyBuf), + privKeyBuf, sizeof(privKeyBuf)); + + run_pubkey_test_host(&sCtx, &cCtx, WS_SUCCESS, hostKeyDer, + (word32)hostKeyDerSz, certDer, (word32)certDerSz, + caDer, (word32)caDerSz, "x509v3-ssh-mldsa-44"); + + WMEMSET(privKeyBuf, 0, sizeof(privKeyBuf)); + WMEMSET(hostKeyDer, 0, hostKeyDerSz); + WFREE(hostKeyDer, NULL, 0); + WFREE(certDer, NULL, 0); + WFREE(caDer, NULL, 0); +} +#endif /* WOLFSSH_HAVE_MLDSA_DERIVE_PUB && !WOLFSSH_NO_ECDSA && + * WOLFSSH_CERTS && WOLFSSL_CERT_GEN && WOLFSSH_NO_FPKI */ + #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && \ defined(WOLFSSL_MLDSA_PUBLIC_KEY) && \ (!defined(WOLFSSH_NO_ECDSA) || !defined(WOLFSSH_NO_RSA)) @@ -2650,6 +2793,11 @@ int wolfSSH_AuthTest(int argc, char** argv) #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && !defined(WOLFSSH_NO_ECDSA) test_pubkey_auth_mldsa_privonly_hostkey(); #endif + #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && \ + !defined(WOLFSSH_NO_ECDSA) && defined(WOLFSSH_CERTS) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSH_NO_FPKI) + test_pubkey_auth_mldsa_privonly_hostcert(); + #endif #if defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) && \ defined(WOLFSSL_MLDSA_PUBLIC_KEY) && \ (!defined(WOLFSSH_NO_ECDSA) || !defined(WOLFSSH_NO_RSA)) diff --git a/tests/auth.h b/tests/auth.h index 32805f1ab..a994a6f56 100644 --- a/tests/auth.h +++ b/tests/auth.h @@ -34,6 +34,8 @@ typedef struct thread_args { word32 caCertSz; const byte* hostKeyBuf; /* server host key; NULL = use load_key() */ word32 hostKeyBufSz; + const byte* hostCertBuf; /* server host cert DER; NULL = none */ + word32 hostCertBufSz; } thread_args; #endif /* _WOLFSSH_TESTS_AUTH_H_ */ diff --git a/tests/unit.c b/tests/unit.c index c8df28487..070d4495e 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -22840,9 +22840,7 @@ static int test_ShutdownPeerChannelId(void) #if defined(WOLFSSH_TEST_INTERNAL) && \ !defined(WOLFSSH_NO_MLDSA) && !defined(WOLFSSH_NO_MLDSA44) && \ defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ - !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ - defined(WOLFSSH_TEST_CAPTURING_ALLOCATOR) && \ - defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) /* Make an ML-DSA-44 DER (private-only when the load can derive) and its * public key. Caller frees *derOut. */ static int mldsa44_der_and_pub(byte** derOut, word32* derSzOut, @@ -22900,6 +22898,45 @@ static int mldsa44_der_and_pub(byte** derOut, word32* derSzOut, } +/* Returns keyId's slot, or NULL when it has none. */ +static const WOLFSSH_PVT_KEY* FindMlDsaSlot(WOLFSSH_CTX* ctx, byte keyId) +{ + word32 slot; + + for (slot = 0; slot < ctx->privateKeyCount; slot++) { + if (ctx->privateKey[slot].publicKeyFmt == keyId) + return &ctx->privateKey[slot]; + } + return NULL; +} + + +#ifdef WOLFSSH_TEST_CAPTURING_ALLOCATOR +/* Nonzero when keyId's slot holds exactly the DER der. */ +static int MlDsaKeyHeld(WOLFSSH_CTX* ctx, byte keyId, + const byte* der, word32 derSz) +{ + const WOLFSSH_PVT_KEY* pvtKey = FindMlDsaSlot(ctx, keyId); + + return pvtKey != NULL && pvtKey->key != NULL && pvtKey->keySz == derSz + && WMEMCMP(pvtKey->key, der, derSz) == 0; +} +#endif + + +/* Nonzero when keyId's cached public key is exactly pub. */ +static int MlDsaPubCached(WOLFSSH_CTX* ctx, byte keyId, + const byte* pub, word32 pubSz) +{ + const WOLFSSH_PVT_KEY* pvtKey = FindMlDsaSlot(ctx, keyId); + + return pvtKey != NULL && pvtKey->mldsaPub != NULL + && pvtKey->mldsaPubSz == pubSz + && WMEMCMP(pvtKey->mldsaPub, pub, pubSz) == 0; +} + + +#ifdef WOLFSSH_TEST_CAPTURING_ALLOCATOR /* Counts allocations of exactly failAllocSz bytes (any size when 0) and * fails the failAllocNth one (1-based); 0 only counts. */ static size_t failAllocSz = 0; @@ -22914,8 +22951,113 @@ static void* FailSizeMalloc(size_t size) } return malloc(size); } +#endif + + +/* The ML-DSA slot cache is filled at load, replaced on reload, and left + * as it was when a reload's export fails. + * Return codes -710..-721 */ +static int test_MlDsaHostPubKeyCache(void) +{ + WOLFSSH_CTX* ctx = NULL; + byte* derA = NULL; + byte* derB = NULL; + word32 derASz = 0, derBSz = 0; + byte pubA[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; + byte pubB[WOLFSSH_MLDSA_MAX_PUB_KEY_SZ]; + word32 pubASz = sizeof(pubA), pubBSz = sizeof(pubB); + int result = 0; + + if (mldsa44_der_and_pub(&derA, &derASz, pubA, &pubASz) != 0) + return -710; + if (mldsa44_der_and_pub(&derB, &derBSz, pubB, &pubBSz) != 0) + result = -711; + /* The keys must differ for the reload check to mean anything. */ + if (result == 0 && pubASz == pubBSz + && WMEMCMP(pubA, pubB, pubASz) == 0) + result = -712; + if (result == 0) { + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + result = -713; + } + /* The load caches the public key on the slot. */ + if (result == 0 && wolfSSH_CTX_UsePrivateKey_buffer(ctx, derA, derASz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -714; + if (result == 0 && !MlDsaPubCached(ctx, ID_MLDSA44, pubA, pubASz)) + result = -715; + + /* A reload replaces the cache. */ + if (result == 0 && wolfSSH_CTX_UsePrivateKey_buffer(ctx, derB, derBSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -716; + if (result == 0 && !MlDsaPubCached(ctx, ID_MLDSA44, pubB, pubBSz)) + result = -717; + +#ifdef WOLFSSH_TEST_CAPTURING_ALLOCATOR + /* Fail the export's allocation on a reload of key A; key B stays. + * Same-size allocations in IdentifyAsn1Key() come first; skip them. */ + if (result == 0) { + wolfSSL_Malloc_cb prevMf = NULL; + wolfSSL_Free_cb prevFf = NULL; + wolfSSL_Realloc_cb prevRf = NULL; + word32 countBefore = ctx->privateKeyCount; + WS_KeySignature* sig = NULL; + int decodeHits; + int loadRet; + + wolfSSL_GetAllocators(&prevMf, &prevFf, &prevRf); + failAllocSz = pubASz; + failAllocNth = 0; + failAllocHits = 0; + if (wolfSSL_SetAllocators(FailSizeMalloc, prevFf, prevRf) != 0) { + result = -718; + } + else { + loadRet = IdentifyAsn1Key(derA, derASz, 1, NULL, &sig); + decodeHits = failAllocHits; + if (sig != NULL) { + wolfSSH_KEY_clean(sig); + WFREE(sig, NULL, DYNTYPE_PRIVKEY); + } + failAllocNth = decodeHits + 1; + failAllocHits = 0; + if (loadRet != ID_MLDSA44) { + result = -719; + } + else { + loadRet = wolfSSH_CTX_UsePrivateKey_buffer(ctx, derA, + derASz, WOLFSSH_FORMAT_ASN1); + if (loadRet != WS_MEMORY_E || failAllocHits != failAllocNth) + result = -720; + else if (ctx->privateKeyCount != countBefore + || !MlDsaPubCached(ctx, ID_MLDSA44, pubB, pubBSz) + || !MlDsaKeyHeld(ctx, ID_MLDSA44, derB, derBSz)) + result = -721; + } + wolfSSL_SetAllocators(prevMf, prevFf, prevRf); + } + } +#endif + + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + WMEMSET(derA, 0, derASz); + WFREE(derA, NULL, 0); + if (derB != NULL) { + WMEMSET(derB, 0, derBSz); + WFREE(derB, NULL, 0); + } + + return result; +} + + +#if defined(WOLFSSH_TEST_CAPTURING_ALLOCATOR) && \ + defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) /* An out-of-memory derivation of a private-only key is WS_MEMORY_E, not * WS_CRYPTO_FAILED. Fails each allocation in turn: only the derivation's * can yield WS_MEMORY_E after the first (the WS_KeySignature itself). @@ -22970,7 +23112,9 @@ static int test_MlDsaDeriveOutOfMemory(void) return result; } -#endif /* ML-DSA-44 derive out-of-memory test */ +#endif + +#endif /* ML-DSA-44 public key cache test */ #if defined(WOLFSSH_TEST_INTERNAL) && !defined(WOLFSSH_NO_MLDSA) && \ @@ -23881,14 +24025,19 @@ int wolfSSH_UnitTest(int argc, char** argv) #if !defined(WOLFSSH_NO_MLDSA) && !defined(WOLFSSH_NO_MLDSA44) && \ defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) && \ - !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \ - defined(WOLFSSH_TEST_CAPTURING_ALLOCATOR) && \ + !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) + unitResult = test_MlDsaHostPubKeyCache(); + printf("MlDsaHostPubKeyCache: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#if defined(WOLFSSH_TEST_CAPTURING_ALLOCATOR) && \ defined(WOLFSSH_HAVE_MLDSA_DERIVE_PUB) unitResult = test_MlDsaDeriveOutOfMemory(); printf("MlDsaDeriveOutOfMemory: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; #endif +#endif #if !defined(WOLFSSH_NO_MLDSA) && \ defined(WOLFSSH_NO_MLDSA87) && !defined(WOLFSSL_NO_ML_DSA_87) && \