diff --git a/ChangeLog.md b/ChangeLog.md index 55fed3026..5d6c1d672 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -156,13 +156,13 @@ ## Vulnerabilities -- [Critical] CVE-2025-14942. wolfSSH’s key exchange state machine can be - manipulated to leak the client’s password in the clear, trick the client to +- [Critical] CVE-2025-14942. wolfSSH's key exchange state machine can be + manipulated to leak the client's password in the clear, trick the client to send a bogus signature, or trick the client into skipping user authentication. This affects client applications with wolfSSH version 1.4.21 - and earlier. Users of wolfSSH must update or apply the fix patch and it’s + and earlier. Users of wolfSSH must update or apply the fix patch and it's recommended to update credentials used. This fix is also recommended for - wolfSSH server applications. While there aren’t any specific attacks, the + wolfSSH server applications. While there aren't any specific attacks, the same defect is present. Thanks to Aina Toky Rasoamanana of Valeo and Olivier Levillain of Telecom SudParis for the report. (PR 855) - [Medium] CVE-2025-15382. The function used to clean up a path string may read @@ -223,7 +223,7 @@ cross-implementation testing. SFTP fix for init to handle channel data which resolves a potential interoperability SFTP connection issue. (PR 846) - Fixed SCP receive handling to reject traversal filenames containing path - separators or “dot” components. (PR 845) + separators or "dot" components. (PR 845) - Fixed missing declaration of wc_SSH_KDF that caused build failures under strict compiler warnings. (PR 848) - Fixed SSH agent test setup so regression tests exercise the intended code @@ -237,7 +237,7 @@ ## Vulnerabilities -- [Critical] CVE-2025-11625 The client's host verification can be bypassed by a malicious server, and client credentials leaked. This affects client applications with wolfSSH version 1.4.20 and earlier. Users of wolfSSH on the client side must update or apply the fix patch and it’s recommended to update credentials used. Fixed in PR (https://github.com/wolfSSL/wolfssh/pull/840) +- [Critical] CVE-2025-11625 The client's host verification can be bypassed by a malicious server, and client credentials leaked. This affects client applications with wolfSSH version 1.4.20 and earlier. Users of wolfSSH on the client side must update or apply the fix patch and it's recommended to update credentials used. Fixed in PR (https://github.com/wolfSSL/wolfssh/pull/840) - [Med] CVE-2025-11624 Potential for stack overflow write when reading the file handle provided by an SFTP client. After a SFTP connection was established there is the case where a SFTP client could craft a malicious read, write or set state SFTP packet which would cause the SFTP server code to write into stack. Thanks to Stanislav Fort of Aisle Research for the report. Fixed in PR (https://github.com/wolfSSL/wolfssh/pull/834) @@ -330,7 +330,7 @@ ## Fixes - Remove Inline for function HashForId() to resolve clash with WOLFSSH_LOCAL declaration (PR 738) -- Fix for wolfSSHd’s handling of re-key and window full when processing a command with lots of stdout text (PR 719) +- Fix for wolfSSHd's handling of re-key and window full when processing a command with lots of stdout text (PR 719) - Fix for wolfSSH client app to gracefully clean up on failure and added more WLOG debug messages (PR 732) - Minor static analysis report fixes (PR 740, 735) - Fix for handling SFTP transfer to non-existent folder (PR 743) @@ -371,7 +371,7 @@ - Add callback hooks for most channel messages including open, close, success, fail, and requests. - Reduce the number of memory allocations SCP makes. -- Improve wolfSSHd’s behavior on closing a connection. It closes channels and +- Improve wolfSSHd's behavior on closing a connection. It closes channels and waits for the peer to close the channels. ## Fixes @@ -532,7 +532,7 @@ - Internal refactor of client apps to simplify them and added X509 support to scpclient - wolfSSH_accept now returns WS_SCP_INIT and needs called again to complete the SCP operation - Update to document Cube Pack dependencies -- Add carriage return for ‘enter’ key in the example client with shell connections to windows server +- Add carriage return for 'enter' key in the example client with shell connections to windows server - Stack usage improvement to limit the scope of variables - Echoserver example SFTP non blocking improvement for want read cases - Increase SFTP performance with throughput @@ -540,7 +540,7 @@ ## Fixes - Fix for calling chdir after chroot with wolfSSHd when jailing connections on unix environments -- Better handling on the server side for when the client’s window is filled up +- Better handling on the server side for when the client's window is filled up - Fix for building the client project on windows when shell support is enabled - Sanity check improvements for handling memory management with non blocking connections - Fix for support with secondary groups with wolfSSHd @@ -716,7 +716,7 @@ - Fix for potential memory leak with agent and a case with wolfSHS_SFTP_GetHandle - Fuzzing fix for potential out of bounds read in the public key user auth messages - MQX build fixes -- Sanity check that agent was set before setting the agent’s channel +- Sanity check that agent was set before setting the agent's channel - Fuzzing fix for bounds checking with DoKexDhReply internal function - Fuzzing fix for clean up of base path with SCP use - Fuzzing fix for sanity checks on setting the prime group and generator @@ -817,7 +817,7 @@ - Fix for warning with enums used with SFTP and set socket type - Added example server with Renesas CS+ port - Fix for initializing UserAuthData to all zeros before use -- Fix for SFTP “LS” operation when setting the default window size to 2048 +- Fix for SFTP "LS" operation when setting the default window size to 2048 - Add structure size print out option -z to example client when the macro WOLFSSH_SHOW_SIZES is defined - Additional automated tests of wolfSSH_CTX_UsePrivateKey_buffer and fix for @@ -853,12 +853,12 @@ - Change name of internal function SendBuffered() to avoid clash with wolfSSL - Add support for SFTP on Windows - Use int types for arguments in examples to fix Raspberry Pi build -- Fix for fail case with leading 0’s on MPINT +- Fix for fail case with leading 0's on MPINT - Default window size (DEFAULT_WINDOW_SZ) lowered from ~ 1 MB to ~ 16 KB - Disable examples option added to configure (--disable-examples) - Callback function and example use added for checking public key sent - AES CTR cipher support added -- Fix for free’ing ECC caches with examples +- Fix for free'ing ECC caches with examples - Renamed example SFTP to be examples/sftpclient/wolfsftp diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 6ff2ec294..362aee270 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -378,7 +378,7 @@ static int test_ConfigCopy(void) return ret; } -/* Verifies ConfigFree releases all string fields — most useful under ASan. */ +/* Verifies ConfigFree releases all string fields - most useful under ASan. */ static int test_ConfigFree(void) { int ret = WS_SUCCESS; @@ -403,10 +403,10 @@ static int test_ConfigFree(void) if (ret == WS_SUCCESS) ret = wolfSSHD_ConfigSetAuthKeysFile(head, ".ssh/authorized_keys"); - /* Match User — allocates usrAppliesTo on the copied node */ + /* Match User - allocates usrAppliesTo on the copied node */ if (ret == WS_SUCCESS) ret = PCL("Match User alice"); - /* Match Group — allocates groupAppliesTo on the next copied node */ + /* Match Group - allocates groupAppliesTo on the next copied node */ if (ret == WS_SUCCESS) ret = PCL("Match Group staff"); #undef PCL diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 2c0682d50..3e22eab4d 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -1424,7 +1424,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, else { /* open interactive shell */ ret = execv(cmd, (char**)args); } - if (ret && errno) { + if (ret) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Issue opening shell"); exit(1); } diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index 576f1c894..106b8fc90 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -164,12 +164,12 @@ static void myStatusCb(WOLFSSH* sshIn, word32* bytes, char* name) currentTime = current_time(0); if (currentTime == lastOutputTime) { if (bytes[0] != lastPrintedBytes[0] || bytes[1] != lastPrintedBytes[1]) { - /* Progress made in the same second — throttle but track latest */ + /* Progress made in the same second - throttle but track latest */ lastPrintedBytes[0] = bytes[0]; lastPrintedBytes[1] = bytes[1]; return; } - /* bytes unchanged: EOF final call — fall through to print */ + /* bytes unchanged: EOF final call - fall through to print */ } else { lastOutputTime = currentTime; diff --git a/scripts/fwd.test.expect b/scripts/fwd.test.expect index 64bd53af8..1bcc9461c 100755 --- a/scripts/fwd.test.expect +++ b/scripts/fwd.test.expect @@ -75,14 +75,14 @@ puts "\n\[1\] Starting nc server: nc -l 11111" spawn nc -l 11111 set nc_server_id $spawn_id set nc_server_pid [exp_pid] -puts " PID $nc_server_pid — waiting for a connection..." +puts " PID $nc_server_pid - waiting for a connection..." # --- [2] Start wolfssh server ------------------------------------------------ puts "\n\[2\] Starting wolfssh server..." spawn ./examples/echoserver/echoserver -1 -f set wolfssh_srv_id $spawn_id set wolfssh_srv_pid [exp_pid] -puts " PID $wolfssh_srv_pid — waiting for a connection..." +puts " PID $wolfssh_srv_pid - waiting for a connection..." # --- [3] Start wolfssh client ------------------------------------------------ puts "\n\[3\] Starting wolfssh client (plain:12345 -> 11111)..." diff --git a/src/internal.c b/src/internal.c index 898aaba50..a1a76d5bd 100644 --- a/src/internal.c +++ b/src/internal.c @@ -571,6 +571,7 @@ static HandshakeInfo* HandshakeInfoNew(void* heap) heap, DYNTYPE_HS); if (newHs != NULL) { WMEMSET(newHs, 0, sizeof(HandshakeInfo)); + newHs->expectMsgId = MSGID_NONE; newHs->kexId = ID_NONE; newHs->kexHashId = WC_HASH_TYPE_NONE; newHs->pubKeyId = ID_NONE; @@ -857,6 +858,30 @@ int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, byte state) { return IsMessageAllowed(ssh, msg, state); } + +static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); +static int DoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); +#ifndef WOLFSSH_NO_DH_GEX_SHA256 +static int DoKexDhGexRequest(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); +#endif + +int wolfSSH_TestDoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) +{ + return DoKexInit(ssh, buf, len, idx); +} + +int wolfSSH_TestDoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) +{ + return DoKexDhInit(ssh, buf, len, idx); +} + +#ifndef WOLFSSH_NO_DH_GEX_SHA256 +int wolfSSH_TestDoKexDhGexRequest(WOLFSSH* ssh, byte* buf, word32 len, + word32* idx) +{ + return DoKexDhGexRequest(ssh, buf, len, idx); +} +#endif #endif @@ -4248,6 +4273,9 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) byte algoId; byte list[24] = {ID_NONE}; byte cannedList[24] = {ID_NONE}; + byte kexIdGuess = ID_NONE; + byte pubKeyIdGuess = ID_NONE; + byte kexPacketFollows = 0; word32 listSz; word32 cannedListSz; word32 cannedAlgoNamesSz; @@ -4319,7 +4347,7 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) (const byte*)ssh->algoListKex, cannedAlgoNamesSz); } if (ret == WS_SUCCESS) { - ssh->handshake->kexIdGuess = list[0]; + kexIdGuess = list[0]; algoId = MatchIdLists(side, list, listSz, cannedList, cannedListSz); if (algoId == ID_UNKNOWN) { @@ -4364,6 +4392,7 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) } } if (ret == WS_SUCCESS) { + pubKeyIdGuess = list[0]; algoId = MatchIdLists(side, list, listSz, cannedList, cannedListSz); if (algoId == ID_UNKNOWN) { WLOG(WS_LOG_DEBUG, "Unable to negotiate Server Host Key Algo"); @@ -4521,10 +4550,15 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) /* First KEX Packet Follows */ if (ret == WS_SUCCESS) { WLOG(WS_LOG_DEBUG, "DKI: KEX Packet Follows"); - ret = GetBoolean(&ssh->handshake->kexPacketFollows, buf, len, &begin); + ret = GetBoolean(&kexPacketFollows, buf, len, &begin); if (ret == WS_SUCCESS) { WLOG(WS_LOG_DEBUG, " packet follows: %s", - ssh->handshake->kexPacketFollows ? "yes" : "no"); + kexPacketFollows ? "yes" : "no"); + if (kexPacketFollows + && (kexIdGuess != ssh->handshake->kexId + || pubKeyIdGuess != ssh->handshake->pubKeyId)) { + ssh->handshake->ignoreNextKexMsg = 1; + } } } @@ -4836,12 +4870,11 @@ static int DoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = WS_BAD_ARGUMENT; if (ret == WS_SUCCESS) { - if (ssh->handshake->kexPacketFollows - && ssh->handshake->kexIdGuess != ssh->handshake->kexId) { - + if (ssh->handshake->ignoreNextKexMsg) { /* skip this message. */ - WLOG(WS_LOG_DEBUG, "Skipping the client's KEX init function."); - ssh->handshake->kexPacketFollows = 0; + WLOG(WS_LOG_DEBUG, "Skipping client's KEXDH_INIT message due to " + "first_packet_follows guess mismatch."); + ssh->handshake->ignoreNextKexMsg = 0; *idx += len; return WS_SUCCESS; } @@ -6290,6 +6323,15 @@ static int DoKexDhGexRequest(WOLFSSH* ssh, ret = WS_BAD_ARGUMENT; if (ret == WS_SUCCESS) { + if (ssh->handshake->ignoreNextKexMsg) { + /* skip this message. */ + WLOG(WS_LOG_DEBUG, "Skipping client's KEXDH_GEX_REQUEST message " + "due to first_packet_follows guess mismatch."); + ssh->handshake->ignoreNextKexMsg = 0; + *idx += len; + return WS_SUCCESS; + } + begin = *idx; ret = GetUint32(&ssh->handshake->dhGexMinSz, buf, len, &begin); } @@ -11929,6 +11971,16 @@ int wolfSSH_RsaVerify(const byte *sig, word32 sigSz, WOLFSSH_UNUSED(loc); /* Unused when WLOG is not defined */ return ret; } + +#ifdef WOLFSSH_TEST_INTERNAL +int wolfSSH_TestRsaVerify(const byte* sig, word32 sigSz, + const byte* encDigest, word32 encDigestSz, + RsaKey* key, void* heap) +{ + return wolfSSH_RsaVerify(sig, sigSz, encDigest, encDigestSz, + key, heap, "wolfSSH_TestRsaVerify"); +} +#endif /* WOLFSSH_TEST_INTERNAL */ #endif /* WOLFSSH_NO_RSA */ diff --git a/src/wolfsftp.c b/src/wolfsftp.c index a4d654f99..7f7d29739 100644 --- a/src/wolfsftp.c +++ b/src/wolfsftp.c @@ -2056,7 +2056,7 @@ int wolfSSH_SFTP_RecvOpen(WOLFSSH* ssh, int reqId, byte* data, word32 maxSz) WLOG(WS_LOG_SFTP, "Receiving WOLFSSH_FTP_OPEN"); - #ifdef MICROCHIP_MPLAB_HARMONY + #if defined(MICROCHIP_MPLAB_HARMONY) || defined(FREESCALE_MQX) fd = WBADFILE; #else fd = -1; @@ -5541,7 +5541,8 @@ static int SFTP_SetMode(void* fs, char* name, word32 mode) { #endif #if !defined(USE_WINDOWS_API) && !defined(WOLFSSH_ZEPHYR) \ - && !defined(WOLFSSH_SFTP_SETMODEHANDLE) && !defined(WOLFSSH_FATFS) + && !defined(WOLFSSH_SFTP_SETMODEHANDLE) && !defined(WOLFSSH_FATFS) \ + && !defined(FREESCALE_MQX) /* Set the files mode * return WS_SUCCESS on success */ static int SFTP_SetModeHandle(void* fs, WFD handle, word32 mode) { @@ -5553,7 +5554,8 @@ static int SFTP_SetModeHandle(void* fs, WFD handle, word32 mode) { } #endif -#if !defined(_WIN32_WCE) && !defined(WOLFSSH_ZEPHYR) && !defined(WOLFSSH_FATFS) +#if !defined(_WIN32_WCE) && !defined(WOLFSSH_ZEPHYR) && !defined(WOLFSSH_FATFS) \ + && !defined(FREESCALE_MQX) /* sets a files attributes * returns WS_SUCCESS on success */ diff --git a/tests/api.c b/tests/api.c index c5211a526..4d8d14543 100644 --- a/tests/api.c +++ b/tests/api.c @@ -522,7 +522,12 @@ static int load_file(const char* filename, byte** buf, word32* bufSz) } if (ret == 0) { - rewind(f); + ret = fseek(f, 0, XSEEK_SET); + if (ret < 0) + ret = -8; + } + + if (ret == 0) { *buf = (byte*)malloc(*bufSz); if (*buf == NULL) ret = -5; @@ -579,6 +584,7 @@ static void test_wolfSSH_CTX_UseCert_buffer(void) wolfSSH_CTX_UseCert_buffer(ctx, cert, certSz, 99)); free(cert); + cert = NULL; AssertIntEQ(0, load_file("./keys/server-cert.der", &cert, &certSz)); AssertNotNull(cert); diff --git a/tests/auth.c b/tests/auth.c index 35d6390b4..e6143f52d 100644 --- a/tests/auth.c +++ b/tests/auth.c @@ -559,9 +559,9 @@ static int AcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz, } /* Run one pubkey auth attempt. - * sCtx – server context (authorised key hash) - * cCtx – client context (key material to present) - * expect – expected return value from both wolfSSH_connect() and + * sCtx - server context (authorised key hash) + * cCtx - client context (key material to present) + * expect - expected return value from both wolfSSH_connect() and * wolfSSH_accept(): WS_SUCCESS for a valid-key test, * WS_FATAL_ERROR for a reject test */ static int run_pubkey_test(PubkeyServerCtx* sCtx, PubkeyClientCtx* cCtx, @@ -767,6 +767,169 @@ static void test_pubkey_auth_wrong_key(void) #endif /* pubkey test guard */ +/* ----------------------------------------------------------------------- + * Password auth: unknown callback return value must not grant auth (issue 2486) + * This block intentionally has no NO_SHA256 guard — password auth does not + * use SHA256. The surrounding utility functions (tcp_listen, load_key, etc.) + * are available because they share the base server/client/threading guard. + * ----------------------------------------------------------------------- */ +#if !defined(NO_WOLFSSH_SERVER) && !defined(NO_WOLFSSH_CLIENT) && \ + !defined(SINGLE_THREADED) && !defined(WOLFSSH_TEST_BLOCK) + +/* Tracks how many times the server password auth callback has been invoked; + * must be reset to 0 before each test run. */ +static int invalidPwAttempts = 0; + +/* Server userAuth callback for test_invalid_cb_password. + * First call returns an out-of-enum value (-999) to exercise the default else + * branch in DoUserAuthRequestPassword. Second call returns REJECTED so the + * connection terminates cleanly and wolfSSH_accept() can return an error. */ +static int invalidPasswordServerAuth(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + (void)authData; + (void)ctx; + if (authType != WOLFSSH_USERAUTH_PASSWORD) + return WOLFSSH_USERAUTH_FAILURE; + if (invalidPwAttempts++ == 0) + return -999; /* unknown value: exercises default else; authFailure=1 */ + return WOLFSSH_USERAUTH_REJECTED; /* clean termination on retry */ +} + +/* Client userAuth callback for password tests: supplies a dummy password so + * the auth request reaches the server-side callback. */ +static int clientPasswordUserAuth(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + static const byte pw[] = "dummypass"; + (void)ctx; + if (authType != WOLFSSH_USERAUTH_PASSWORD) + return WOLFSSH_USERAUTH_FAILURE; + authData->sf.password.password = pw; + authData->sf.password.passwordSz = (word32)(sizeof(pw) - 1); + return WOLFSSH_USERAUTH_SUCCESS; +} + +/* Server thread for test_invalid_cb_password. Mirrors pubkey_server_thread + * but registers invalidPasswordServerAuth and stores the return code cleanly + * (no ES_ERROR abort) so the test can assert on it. */ +static THREAD_RETURN WOLFSSH_THREAD password_server_thread(void* args) +{ + thread_args* serverArgs = (thread_args*)args; + int ret = WS_SUCCESS; + word16 port = 0; + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + byte buf[EXAMPLE_KEYLOAD_BUFFER_SZ]; + word32 bufSz; + WS_SOCKET_T listenFd = WOLFSSH_SOCKET_INVALID; + WS_SOCKET_T clientFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + + serverArgs->return_code = EXIT_SUCCESS; + + tcp_listen(&listenFd, &port, 1); + SignalTcpReady(serverArgs->signal, port); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { serverArgs->return_code = WS_MEMORY_E; goto cleanup; } + + wolfSSH_SetUserAuth(ctx, invalidPasswordServerAuth); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { serverArgs->return_code = WS_MEMORY_E; goto cleanup; } + +#ifndef WOLFSSH_NO_ECDSA + bufSz = (word32)load_key(1, buf, sizeof(buf)); +#else + bufSz = (word32)load_key(0, buf, sizeof(buf)); +#endif + if (bufSz == 0 || wolfSSH_CTX_UsePrivateKey_buffer(ctx, buf, bufSz, + WOLFSSH_FORMAT_ASN1) < 0) { + serverArgs->return_code = WS_BAD_FILE_E; goto cleanup; + } + + clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &clientAddrSz); + if (clientFd == WOLFSSH_SOCKET_INVALID) { + serverArgs->return_code = WS_SOCKET_ERROR_E; goto cleanup; + } + wolfSSH_set_fd(ssh, (int)clientFd); + + ret = wolfSSH_accept(ssh); + serverArgs->return_code = ret; + +cleanup: + if (ssh != NULL && clientFd != WOLFSSH_SOCKET_INVALID) + wolfSSH_shutdown(ssh); + if (ssh != NULL) wolfSSH_free(ssh); + if (ctx != NULL) wolfSSH_CTX_free(ctx); + if (clientFd != WOLFSSH_SOCKET_INVALID) WCLOSESOCKET(clientFd); + if (listenFd != WOLFSSH_SOCKET_INVALID) WCLOSESOCKET(listenFd); + + WOLFSSL_RETURN_FROM_THREAD(0); +} + +/* Test: server password-auth callback returning an unknown value must not + * grant authentication. Flow: + * 1. Client sends password -> server callback returns -999 -> authFailure=1 + * -> SendUserAuthFailure (else branch in DoUserAuthRequestPassword hit). + * 2. Client retries -> server callback returns WOLFSSH_USERAUTH_REJECTED + * -> WS_USER_AUTH_E -> server sends disconnect. + * 3. wolfSSH_connect() returns WS_FATAL_ERROR; server return_code != WS_SUCCESS. */ +static void test_invalid_cb_password(void) +{ + thread_args serverArgs; + tcp_ready ready; + THREAD_TYPE serThread; + WOLFSSH_CTX* clientCtx = NULL; + WOLFSSH* clientSsh = NULL; + SOCKET_T sockFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + int ret; + + printf("Testing password auth with unknown callback return value\n"); + invalidPwAttempts = 0; + + serverArgs.signal = &ready; + serverArgs.pubkeyServerCtx = NULL; + InitTcpReady(serverArgs.signal); + + ThreadStart(password_server_thread, (void*)&serverArgs, &serThread); + WaitTcpReady(&ready); + + clientCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(clientCtx); + wolfSSH_CTX_SetPublicKeyCheck(clientCtx, AcceptAnyServerHostKey); + wolfSSH_SetUserAuth(clientCtx, clientPasswordUserAuth); + + clientSsh = wolfSSH_new(clientCtx); + AssertNotNull(clientSsh); + wolfSSH_SetUsername(clientSsh, "jill"); + + build_addr(&clientAddr, (char*)wolfSshIp, ready.port); + tcp_socket(&sockFd, ((struct sockaddr_in*)&clientAddr)->sin_family); + AssertIntEQ(connect(sockFd, (const struct sockaddr*)&clientAddr, + clientAddrSz), 0); + wolfSSH_set_fd(clientSsh, (int)sockFd); + + ret = wolfSSH_connect(clientSsh); + AssertIntEQ(ret, WS_FATAL_ERROR); + + wolfSSH_shutdown(clientSsh); + WCLOSESOCKET(sockFd); + wolfSSH_free(clientSsh); + wolfSSH_CTX_free(clientCtx); + + ThreadJoin(serThread); + AssertIntNE(serverArgs.return_code, WS_SUCCESS); /* auth must NOT be granted */ + + FreeTcpReady(&ready); +} + +#endif /* password invalid-cb test guard */ + #if !defined(NO_WOLFSSH_SERVER) && !defined(NO_WOLFSSH_CLIENT) && \ !defined(SINGLE_THREADED) && !defined(WOLFSSH_TEST_BLOCK) && \ !defined(NO_FILESYSTEM) && defined(WOLFSSH_KEYBOARD_INTERACTIVE) @@ -1153,6 +1316,165 @@ static void test_unbalanced_client_KeyboardInteractive(void) test_client(); unbalanced = 0; } + +/* ----------------------------------------------------------------------- + * Keyboard-interactive auth: unknown callback return value must not grant + * authentication (issue 2486) + * ----------------------------------------------------------------------- */ + +/* Server userAuth callback for test_invalid_cb_keyboard. + * KEYBOARD_SETUP is handled normally so the exchange reaches + * DoUserAuthInfoResponse. For the KEYBOARD (response-validation) step the + * callback returns -999, an out-of-enum value, to exercise the default else + * branch in DoUserAuthInfoResponse. */ +static int invalidKbServerAuth(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + WS_UserAuthData_Keyboard* prompts = (WS_UserAuthData_Keyboard*)ctx; + + if (authType == WOLFSSH_USERAUTH_KEYBOARD_SETUP) { + WMEMCPY(&authData->sf.keyboard, prompts, sizeof(WS_UserAuthData_Keyboard)); + return WS_SUCCESS; + } + if (authType == WOLFSSH_USERAUTH_KEYBOARD) + return -999; /* unknown value: exercises default else; authFailure=1 */ + return WOLFSSH_USERAUTH_FAILURE; +} + +/* Server thread for test_invalid_cb_keyboard. Sets up one keyboard prompt and + * registers invalidKbServerAuth. Stores the return code cleanly (no ES_ERROR + * abort) so the test can assert on it. */ +static THREAD_RETURN WOLFSSH_THREAD kb_invalid_server_thread(void* args) +{ + thread_args* serverArgs = (thread_args*)args; + int ret = WS_SUCCESS; + word16 port = 0; + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + byte buf[EXAMPLE_KEYLOAD_BUFFER_SZ]; + word32 bufSz; + WS_SOCKET_T listenFd = WOLFSSH_SOCKET_INVALID; + WS_SOCKET_T clientFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + WS_UserAuthData_Keyboard localPrompts; + byte* kbPrompts[1]; + word32 kbPromptLengths[1]; + byte kbPromptEcho[1]; + + serverArgs->return_code = EXIT_SUCCESS; + + kbPrompts[0] = (byte*)"Password: "; + kbPromptLengths[0] = 10; + kbPromptEcho[0] = 0; + WMEMSET(&localPrompts, 0, sizeof(localPrompts)); + localPrompts.promptCount = 1; + localPrompts.prompts = kbPrompts; + localPrompts.promptLengths = kbPromptLengths; + localPrompts.promptEcho = kbPromptEcho; + + tcp_listen(&listenFd, &port, 1); + SignalTcpReady(serverArgs->signal, port); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { serverArgs->return_code = WS_MEMORY_E; goto cleanup; } + + wolfSSH_SetUserAuth(ctx, invalidKbServerAuth); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { serverArgs->return_code = WS_MEMORY_E; goto cleanup; } + + wolfSSH_SetUserAuthCtx(ssh, &localPrompts); + + bufSz = (word32)load_key(1, buf, sizeof(buf)); + if (bufSz == 0) bufSz = (word32)load_key(0, buf, sizeof(buf)); + if (bufSz == 0 || wolfSSH_CTX_UsePrivateKey_buffer(ctx, buf, bufSz, + WOLFSSH_FORMAT_ASN1) < 0) { + serverArgs->return_code = WS_BAD_FILE_E; goto cleanup; + } + + clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &clientAddrSz); + if (clientFd == WOLFSSH_SOCKET_INVALID) { + serverArgs->return_code = WS_SOCKET_ERROR_E; goto cleanup; + } + wolfSSH_set_fd(ssh, (int)clientFd); + + ret = wolfSSH_accept(ssh); + serverArgs->return_code = ret; + +cleanup: + if (ssh != NULL && clientFd != WOLFSSH_SOCKET_INVALID) + wolfSSH_shutdown(ssh); + if (ssh != NULL) wolfSSH_free(ssh); + if (ctx != NULL) wolfSSH_CTX_free(ctx); + if (clientFd != WOLFSSH_SOCKET_INVALID) WCLOSESOCKET(clientFd); + if (listenFd != WOLFSSH_SOCKET_INVALID) WCLOSESOCKET(listenFd); + + WOLFSSL_RETURN_FROM_THREAD(0); +} + +/* Test: server keyboard-interactive callback returning an unknown value must + * not grant authentication. Flow: + * 1. Server provides one prompt; client sends one response. + * 2. Server callback returns -999 -> authFailure=1 -> SendUserAuthFailure + * (else branch in DoUserAuthInfoResponse hit). + * 3. Client retries keyboard auth; after ssh->kbAuthAttempts reaches 3 the + * client stops trying keyboard and DoUserAuthFailure returns WS_USER_AUTH_E. + * 4. wolfSSH_connect() returns WS_FATAL_ERROR; server return_code != WS_SUCCESS. */ +static void test_invalid_cb_keyboard(void) +{ + thread_args serverArgs; + tcp_ready ready; + THREAD_TYPE serThread; + WOLFSSH_CTX* clientCtx = NULL; + WOLFSSH* clientSsh = NULL; + SOCKET_T sockFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + int ret; + + printf("Testing keyboard-interactive auth with unknown callback return value\n"); + + kbResponses[0] = (byte*)testText1; + kbResponseLengths[0] = 4; + kbResponseCount = 1; + + serverArgs.signal = &ready; + serverArgs.pubkeyServerCtx = NULL; + InitTcpReady(serverArgs.signal); + + ThreadStart(kb_invalid_server_thread, (void*)&serverArgs, &serThread); + WaitTcpReady(&ready); + + clientCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(clientCtx); + wolfSSH_CTX_SetPublicKeyCheck(clientCtx, AcceptAnyServerHostKey); + wolfSSH_SetUserAuth(clientCtx, keyboardUserAuth); + + clientSsh = wolfSSH_new(clientCtx); + AssertNotNull(clientSsh); + wolfSSH_SetUsername(clientSsh, "test"); + + build_addr(&clientAddr, (char*)wolfSshIp, ready.port); + tcp_socket(&sockFd, ((struct sockaddr_in*)&clientAddr)->sin_family); + AssertIntEQ(connect(sockFd, (const struct sockaddr*)&clientAddr, + clientAddrSz), 0); + wolfSSH_set_fd(clientSsh, (int)sockFd); + + ret = wolfSSH_connect(clientSsh); + AssertIntEQ(ret, WS_FATAL_ERROR); + + wolfSSH_shutdown(clientSsh); + WCLOSESOCKET(sockFd); + wolfSSH_free(clientSsh); + wolfSSH_CTX_free(clientCtx); + + ThreadJoin(serThread); + AssertIntNE(serverArgs.return_code, WS_SUCCESS); /* auth must NOT be granted */ + + FreeTcpReady(&ready); +} + #endif /* WOLFSSH_TEST_BLOCK */ int wolfSSH_AuthTest(int argc, char** argv) @@ -1204,6 +1526,12 @@ int wolfSSH_AuthTest(int argc, char** argv) test_unbalanced_client_KeyboardInteractive(); #endif + /* Unknown callback return value must not grant auth (issue 2486) */ + test_invalid_cb_password(); +#if !defined(NO_FILESYSTEM) && defined(WOLFSSH_KEYBOARD_INTERACTIVE) + test_invalid_cb_keyboard(); +#endif + AssertIntEQ(wolfSSH_Cleanup(), WS_SUCCESS); return 0; diff --git a/tests/regress.c b/tests/regress.c index 8be5e8a80..1fa8e01c4 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1648,6 +1648,7 @@ static void TestPasswordEofNoCrash(void) WMEMSET(&auth, 0, sizeof(auth)); savedStdin = dup(STDIN_FILENO); + AssertTrue(savedStdin >= 0); devNull = open("/dev/null", O_RDONLY); AssertTrue(devNull >= 0); AssertTrue(dup2(devNull, STDIN_FILENO) >= 0); @@ -1924,6 +1925,167 @@ static void TestKeyboardResponseNullCtx(WOLFSSH* ssh) #endif /* WOLFSSH_KEYBOARD_INTERACTIVE */ +#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) \ + && !defined(WOLFSSH_NO_RSA) \ + && !defined(WOLFSSH_NO_CURVE25519_SHA256) \ + && !defined(WOLFSSH_NO_RSA_SHA2_256) + +#define FPF_KEX_GOOD "ecdh-sha2-nistp256" +#define FPF_KEX_BAD "curve25519-sha256" +#define FPF_KEY_GOOD "ssh-rsa" +#define FPF_KEY_BAD "rsa-sha2-256" + +/* Build a KEXINIT payload using the server ssh's own canned cipher/MAC lists + * so negotiation succeeds whichever AES/HMAC modes are compiled in. */ +static word32 BuildKexInitPayload(WOLFSSH* ssh, const char* kexList, + const char* keyList, byte firstPacketFollows, + byte* out, word32 outSz) +{ + word32 idx = 0; + + /* cookie */ + AssertTrue(idx + COOKIE_SZ <= outSz); + WMEMSET(out + idx, 0, COOKIE_SZ); + idx += COOKIE_SZ; + + idx = AppendString(out, outSz, idx, kexList); + idx = AppendString(out, outSz, idx, keyList); + idx = AppendString(out, outSz, idx, ssh->algoListCipher); + idx = AppendString(out, outSz, idx, ssh->algoListCipher); + idx = AppendString(out, outSz, idx, ssh->algoListMac); + idx = AppendString(out, outSz, idx, ssh->algoListMac); + idx = AppendString(out, outSz, idx, "none"); + idx = AppendString(out, outSz, idx, "none"); + idx = AppendString(out, outSz, idx, ""); + idx = AppendString(out, outSz, idx, ""); + + idx = AppendByte(out, outSz, idx, firstPacketFollows); + idx = AppendUint32(out, outSz, idx, 0); /* reserved */ + + return idx; +} + +typedef struct { + const char* description; + const char* kexList; + const char* keyList; + byte firstPacketFollows; + byte expectIgnore; +} FirstPacketFollowsCase; + +static const FirstPacketFollowsCase firstPacketFollowsCases[] = { + { "follows=0, guesses irrelevant: flag stays off", + FPF_KEX_BAD "," FPF_KEX_GOOD, FPF_KEY_BAD "," FPF_KEY_GOOD, 0, 0 }, + { "follows=1, both guesses match: do not skip", + FPF_KEX_GOOD, FPF_KEY_GOOD, 1, 0 }, + { "follows=1, KEX guess wrong: skip", + FPF_KEX_BAD "," FPF_KEX_GOOD, FPF_KEY_GOOD, 1, 1 }, + { "follows=1, host-key guess wrong: skip", /* regression case */ + FPF_KEX_GOOD, FPF_KEY_BAD "," FPF_KEY_GOOD, 1, 1 }, + { "follows=1, both guesses wrong: skip", + FPF_KEX_BAD "," FPF_KEX_GOOD, FPF_KEY_BAD "," FPF_KEY_GOOD, 1, 1 }, +}; + +static void RunFirstPacketFollowsCase(const FirstPacketFollowsCase* tc) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[512]; + word32 payloadSz; + word32 idx = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + + AssertIntEQ(wolfSSH_SetAlgoListKex(ssh, FPF_KEX_GOOD), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, FPF_KEY_GOOD), WS_SUCCESS); + + payloadSz = BuildKexInitPayload(ssh, tc->kexList, tc->keyList, + tc->firstPacketFollows, payload, sizeof(payload)); + + /* DoKexInit's tail hashes and sends a response; on a stripped-down + * WOLFSSH without a loaded host key or a primed peer proto id, that + * tail errors. We only care about the parse path up through + * first_packet_follows, where ignoreNextKexMsg is set. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + + AssertNotNull(ssh->handshake); + if (ssh->handshake->ignoreNextKexMsg != tc->expectIgnore) { + Fail(("ignoreNextKexMsg == %u (%s)", + tc->expectIgnore, tc->description), + ("%u", ssh->handshake->ignoreNextKexMsg)); + } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + +typedef int (*FirstPacketFollowsSkipFn)(WOLFSSH* ssh, byte* buf, word32 len, + word32* idx); + +/* With ignoreNextKexMsg set, the target Do* handler must consume the packet, + * clear the flag, and not advance clientState past CLIENT_KEXINIT_DONE. */ +static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn, + const char* label) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[8]; + word32 idx = 0; + int ret; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertNotNull(ssh->handshake); + + ssh->handshake->ignoreNextKexMsg = 1; + ssh->clientState = CLIENT_KEXINIT_DONE; + + /* Garbage payload — must never be parsed when skipped. */ + WMEMSET(payload, 0xAB, sizeof(payload)); + + ret = fn(ssh, payload, sizeof(payload), &idx); + if (ret != WS_SUCCESS) { + Fail(("%s returns WS_SUCCESS when skipping", label), ("%d", ret)); + } + AssertIntEQ(idx, sizeof(payload)); + AssertIntEQ(ssh->handshake->ignoreNextKexMsg, 0); + AssertIntEQ(ssh->clientState, CLIENT_KEXINIT_DONE); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + +static void TestFirstPacketFollowsSkipped(void) +{ + RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhInit, "DoKexDhInit"); +#ifndef WOLFSSH_NO_DH_GEX_SHA256 + RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhGexRequest, + "DoKexDhGexRequest"); +#endif +} + +static void TestFirstPacketFollows(void) +{ + size_t i; + size_t n = sizeof(firstPacketFollowsCases) + / sizeof(firstPacketFollowsCases[0]); + + for (i = 0; i < n; i++) { + RunFirstPacketFollowsCase(&firstPacketFollowsCases[i]); + } + TestFirstPacketFollowsSkipped(); +} + +#endif /* first_packet_follows coverage guard */ + + int main(int argc, char** argv) { WOLFSSH_CTX* ctx; @@ -1964,6 +2126,11 @@ int main(int argc, char** argv) TestAgentChannelNullAgentSendsOpenFail(); #endif TestKexInitRejectedWhenKeying(ssh); +#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) && !defined(WOLFSSH_NO_RSA) \ + && !defined(WOLFSSH_NO_CURVE25519_SHA256) \ + && !defined(WOLFSSH_NO_RSA_SHA2_256) + TestFirstPacketFollows(); +#endif TestDisconnectSetsDisconnectError(); TestClientBuffersIdempotent(); TestPasswordEofNoCrash(); diff --git a/tests/unit.c b/tests/unit.c index d443f756c..dfecaa85f 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -36,6 +36,10 @@ #include #include #include +#ifndef WOLFSSH_NO_RSA +#include +#include +#endif #define WOLFSSH_TEST_HEX2BIN #include @@ -845,8 +849,192 @@ static int test_DoChannelRequest(void) return result; } -#endif /* WOLFSSH_TEST_INTERNAL */ +#if !defined(WOLFSSH_NO_RSA) + +/* 2048-bit RSA private key (PKCS#1 DER). + * Same key as tests/auth.c hanselPrivateRsa - copied here so this + * test has no dependency on WOLFSSH_KEYGEN. */ +static const byte unitTestRsaPrivKey[] = { + 0x30, 0x82, 0x04, 0xa3, 0x02, 0x01, 0x00, 0x02, 0x82, 0x01, 0x01, 0x00, + 0xbd, 0x3f, 0x76, 0x45, 0xa3, 0x03, 0xac, 0x38, 0xd5, 0xc7, 0x0f, 0x93, + 0x30, 0x5a, 0x20, 0x9c, 0x89, 0x7c, 0xad, 0x05, 0x16, 0x46, 0x86, 0x83, + 0x0d, 0x8a, 0x2b, 0x16, 0x4a, 0x05, 0x2c, 0xe4, 0x77, 0x47, 0x70, 0x00, + 0xae, 0x1d, 0x83, 0xe2, 0xd9, 0x6e, 0x99, 0xd4, 0xf0, 0x45, 0x98, 0x15, + 0x93, 0xf6, 0x87, 0x4e, 0xac, 0x64, 0x63, 0xa1, 0x95, 0xc9, 0x7c, 0x30, + 0xe8, 0x3e, 0x2f, 0xa3, 0xf1, 0x24, 0x9f, 0x0c, 0x6b, 0x1c, 0xfe, 0x1b, + 0x02, 0x99, 0xcd, 0xc6, 0xa7, 0x6c, 0x84, 0x85, 0x46, 0x54, 0x12, 0x40, + 0xe1, 0xb4, 0xe5, 0xf2, 0xaa, 0x39, 0xec, 0xd6, 0x27, 0x24, 0x0b, 0xd1, + 0xa1, 0xe2, 0xef, 0x34, 0x69, 0x25, 0x6d, 0xc0, 0x74, 0x67, 0x25, 0x98, + 0x7d, 0xc4, 0xf8, 0x52, 0xab, 0x9b, 0x4b, 0x3a, 0x12, 0x1d, 0xe1, 0xe3, + 0xfa, 0xd6, 0xcf, 0x9a, 0xe6, 0x9c, 0x23, 0x4e, 0x39, 0xc4, 0x84, 0x16, + 0x88, 0x3d, 0x42, 0x4e, 0xd8, 0x2f, 0xcc, 0xd2, 0x91, 0x67, 0x9d, 0xb6, + 0x71, 0x2a, 0x02, 0x65, 0x5f, 0xbb, 0x75, 0x0e, 0x8c, 0xbb, 0x87, 0x97, + 0x97, 0xc6, 0xf8, 0xb2, 0x98, 0xe2, 0x2f, 0x68, 0x26, 0x4a, 0x53, 0xec, + 0x79, 0x3a, 0x8a, 0x5f, 0xcc, 0xcf, 0xf0, 0x16, 0x47, 0xb2, 0xd0, 0x43, + 0xd6, 0x36, 0x6c, 0xc8, 0xe7, 0x2f, 0xfe, 0xa7, 0x35, 0x39, 0x69, 0xfb, + 0x1d, 0x78, 0x45, 0x9d, 0x89, 0x00, 0xc8, 0x41, 0xcf, 0x34, 0x1f, 0xa3, + 0xf3, 0xf1, 0xfb, 0x28, 0x14, 0xfb, 0xd8, 0x48, 0x6f, 0xac, 0xe3, 0xfc, + 0x33, 0xd1, 0xdb, 0xae, 0xef, 0x27, 0x9e, 0x57, 0x56, 0x29, 0xa2, 0x1a, + 0x3a, 0xe5, 0x9a, 0xfe, 0xa4, 0x49, 0xc8, 0x7f, 0xb7, 0x4e, 0xd0, 0x1f, + 0x04, 0x6e, 0x58, 0x16, 0xb7, 0xeb, 0x9d, 0xf8, 0x92, 0x3c, 0xc2, 0xb0, + 0x21, 0x7c, 0x4e, 0x31, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x82, 0x01, + 0x01, 0x00, 0x8d, 0xa4, 0x61, 0x06, 0x2f, 0xc3, 0x40, 0xf4, 0x6c, 0xf4, + 0x87, 0x30, 0xb8, 0x00, 0xcc, 0xe5, 0xbc, 0x75, 0x87, 0x1e, 0x06, 0x95, + 0x14, 0x7a, 0x23, 0xf9, 0x24, 0xd4, 0x92, 0xe4, 0x1a, 0xbc, 0x88, 0x95, + 0xfc, 0x3b, 0x56, 0x16, 0x1b, 0x2e, 0xff, 0x64, 0x2b, 0x58, 0xd7, 0xd8, + 0x8e, 0xc2, 0x9f, 0xb2, 0xe5, 0x84, 0xb9, 0xbc, 0x8d, 0x61, 0x54, 0x35, + 0xb0, 0x70, 0xfe, 0x72, 0x04, 0xc0, 0x24, 0x6d, 0x2f, 0x69, 0x61, 0x06, + 0x1b, 0x1d, 0xe6, 0x2d, 0x6d, 0x79, 0x60, 0xb7, 0xf4, 0xdb, 0xb7, 0x4e, + 0x97, 0x36, 0xde, 0x77, 0xc1, 0x9f, 0x85, 0x4e, 0xc3, 0x77, 0x69, 0x66, + 0x2e, 0x3e, 0x61, 0x76, 0xf3, 0x67, 0xfb, 0xc6, 0x9a, 0xc5, 0x6f, 0x99, + 0xff, 0xe6, 0x89, 0x43, 0x92, 0x44, 0x75, 0xd2, 0x4e, 0x54, 0x91, 0x58, + 0xb2, 0x48, 0x2a, 0xe6, 0xfa, 0x0d, 0x4a, 0xca, 0xd4, 0x14, 0x9e, 0xf6, + 0x27, 0x67, 0xb7, 0x25, 0x7a, 0x43, 0xbb, 0x2b, 0x67, 0xd1, 0xfe, 0xd1, + 0x68, 0x23, 0x06, 0x30, 0x7c, 0xbf, 0x60, 0x49, 0xde, 0xcc, 0x7e, 0x26, + 0x5a, 0x3b, 0xfe, 0xa6, 0xa6, 0xe7, 0xa8, 0xdd, 0xac, 0xb9, 0xaf, 0x82, + 0x9a, 0x3a, 0x41, 0x7e, 0x61, 0x21, 0x37, 0xa3, 0x08, 0xe4, 0xc4, 0xbc, + 0x11, 0xf5, 0x3b, 0x8e, 0x4d, 0x51, 0xf3, 0xbd, 0xda, 0xba, 0xb2, 0xc5, + 0xee, 0xfb, 0xcf, 0xdf, 0x83, 0xa1, 0x82, 0x01, 0xe1, 0x51, 0x9d, 0x07, + 0x5a, 0x5d, 0xd8, 0xc7, 0x5b, 0x3f, 0x97, 0x13, 0x6a, 0x4d, 0x1e, 0x8d, + 0x39, 0xac, 0x40, 0x95, 0x82, 0x6c, 0xa2, 0xa1, 0xcc, 0x8a, 0x9b, 0x21, + 0x32, 0x3a, 0x58, 0xcc, 0xe7, 0x2d, 0x1a, 0x79, 0xa4, 0x31, 0x50, 0xb1, + 0x4b, 0x76, 0x23, 0x1b, 0xb3, 0x40, 0x3d, 0x3d, 0x72, 0x72, 0x32, 0xec, + 0x5f, 0x38, 0xb5, 0x8d, 0xb2, 0x8d, 0x02, 0x81, 0x81, 0x00, 0xed, 0x5a, + 0x7e, 0x8e, 0xa1, 0x62, 0x7d, 0x26, 0x5c, 0x78, 0xc4, 0x87, 0x71, 0xc9, + 0x41, 0x57, 0x77, 0x94, 0x93, 0x93, 0x26, 0x78, 0xc8, 0xa3, 0x15, 0xbd, + 0x59, 0xcb, 0x1b, 0xb4, 0xb2, 0x6b, 0x0f, 0xe7, 0x80, 0xf2, 0xfa, 0xfc, + 0x8e, 0x32, 0xa9, 0x1b, 0x1e, 0x7f, 0xe1, 0x26, 0xef, 0x00, 0x25, 0xd8, + 0xdd, 0xc9, 0x1a, 0x23, 0x00, 0x26, 0x3b, 0x46, 0x23, 0xc0, 0x50, 0xe7, + 0xce, 0x62, 0xb2, 0x36, 0xb2, 0x98, 0x09, 0x16, 0x34, 0x18, 0x9e, 0x46, + 0xbc, 0xaf, 0x2c, 0x28, 0x94, 0x2f, 0xe0, 0x5d, 0xc9, 0xb2, 0xc8, 0xfb, + 0x5d, 0x13, 0xd5, 0x36, 0xaa, 0x15, 0x0f, 0x89, 0xa5, 0x16, 0x59, 0x5d, + 0x22, 0x74, 0xa4, 0x47, 0x5d, 0xfa, 0xfb, 0x0c, 0x5e, 0x80, 0xbf, 0x0f, + 0xc2, 0x9c, 0x95, 0x0f, 0xe7, 0xaa, 0x7f, 0x16, 0x1b, 0xd4, 0xdb, 0x38, + 0x7d, 0x58, 0x2e, 0x57, 0x78, 0x2f, 0x02, 0x81, 0x81, 0x00, 0xcc, 0x1d, + 0x7f, 0x74, 0x36, 0x6d, 0xb4, 0x92, 0x25, 0x62, 0xc5, 0x50, 0xb0, 0x5c, + 0xa1, 0xda, 0xf3, 0xb2, 0xfd, 0x1e, 0x98, 0x0d, 0x8b, 0x05, 0x69, 0x60, + 0x8e, 0x5e, 0xd2, 0x89, 0x90, 0x4a, 0x0d, 0x46, 0x7e, 0xe2, 0x54, 0x69, + 0xae, 0x16, 0xe6, 0xcb, 0xd5, 0xbd, 0x7b, 0x30, 0x2b, 0x7b, 0x5c, 0xee, + 0x93, 0x12, 0xcf, 0x63, 0x89, 0x9c, 0x3d, 0xc8, 0x2d, 0xe4, 0x7a, 0x61, + 0x09, 0x5e, 0x80, 0xfb, 0x3c, 0x03, 0xb3, 0x73, 0xd6, 0x98, 0xd0, 0x84, + 0x0c, 0x59, 0x9f, 0x4e, 0x80, 0xf3, 0x46, 0xed, 0x03, 0x9d, 0xd5, 0xdc, + 0x8b, 0xe7, 0xb1, 0xe8, 0xaa, 0x57, 0xdc, 0xd1, 0x41, 0x55, 0x07, 0xc7, + 0xdf, 0x67, 0x3c, 0x72, 0x78, 0xb0, 0x60, 0x8f, 0x85, 0xa1, 0x90, 0x99, + 0x0c, 0xa5, 0x67, 0xab, 0xf0, 0xb6, 0x74, 0x90, 0x03, 0x55, 0x7b, 0x5e, + 0xcc, 0xc5, 0xbf, 0xde, 0xa7, 0x9f, 0x02, 0x81, 0x80, 0x40, 0x81, 0x6e, + 0x91, 0xae, 0xd4, 0x88, 0x74, 0xab, 0x7e, 0xfa, 0xd2, 0x60, 0x9f, 0x34, + 0x8d, 0xe3, 0xe6, 0xd2, 0x30, 0x94, 0xad, 0x10, 0xc2, 0x19, 0xbf, 0x6b, + 0x2e, 0xe2, 0xe9, 0xb9, 0xef, 0x94, 0xd3, 0xf2, 0xdc, 0x96, 0x4f, 0x9b, + 0x09, 0xb3, 0xa1, 0xb6, 0x29, 0x44, 0xf4, 0x82, 0xd1, 0xc4, 0x77, 0x6a, + 0xd7, 0x23, 0xae, 0x4d, 0x75, 0x16, 0x78, 0xda, 0x70, 0x82, 0xcc, 0x6c, + 0xef, 0xaf, 0xc5, 0x63, 0xc6, 0x23, 0xfa, 0x0f, 0xd0, 0x7c, 0xfb, 0x76, + 0x7e, 0x18, 0xff, 0x32, 0x3e, 0xcc, 0xb8, 0x50, 0x7f, 0xb1, 0x55, 0x77, + 0x17, 0x53, 0xc3, 0xd6, 0x77, 0x80, 0xd0, 0x84, 0xb8, 0x4d, 0x33, 0x1d, + 0x91, 0x1b, 0xb0, 0x75, 0x9f, 0x27, 0x29, 0x56, 0x69, 0xa1, 0x03, 0x54, + 0x7d, 0x9f, 0x99, 0x41, 0xf9, 0xb9, 0x2e, 0x36, 0x04, 0x24, 0x4b, 0xf6, + 0xec, 0xc7, 0x33, 0x68, 0x6b, 0x02, 0x81, 0x80, 0x60, 0x35, 0xcb, 0x3c, + 0xd0, 0xe6, 0xf7, 0x05, 0x28, 0x20, 0x1d, 0x57, 0x82, 0x39, 0xb7, 0x85, + 0x07, 0xf7, 0xa7, 0x3d, 0xc3, 0x78, 0x26, 0xbe, 0x3f, 0x44, 0x66, 0xf7, + 0x25, 0x0f, 0xf8, 0x76, 0x1f, 0x39, 0xca, 0x57, 0x0e, 0x68, 0xdd, 0xc9, + 0x27, 0xb2, 0x8e, 0xa6, 0x08, 0xa9, 0xd4, 0xe5, 0x0a, 0x11, 0xde, 0x3b, + 0x30, 0x8b, 0xff, 0x72, 0x28, 0xe0, 0xf1, 0x58, 0xcf, 0xa2, 0x6b, 0x93, + 0x23, 0x02, 0xc8, 0xf0, 0x09, 0xa7, 0x21, 0x50, 0xd8, 0x80, 0x55, 0x7d, + 0xed, 0x0c, 0x48, 0xd5, 0xe2, 0xe9, 0x97, 0x19, 0xcf, 0x93, 0x6c, 0x52, + 0xa2, 0xd6, 0x43, 0x6c, 0xb4, 0xc5, 0xe1, 0xa0, 0x9d, 0xd1, 0x45, 0x69, + 0x58, 0xe1, 0xb0, 0x27, 0x9a, 0xec, 0x2b, 0x95, 0xd3, 0x1d, 0x81, 0x0b, + 0x7a, 0x09, 0x5e, 0xa5, 0xf1, 0xdd, 0x6b, 0xe4, 0xe0, 0x08, 0xf8, 0x46, + 0x81, 0xc1, 0x06, 0x8b, 0x02, 0x81, 0x80, 0x00, 0xf6, 0xf2, 0xeb, 0x25, + 0xba, 0x78, 0x04, 0xad, 0x0e, 0x0d, 0x2e, 0xa7, 0x69, 0xd6, 0x57, 0xe6, + 0x36, 0x32, 0x50, 0xd2, 0xf2, 0xeb, 0xad, 0x31, 0x46, 0x65, 0xc0, 0x07, + 0x97, 0x83, 0x6c, 0x66, 0x27, 0x3e, 0x94, 0x2c, 0x05, 0x01, 0x5f, 0x5c, + 0xe0, 0x31, 0x30, 0xec, 0x61, 0xd2, 0x74, 0x35, 0xb7, 0x9f, 0x38, 0xe7, + 0x8e, 0x67, 0xb1, 0x50, 0x08, 0x68, 0xce, 0xcf, 0xd8, 0xee, 0x88, 0xfd, + 0x5d, 0xc4, 0xcd, 0xe2, 0x86, 0x3d, 0x4a, 0x0e, 0x04, 0x7f, 0xee, 0x8a, + 0xe8, 0x9b, 0x16, 0xa1, 0xfc, 0x09, 0x82, 0xe2, 0x62, 0x03, 0x3c, 0xe8, + 0x25, 0x7f, 0x3c, 0x9a, 0xaa, 0x83, 0xf8, 0xd8, 0x93, 0xd1, 0x54, 0xf9, + 0xce, 0xb4, 0xfa, 0x35, 0x36, 0xcc, 0x18, 0x54, 0xaa, 0xf2, 0x90, 0xb7, + 0x7c, 0x97, 0x0b, 0x27, 0x2f, 0xae, 0xfc, 0xc3, 0x93, 0xaf, 0x1a, 0x75, + 0xec, 0x18, 0xdb +}; +static const word32 unitTestRsaPrivKeySz = + (word32)sizeof(unitTestRsaPrivKey); +/* wolfSSH_RsaVerify unit test + * + * Verifies that wolfSSH_RsaVerify returns WS_RSA_E when given a signature + * whose decoded digest is the correct size but contains wrong content. + * This makes the `compare = ConstantCompare(...)` term in wolfSSH_RsaVerify + * load-bearing: deleting it from the condition would silently pass this test. + */ +static int test_RsaVerify_BadDigest(void) +{ + int result = 0; + int ret; + RsaKey key; + WC_RNG rng; + word32 idx = 0; + byte data[32]; + byte digest[WC_SHA256_DIGEST_SIZE]; + byte encDigest[MAX_ENCODED_SIG_SZ]; + int encDigestSz; + byte badEncDigest[MAX_ENCODED_SIG_SZ]; + byte sig[256]; /* 2048-bit RSA produces a 256-byte signature */ + int sigSz; + + WMEMSET(data, 0x42, sizeof(data)); + + if (wc_InitRng(&rng) != 0) { + printf("RsaVerify_BadDigest: wc_InitRng failed\n"); + return -500; + } + if (wc_InitRsaKey(&key, NULL) != 0) { + printf("RsaVerify_BadDigest: wc_InitRsaKey failed\n"); + wc_FreeRng(&rng); + return -501; + } + + ret = wc_RsaPrivateKeyDecode(unitTestRsaPrivKey, &idx, &key, + unitTestRsaPrivKeySz); + if (ret != 0) { result = -502; goto done; } + + /* Hash the payload */ + ret = wc_Hash(WC_HASH_TYPE_SHA256, data, sizeof(data), + digest, WC_SHA256_DIGEST_SIZE); + if (ret != 0) { result = -503; goto done; } + + /* Encode as PKCS#1 v1.5 DigestInfo */ + encDigestSz = wc_EncodeSignature(encDigest, digest, + WC_SHA256_DIGEST_SIZE, wc_HashGetOID(WC_HASH_TYPE_SHA256)); + if (encDigestSz <= 0) { result = -504; goto done; } + + /* Sign */ + sigSz = wc_RsaSSL_Sign(encDigest, (word32)encDigestSz, + sig, sizeof(sig), &key, &rng); + if (sigSz <= 0) { result = -505; goto done; } + + /* Positive case: correct sig + correct encDigest must succeed */ + ret = wolfSSH_TestRsaVerify(sig, (word32)sigSz, + encDigest, (word32)encDigestSz, &key, NULL); + if (ret != WS_SUCCESS) { result = -506; goto done; } + + /* Negative case: correct sig but tampered encDigest (same size, + * last byte of the SHA-256 hash flipped) must return WS_RSA_E. + * This is the scenario that deleting `compare` from the condition + * inside wolfSSH_RsaVerify would silently pass. */ + WMEMCPY(badEncDigest, encDigest, encDigestSz); + badEncDigest[encDigestSz - 1] ^= 0xFF; + ret = wolfSSH_TestRsaVerify(sig, (word32)sigSz, + badEncDigest, (word32)encDigestSz, &key, NULL); + if (ret != WS_RSA_E) { result = -507; goto done; } + +done: + wc_FreeRng(&rng); + wc_FreeRsaKey(&key); + return result; +} + +#endif /* !WOLFSSH_NO_RSA */ +#endif /* WOLFSSH_TEST_INTERNAL */ /* Error Code And Message Test */ @@ -944,6 +1132,12 @@ int wolfSSH_UnitTest(int argc, char** argv) unitResult = test_DoChannelRequest(); printf("DoChannelRequest: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#if !defined(WOLFSSH_NO_RSA) + unitResult = test_RsaVerify_BadDigest(); + printf("RsaVerify_BadDigest: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif #endif #ifdef WOLFSSH_KEYGEN diff --git a/wolfssh/internal.h b/wolfssh/internal.h index e7873bb9e..dd2a4e032 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -631,12 +631,10 @@ typedef struct Keys { typedef struct HandshakeInfo { byte expectMsgId; byte kexId; - byte kexIdGuess; byte kexHashId; byte pubKeyId; byte encryptId; byte macId; - byte kexPacketFollows; byte aeadMode; byte blockSz; @@ -663,6 +661,7 @@ typedef struct HandshakeInfo { word32 generatorSz; #endif + byte ignoreNextKexMsg:1; byte useDh:1; byte useEcc:1; byte useEccMlKem:1; @@ -1334,11 +1333,22 @@ enum WS_MessageIdLimits { word32 len, word32* idx); WOLFSSH_API int wolfSSH_TestDoChannelRequest(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); + WOLFSSH_API int wolfSSH_TestDoKexInit(WOLFSSH* ssh, byte* buf, + word32 len, word32* idx); + WOLFSSH_API int wolfSSH_TestDoKexDhInit(WOLFSSH* ssh, byte* buf, + word32 len, word32* idx); #ifndef WOLFSSH_NO_DH_GEX_SHA256 + WOLFSSH_API int wolfSSH_TestDoKexDhGexRequest(WOLFSSH* ssh, byte* buf, + word32 len, word32* idx); WOLFSSH_API int wolfSSH_TestValidateKexDhGexGroup(const byte* primeGroup, word32 primeGroupSz, const byte* generator, word32 generatorSz, word32 minBits, word32 maxBits, WC_RNG* rng); #endif /* !WOLFSSH_NO_DH_GEX_SHA256 */ +#ifndef WOLFSSH_NO_RSA + WOLFSSH_API int wolfSSH_TestRsaVerify(const byte* sig, word32 sigSz, + const byte* encDigest, word32 encDigestSz, + RsaKey* key, void* heap); +#endif /* !WOLFSSH_NO_RSA */ #endif /* WOLFSSH_TEST_INTERNAL */ /* dynamic memory types */ diff --git a/wolfssh/port.h b/wolfssh/port.h index 5e9571f9a..c78862903 100644 --- a/wolfssh/port.h +++ b/wolfssh/port.h @@ -449,7 +449,8 @@ extern "C" { #define WFSEEK(fs,s,o,w) fseek((s),(o),(w)) #define WFTELL(fs,s) ftell((s)) #define WFSTAT(fs,fd,b) fstat((fd),(b)) - #define WREWIND(fs,s) rewind((s)) + #define WREWIND(fs,s) do { fseek((s),0,SEEK_SET); \ + clearerr((s)); } while (0) #define WSEEK_END SEEK_END #define WBADFILE NULL #define WSETTIME(fs,f,a,m) (0)