diff --git a/src/internal.c b/src/internal.c index ee46f6f5d..cda3967d2 100644 --- a/src/internal.c +++ b/src/internal.c @@ -7398,6 +7398,54 @@ struct wolfSSH_sigKeyBlock { static int InitPubKey(struct wolfSSH_sigKeyBlock *p, WOLFSSH *ssh); +#ifndef WOLFSSH_NO_RSA +/* Utility for the RSA host key and user auth paths. */ +/* returns WS_SUCCESS when the key clears WOLFSSH_RSA_MIN_KEY_BITS. */ +static int CheckRsaKeyBits(RsaKey* key) +{ + int ret = WS_SUCCESS; + int keyBits; + + /* The encrypt size rounds up to a byte; offload builds leave n empty. */ + keyBits = mp_count_bits(&key->n); + if (keyBits == 0) { + keyBits = wc_RsaEncryptSize(key) * 8; + } + + if (keyBits < WOLFSSH_RSA_MIN_KEY_BITS) { + WLOG(WS_LOG_DEBUG, "RSA key too small (%d bits)", keyBits); + ret = WS_CERT_KEY_SIZE_E; + } + + return ret; +} +#endif + + +#if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ED25519) \ + || !defined(WOLFSSH_NO_MLDSA) +/* Compare the format identifier at the start of a public key blob + * against the expected name (RFC 4253 section 6.6). */ +static int GetPubKeyFormat(const char* expected, const byte* pubKey, + word32 pubKeySz, word32* pubKeyIdx) +{ + const byte* name = NULL; + word32 nameSz = 0; + int ret; + + ret = GetStringRef(&nameSz, &name, pubKey, pubKeySz, pubKeyIdx); + if (ret == WS_SUCCESS) { + if (nameSz != (word32)WSTRLEN(expected) + || WMEMCMP(name, expected, nameSz) != 0) { + ret = WS_INVALID_ALGO_ID; + } + } + + return ret; +} +#endif + + /* Parse out a RAW RSA public key from buffer */ static int ParseRSAPubKey(WOLFSSH *ssh, struct wolfSSH_sigKeyBlock *sigKeyBlock_ptr, byte *pubKey, word32 pubKeySz) @@ -7411,9 +7459,9 @@ static int ParseRSAPubKey(WOLFSSH *ssh, word32 pubKeyIdx = 0; ret = InitPubKey(sigKeyBlock_ptr, ssh); - /* Skip the algo name. */ + /* All three RSA host key algorithms use the "ssh-rsa" format. */ if (ret == WS_SUCCESS) - ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx); + ret = GetPubKeyFormat("ssh-rsa", pubKey, pubKeySz, &pubKeyIdx); if (ret == WS_SUCCESS) { ret = GetUint32(&eSz, pubKey, pubKeySz, &pubKeyIdx); if (ret == WS_SUCCESS && eSz > pubKeySz - pubKeyIdx) @@ -7430,13 +7478,15 @@ static int ParseRSAPubKey(WOLFSSH *ssh, n = pubKey + pubKeyIdx; ret = wc_RsaPublicKeyDecodeRaw(n, nSz, e, eSz, &sigKeyBlock_ptr->sk.rsa.key); + if (ret != 0) + ret = WS_RSA_E; } + if (ret == WS_SUCCESS) + ret = CheckRsaKeyBits(&sigKeyBlock_ptr->sk.rsa.key); - if (ret == 0) { + if (ret == WS_SUCCESS) { sigKeyBlock_ptr->keySz = (word32)sizeof(sigKeyBlock_ptr->sk.rsa.key); } - else - ret = WS_RSA_E; #else WOLFSSH_UNUSED(ssh); WOLFSSH_UNUSED(sigKeyBlock_ptr); @@ -7552,9 +7602,8 @@ static int ParseEd25519PubKey(WOLFSSH *ssh, ret = InitPubKey(sigKeyBlock_ptr, ssh); - /* Skip the algo name */ if (ret == WS_SUCCESS) { - ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx); + ret = GetPubKeyFormat("ssh-ed25519", pubKey, pubKeySz, &pubKeyIdx); } if (ret == WS_SUCCESS) { @@ -7636,8 +7685,7 @@ static int ParseMlDsaPubKey(WOLFSSH* ssh, return WS_INVALID_ALGO_ID; } - /* skip the algo name string */ - ret = GetSkip(pubKey, pubKeySz, &pubKeyIdx); + ret = GetPubKeyFormat(IdToName(keyId), pubKey, pubKeySz, &pubKeyIdx); if (ret == WS_SUCCESS) ret = GetStringRef(&pubSz, &pub, pubKey, pubKeySz, &pubKeyIdx); if (ret == WS_SUCCESS) @@ -7905,6 +7953,8 @@ static int ParseRSAPubKeyCert(WOLFSSH *ssh, if (error == 0) error = wc_RsaPublicKeyDecode(der, &idx, &sigKeyBlock_ptr->sk.rsa.key, derSz); + if (error == 0) + error = CheckRsaKeyBits(&sigKeyBlock_ptr->sk.rsa.key); if (error == 0) { sigKeyBlock_ptr->keySz = (word32)sizeof(sigKeyBlock_ptr->sk.rsa.key); @@ -8163,8 +8213,12 @@ static int KeyAgreeDh_client(WOLFSSH* ssh, byte hashId, /* Reject a peer public value outside the safe range [2, p-2] before key * agreement. Keeps wolfSSH independent of whether the linked wolfSSL - * validates the peer key inside wc_DhAgree. */ - ret = wc_DhCheckPubKey(&ssh->handshake->privKey.dh, f, fSz); + * validates the peer key inside wc_DhAgree. f is an mpint, so a set + * top bit makes it negative (RFC 4251 section 5). */ + if (fSz > 0 && (f[0] & 0x80) != 0) + ret = WS_PARSE_E; + else + ret = wc_DhCheckPubKey(&ssh->handshake->privKey.dh, f, fSz); if (ret != 0) { /* Peer-driven reject; log at DEBUG like other peer-input rejections * so a remote peer cannot flood the error log. */ @@ -10208,28 +10262,6 @@ static int DoUserAuthRequestPassword(WOLFSSH* ssh, WS_UserAuthData* authData, } #ifndef WOLFSSH_NO_RSA -/* Utility for the RSA user auth paths. */ -/* returns WS_SUCCESS when the key clears WOLFSSH_RSA_MIN_KEY_BITS. */ -static int CheckRsaKeyBits(RsaKey* key) -{ - int ret = WS_SUCCESS; - int keyBits; - - /* The encrypt size rounds up to a byte; offload builds leave n empty. */ - keyBits = mp_count_bits(&key->n); - if (keyBits == 0) { - keyBits = wc_RsaEncryptSize(key) * 8; - } - - if (keyBits < WOLFSSH_RSA_MIN_KEY_BITS) { - WLOG(WS_LOG_DEBUG, "RSA auth key too small (%d bits)", keyBits); - ret = WS_CERT_KEY_SIZE_E; - } - - return ret; -} - - /* Utility for DoUserAuthRequestPublicKey() */ /* returns negative for error, positive is size of digest. */ static int DoUserAuthRequestRsa(WOLFSSH* ssh, WS_UserAuthData_PublicKey* pk, @@ -17060,10 +17092,24 @@ static int KeyAgreeDh_server(WOLFSSH* ssh, byte hashId, byte* f, word32* fSz) PRIVATE_KEY_LOCK(); } /* Reject a peer public value outside the safe range [2, p-2] before - * key agreement, independent of the linked wolfSSL's own check. */ - if (ret == 0) - ret = wc_DhCheckPubKey(privKey, ssh->handshake->e, - ssh->handshake->eSz); + * key agreement, independent of the linked wolfSSL's own check. + * e is an mpint, so a set top bit makes it negative (RFC 4251 + * section 5). */ + if (ret == 0) { + if (ssh->handshake->eSz > 0 + && (ssh->handshake->e[0] & 0x80) != 0) + ret = WS_PARSE_E; + else + ret = wc_DhCheckPubKey(privKey, ssh->handshake->e, + ssh->handshake->eSz); + if (ret != 0) { + /* Peer-driven reject; log at DEBUG like other peer-input + * rejections so a remote peer cannot flood the error log. */ + WLOG(WS_LOG_DEBUG, + "Peer DH public key out of range, %d", ret); + ret = WS_CRYPTO_FAILED; + } + } if (ret == 0) { PRIVATE_KEY_UNLOCK(); ret = wc_DhAgree(privKey, ssh->k, &ssh->kSz, y_ptr, ySz, @@ -26529,6 +26575,20 @@ int wolfSSH_TestDoUserAuthRequestEd25519(WOLFSSH* ssh, #ifndef WOLFSSH_NO_MLDSA +int wolfSSH_TestParseMlDsaPubKey(WOLFSSH* ssh, byte* pubKey, word32 pubKeySz, + byte keyId) +{ + struct wolfSSH_sigKeyBlock sigKeyBlock; + int ret; + + WMEMSET(&sigKeyBlock, 0, sizeof(sigKeyBlock)); + sigKeyBlock.useMlDsa = 1; + ret = ParseMlDsaPubKey(ssh, &sigKeyBlock, pubKey, pubKeySz, keyId); + FreePubKey(&sigKeyBlock); + + return ret; +} + WOLFSSH_API int wolfSSH_TestDoUserAuthRequestMlDsa(WOLFSSH* ssh, WS_UserAuthData* authData, word32 pubKeyBlobSz) { diff --git a/tests/unit.c b/tests/unit.c index 84779ac03..721230b6b 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -3537,9 +3537,13 @@ static int test_DhGexGroup16KeyAgree(void) goto out; } - /* Hand the client's public value to the server and run the agreement. */ - WMEMCPY(ssh->handshake->e, cPub, cPubSz); - ssh->handshake->eSz = cPubSz; + /* Hand the client's public value to the server as the mpint + * SendKexDhInit() would send, and run the agreement. */ + ssh->handshake->eSz = 0; + if (cPub[0] & 0x80) + ssh->handshake->e[ssh->handshake->eSz++] = 0; + WMEMCPY(ssh->handshake->e + ssh->handshake->eSz, cPub, cPubSz); + ssh->handshake->eSz += cPubSz; ssh->kSz = MAX_KEX_KEY_SZ; ret = wolfSSH_TestKeyAgreeDh_server(ssh, WC_HASH_TYPE_SHA256, f, &fSz); @@ -14898,6 +14902,15 @@ static int test_ParseEd25519PubKey(void) failures++; } + /* format identifier other than "ssh-ed25519" */ + blob[UINT32_SZ + keyTypeNameSz - 1] = '8'; + ret = wolfSSH_TestParseEd25519PubKey(ssh, blob, blobSz); + if (ret != WS_INVALID_ALGO_ID) { + fprintf(stderr, "\t\"format mismatch\" FAIL: got %d, expected %d\n", + ret, WS_INVALID_ALGO_ID); + failures++; + } + wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); @@ -15931,6 +15944,99 @@ static int test_DoUserAuthRequestMlDsa(void) return 0; } +#ifndef WOLFSSH_NO_MLDSA44 +/* Host key blob parse for ssh-mldsa-44. The format identifier must match + * the negotiated algorithm (RFC 4253 section 6.6). */ +static int test_ParseMlDsaPubKey(void) +{ + static const char keyTypeName[] = "ssh-mldsa-44"; + const word32 keyTypeNameSz = (word32)(sizeof(keyTypeName) - 1); + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + MlDsaKey key; + int keyInit = 0; + WC_RNG rng; + int rngInit = 0; + byte blob[UINT32_SZ * 2 + sizeof(keyTypeName) - 1 + + WC_MLDSA_44_PUB_KEY_SIZE]; + word32 pubSz = WC_MLDSA_44_PUB_KEY_SIZE; + word32 blobSz; + int ret; + int result = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) + return -780; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { + result = -781; + goto done; + } + + if (wc_InitRng(&rng) != 0) { + result = -782; + goto done; + } + rngInit = 1; + if (wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID) != 0) { + result = -783; + goto done; + } + keyInit = 1; + if (wc_MlDsaKey_SetParams(&key, WC_ML_DSA_44) != 0 + || wc_MlDsaKey_MakeKey(&key, &rng) != 0) { + result = -784; + goto done; + } + + /* string "ssh-mldsa-44" || string pubkey */ + PutU32BE(blob, keyTypeNameSz); + WMEMCPY(blob + UINT32_SZ, keyTypeName, keyTypeNameSz); + if (wc_MlDsaKey_ExportPubRaw(&key, + blob + UINT32_SZ * 2 + keyTypeNameSz, &pubSz) != 0) { + result = -785; + goto done; + } + PutU32BE(blob + UINT32_SZ + keyTypeNameSz, pubSz); + blobSz = UINT32_SZ * 2 + keyTypeNameSz + pubSz; + + ret = wolfSSH_TestParseMlDsaPubKey(ssh, blob, blobSz, ID_MLDSA44); + if (ret != WS_SUCCESS) { + fprintf(stderr, "\t\"valid\" FAIL: got %d, expected %d\n", + ret, WS_SUCCESS); + result = -786; + goto done; + } + + ret = wolfSSH_TestParseMlDsaPubKey(ssh, blob, blobSz - 4, ID_MLDSA44); + if (ret == WS_SUCCESS) { + fprintf(stderr, "\t\"truncated\" FAIL: got %d\n", ret); + result = -787; + goto done; + } + + /* format identifier other than "ssh-mldsa-44" */ + blob[UINT32_SZ + keyTypeNameSz - 1] = '5'; + ret = wolfSSH_TestParseMlDsaPubKey(ssh, blob, blobSz, ID_MLDSA44); + if (ret != WS_INVALID_ALGO_ID) { + fprintf(stderr, "\t\"format mismatch\" FAIL: got %d, expected %d\n", + ret, WS_INVALID_ALGO_ID); + result = -788; + goto done; + } + +done: + if (keyInit) + wc_MlDsaKey_Free(&key); + if (rngInit) + wc_FreeRng(&rng); + if (ssh != NULL) + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} +#endif /* !WOLFSSH_NO_MLDSA44 */ + /* tamperSig: 0 = no tampering, 1 = flip a byte in the trad-algorithm * signature component, 2 = flip a byte in the ML-DSA signature component, * 3 = claim a composite signature length shorter than the ML-DSA half @@ -20437,7 +20543,7 @@ static int test_KeyAgreeDh_server_rejectsOutOfRangePeer(void) hs->eSz = 1; fSz = (word32)sizeof(f); ret = wolfSSH_TestKeyAgreeDh_server(ssh, WC_HASH_TYPE_SHA256, f, &fSz); - if (ret == WS_SUCCESS) { + if (ret != WS_CRYPTO_FAILED) { result = -743; break; } @@ -20509,6 +20615,111 @@ static int test_KeyAgreeDh_client_rejectsOutOfRangePeer(void) return result; } #endif /* HAVE_FFDHE_2048 */ + +/* Verify both DH key agreement sides reject a peer public value encoded + * as a negative mpint (RFC 4251 section 5). 0x80 is 128 read unsigned, + * inside [2, p-2], so only the sign check can reject it; 0x00 0x80 is + * the same value encoded correctly and must be accepted. */ +static int test_KeyAgreeDh_rejectsNegativePeer(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + HandshakeInfo* hs = NULL; + byte f[MAX_KEX_KEY_SZ]; + word32 fSz; + int ret; + int result = 0; + static const byte negPeer[1] = { 0x80 }; + static const byte posPeer[2] = { 0x00, 0x80 }; + byte kexId; + + /* server: the peer value is the client's e */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -760; + ssh = wolfSSH_new(ctx); + if (ssh == NULL || ssh->handshake == NULL) { + result = -761; + goto cleanup; + } +#ifndef WOLFSSH_NO_DH_GROUP14_SHA256 + kexId = ID_DH_GROUP14_SHA256; +#elif !defined(WOLFSSH_NO_DH_GROUP16_SHA512) + kexId = ID_DH_GROUP16_SHA512; +#elif !defined(WOLFSSH_NO_DH_GROUP14_SHA1) + kexId = ID_DH_GROUP14_SHA1; +#elif !defined(WOLFSSH_NO_DH_GROUP1_SHA1) + kexId = ID_DH_GROUP1_SHA1; +#else + /* No fixed DH group enabled; fail rather than silently pass. */ + result = -762; + goto cleanup; +#endif + hs = ssh->handshake; + hs->kexId = kexId; + + WMEMCPY(hs->e, negPeer, sizeof(negPeer)); + hs->eSz = (word32)sizeof(negPeer); + fSz = (word32)sizeof(f); + ret = wolfSSH_TestKeyAgreeDh_server(ssh, WC_HASH_TYPE_SHA256, f, &fSz); + if (ret != WS_CRYPTO_FAILED) { + result = -763; + goto cleanup; + } + + WMEMCPY(hs->e, posPeer, sizeof(posPeer)); + hs->eSz = (word32)sizeof(posPeer); + fSz = (word32)sizeof(f); + ret = wolfSSH_TestKeyAgreeDh_server(ssh, WC_HASH_TYPE_SHA256, f, &fSz); + if (ret != WS_SUCCESS) { + result = -764; + goto cleanup; + } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + + /* client: the peer value is the server's f */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) + return -765; + ssh = wolfSSH_new(ctx); + if (ssh == NULL || ssh->handshake == NULL) { + result = -766; + goto cleanup; + } + ssh->handshake->kexId = kexId; + + /* The hook frees the DH key on return, so set it up for each call. */ + if (wolfSSH_TestSetDhKexKey(ssh) != 0) { + result = -767; + goto cleanup; + } + ret = wolfSSH_TestKeyAgreeDh_client(ssh, WC_HASH_TYPE_SHA256, + negPeer, (word32)sizeof(negPeer)); + if (ret != WS_CRYPTO_FAILED) { + result = -768; + goto cleanup; + } + + if (wolfSSH_TestSetDhKexKey(ssh) != 0) { + result = -769; + goto cleanup; + } + ret = wolfSSH_TestKeyAgreeDh_client(ssh, WC_HASH_TYPE_SHA256, + posPeer, (word32)sizeof(posPeer)); + if (ret != WS_SUCCESS) { + result = -770; + goto cleanup; + } + +cleanup: + if (ssh != NULL) + wolfSSH_free(ssh); + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + return result; +} #endif /* !WOLFSSH_NO_DH */ #if !defined(WOLFSSH_NO_ECDH) && !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) @@ -22578,6 +22789,7 @@ static const byte userAuthRsaSigBlob[] = { static const byte userAuthRsaSigLenOverrun[] = { 0x00, 0x00, 0x01, 0x01 }; /* Offsets of interest in userAuthRsaPubKeyBlob. */ +#define RSA_PUB_BLOB_ALGO 4 /* "ssh-rsa" */ #define RSA_PUB_BLOB_NLEN 18 /* length of the modulus mpint */ #define RSA_PUB_BLOB_N 23 /* first byte of the modulus, past the pad */ @@ -22718,9 +22930,39 @@ static int test_ParseRSAPubKey(void) /* truncated blob: fails after the key is initialized */ ret = wolfSSH_TestParseRSAPubKey(ssh, blob, (word32)(sizeof(blob) / 2)); - if (ret != WS_RSA_E) { + if (ret != WS_BUFFER_E) { fprintf(stderr, "\t\"truncated\" FAIL: got %d, expected %d\n", - ret, WS_RSA_E); + ret, WS_BUFFER_E); + failures++; + } + + /* host key under WOLFSSH_RSA_MIN_KEY_BITS */ + WMEMCPY(blob + RSA_PUB_BLOB_NLEN, userAuthRsaPubKeyNLen1024, + sizeof(userAuthRsaPubKeyNLen1024)); + ret = wolfSSH_TestParseRSAPubKey(ssh, blob, (word32)sizeof(blob)); + if (ret != WS_CERT_KEY_SIZE_E) { + fprintf(stderr, "\t\"1024 bit\" FAIL: got %d, expected %d\n", + ret, WS_CERT_KEY_SIZE_E); + failures++; + } + + WMEMCPY(blob, userAuthRsaPubKeyBlob, sizeof(blob)); + WMEMCPY(blob + RSA_PUB_BLOB_N, userAuthRsaPubKeyMsb2047, + sizeof(userAuthRsaPubKeyMsb2047)); + ret = wolfSSH_TestParseRSAPubKey(ssh, blob, (word32)sizeof(blob)); + if (ret != WS_CERT_KEY_SIZE_E) { + fprintf(stderr, "\t\"2047 bit\" FAIL: got %d, expected %d\n", + ret, WS_CERT_KEY_SIZE_E); + failures++; + } + + /* format identifier other than "ssh-rsa" */ + WMEMCPY(blob, userAuthRsaPubKeyBlob, sizeof(blob)); + WMEMCPY(blob + RSA_PUB_BLOB_ALGO, "ssh-dss", 7); + ret = wolfSSH_TestParseRSAPubKey(ssh, blob, (word32)sizeof(blob)); + if (ret != WS_INVALID_ALGO_ID) { + fprintf(stderr, "\t\"format mismatch\" FAIL: got %d, expected %d\n", + ret, WS_INVALID_ALGO_ID); failures++; } @@ -25090,6 +25332,12 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("DoUserAuthRequestMlDsa: %s (result=%d)\n", (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); testResult = testResult || (unitResult != 0); +#ifndef WOLFSSH_NO_MLDSA44 + unitResult = test_ParseMlDsaPubKey(); + printf("ParseMlDsaPubKey: %s (result=%d)\n", + (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); + testResult = testResult || (unitResult != 0); +#endif #ifndef WOLFSSH_NO_MLDSA_COMPOSITES unitResult = test_DoUserAuthRequestMlDsaComposite(); printf("DoUserAuthRequestMlDsaComposite: %s (result=%d)\n", @@ -25365,6 +25613,11 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; #endif + + unitResult = test_KeyAgreeDh_rejectsNegativePeer(); + printf("KeyAgreeDh_rejectsNegativePeer: %s (result=%d)\n", + (unitResult == 0 ? "SUCCESS" : "FAILED"), unitResult); + testResult = testResult || unitResult; #endif /* !WOLFSSH_NO_DH */ #if !defined(WOLFSSH_NO_ECDH) && !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 29955cd7e..fcb08b619 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -706,7 +706,8 @@ enum NameIdType { #endif #ifndef WOLFSSH_RSA_MIN_KEY_BITS - /* Minimum accepted RSA public-key size (bits) for user authentication. + /* Minimum accepted RSA public-key size (bits) for host keys and user + * authentication. * Per NIST SP 800-131A; override at build time if a smaller key must be * accepted. */ #define WOLFSSH_RSA_MIN_KEY_BITS 2048 @@ -2230,6 +2231,8 @@ enum WS_MessageIdLimits { WS_UserAuthData* authData); #endif /* !WOLFSSH_NO_ED25519 */ #ifndef WOLFSSH_NO_MLDSA + WOLFSSH_API int wolfSSH_TestParseMlDsaPubKey(WOLFSSH* ssh, byte* pubKey, + word32 pubKeySz, byte keyId); WOLFSSH_API int wolfSSH_TestDoUserAuthRequestMlDsa(WOLFSSH* ssh, WS_UserAuthData* authData, word32 pubKeyBlobSz); WOLFSSH_API int wolfSSH_TestPrepareUserAuthRequestMlDsa(WOLFSSH* ssh,