diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 793ee2dcc..519d98434 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -402,7 +402,11 @@ static int CheckPasswordHashUnix(const char* input, char* stored) } #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ +#ifdef WOLFSSHD_UNIT_TEST +int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFSSHD_AUTH* authCtx) +#else static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFSSHD_AUTH* authCtx) +#endif { int ret = WS_SUCCESS; char* pwStr = NULL; diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index c487a68ca..61d241571 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -96,6 +96,7 @@ void wolfSSHD_FreeUserGroupNames(void* heap, char** names, word32 count); #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) int CheckPasswordHashUnix(const char* input, char* stored); +int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFSSHD_AUTH* authCtx); #endif int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, word32 keySz); diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index aa5c410de..8c899d9e8 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -1307,6 +1308,129 @@ static int test_CheckPasswordHashUnix(void) } } + /* (a) empty input + empty stored hash -> SUCCESS (pins the empty branch). */ + if (ret == WS_SUCCESS) { + char emptyStored[2]; + emptyStored[0] = 0; + Log(" Testing scenario: empty password + empty stored hash authenticates."); + rc = CheckPasswordHashUnix("", emptyStored); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + /* (b) empty input + real $6$ hash -> FAILURE (kills the && -> || mutant). */ + if (ret == WS_SUCCESS) { + Log(" Testing scenario: empty password + real hash is rejected."); + rc = CheckPasswordHashUnix("", stored); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + /* (c) non-empty input + empty stored hash -> FAILURE. */ + if (ret == WS_SUCCESS) { + char emptyStored[2]; + emptyStored[0] = 0; + Log(" Testing scenario: non-empty password + empty stored hash is rejected."); + rc = CheckPasswordHashUnix(correct, emptyStored); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + /* (d) locked account (stored[0]=='*') -> FAILURE (pins the '*' guard). */ + if (ret == WS_SUCCESS) { + char lockedStored[3]; + lockedStored[0] = '*'; + lockedStored[1] = 0; + Log(" Testing scenario: locked account is rejected."); + rc = CheckPasswordHashUnix(correct, lockedStored); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + return ret; +} + +/* Verify CheckPasswordUnix zeroes the pwStr buffer before freeing it. The + * cleanup ForceZero(pwStr, pwSz+1) in auth.c is otherwise uncovered; removing + * it leaves the plaintext password in freed heap memory. getpwnam() fails for + * the bogus user, so pwStr (size pwSz+1) is the only wolfSSL-heap allocation + * on this path and storedHashCpy stays NULL. The capturing allocator copies + * the buffer's bytes at free time so they can be inspected. */ +static void* pwCapPtr = NULL; +static byte pwCapBuf[7]; /* pwSz(6) + 1 */ +static int pwCapDone = 0; + +static void* pwCapMalloc(size_t size) +{ + void* p = malloc(size); + if (p != NULL && size == sizeof(pwCapBuf) && pwCapPtr == NULL) + pwCapPtr = p; + return p; +} + +static void pwCapFree(void* ptr) +{ + if (ptr != NULL && ptr == pwCapPtr) { + WMEMCPY(pwCapBuf, ptr, sizeof(pwCapBuf)); + pwCapDone = 1; + } + free(ptr); +} + +static void* pwCapRealloc(void* ptr, size_t size) +{ + return realloc(ptr, size); +} + +static int test_CheckPasswordUnix(void) +{ + int ret = WS_SUCCESS; + word32 i; + + pwCapPtr = NULL; + pwCapDone = 0; + WMEMSET(pwCapBuf, 0xA5, sizeof(pwCapBuf)); + + Log(" Testing scenario: CheckPasswordUnix wipes pwStr before free."); + if (wolfSSL_SetAllocators(pwCapMalloc, pwCapFree, pwCapRealloc) != 0) { + Log(" FAILED (set allocators).\n"); + return WS_FATAL_ERROR; + } + (void)CheckPasswordUnix("wolfsshd-no-such-user-xyz", + (const byte*)"secret", 6, NULL); + wolfSSL_SetAllocators(NULL, NULL, NULL); + + if (!pwCapDone) { + Log(" FAILED (pwStr allocation not captured).\n"); + return WS_FATAL_ERROR; + } + for (i = 0; i < (word32)sizeof(pwCapBuf); i++) { + if (pwCapBuf[i] != 0) { + ret = WS_FATAL_ERROR; + break; + } + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); return ret; } #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ @@ -1932,6 +2056,7 @@ const TEST_CASE testCases[] = { #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) TEST_DECL(test_CheckPasswordHashUnix), + TEST_DECL(test_CheckPasswordUnix), #endif }; diff --git a/tests/unit.c b/tests/unit.c index 536999c14..ccf1ebe35 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -5332,6 +5332,89 @@ static int test_SshResourceFree_zeroesSecrets(void) return result; } +/* Verify HandshakeInfoFree wipes the HandshakeInfo before releasing it: + * - hs->keys / hs->peerKeys: negotiated session enc+MAC keys + * - hs->e / hs->x: KEX public value and ephemeral private + * - hs->privKey: ephemeral KEX key union + * Mutation testing flagged the ForceZero(hs, sizeof(HandshakeInfo)) in + * HandshakeInfoFree (src/internal.c) as uncovered; removing it would leave + * this material in heap memory after free. wolfSSH_TestFreeHandshake exposes + * the free path; the retain-on-free allocator diverts the freed block so its + * bytes can be inspected. Only data fields are marked - pointer fields stay + * NULL (from wolfSSH_new zero-init) and the useX/kexHashId flags stay 0 so no + * per-key free runs over the poisoned bytes. */ +static int test_HandshakeInfoFree_zeroesSecrets(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + HandshakeInfo* hs = NULL; + word32 i; + const byte* p; + int result = 0; + int retainInstalled = 0; + wolfSSL_Malloc_cb prevMf = NULL; + wolfSSL_Free_cb prevFf = NULL; + wolfSSL_Realloc_cb prevRf = NULL; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -760; + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { + result = -761; + goto out; + } + hs = ssh->handshake; + if (hs == NULL) { + result = -762; + goto out; + } + + WMEMSET(&hs->keys, 0xA5, sizeof(hs->keys)); + WMEMSET(&hs->peerKeys, 0xA5, sizeof(hs->peerKeys)); + WMEMSET(hs->e, 0xA5, sizeof(hs->e)); + WMEMSET(hs->x, 0xA5, sizeof(hs->x)); + WMEMSET(&hs->privKey, 0xA5, sizeof(hs->privKey)); + + wolfSSL_GetAllocators(&prevMf, &prevFf, &prevRf); + if (wolfSSL_SetAllocators(RetainMalloc, RetainFree, + RetainRealloc) != 0) { + result = -763; + goto out; + } + retainInstalled = 1; + wolfSSH_TestFreeHandshake(ssh); /* frees hs via HandshakeInfoFree */ + wolfSSL_SetAllocators(prevMf, prevFf, prevRf); + retainInstalled = 0; + + if (!IsRetained(hs)) { + result = -764; + goto out; + } + + p = (const byte*)&hs->keys; + for (i = 0; i < (word32)sizeof(hs->keys); i++) + if (p[i] != 0) { result = -765; goto out; } + p = (const byte*)&hs->peerKeys; + for (i = 0; i < (word32)sizeof(hs->peerKeys); i++) + if (p[i] != 0) { result = -766; goto out; } + for (i = 0; i < (word32)sizeof(hs->e); i++) + if (hs->e[i] != 0) { result = -767; goto out; } + for (i = 0; i < (word32)sizeof(hs->x); i++) + if (hs->x[i] != 0) { result = -768; goto out; } + p = (const byte*)&hs->privKey; + for (i = 0; i < (word32)sizeof(hs->privKey); i++) + if (p[i] != 0) { result = -769; goto out; } + +out: + if (retainInstalled) + wolfSSL_SetAllocators(prevMf, prevFf, prevRf); + DrainRetained(); + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + return result; +} + #endif /* WOLFSSH_TEST_CAPTURING_ALLOCATOR */ #ifndef WOLFSSH_NO_DH @@ -7845,6 +7928,10 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("SshResourceFree_zeroesSecrets: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_HandshakeInfoFree_zeroesSecrets(); + printf("HandshakeInfoFree_zeroesSecrets: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; #endif #ifndef WOLFSSH_NO_DH