From 9937fffae87b988cd2c462a2f1118c589e576e8b Mon Sep 17 00:00:00 2001 From: Emma Stensland Date: Wed, 15 Jul 2026 15:36:25 -0600 Subject: [PATCH] F-6700: Add PermitRootLogin prohibit-password and forced-commands-only modes Update test keys for authentication testing --- apps/wolfsshd/auth.c | 89 +++++-- apps/wolfsshd/auth.h | 2 +- apps/wolfsshd/configuration.c | 16 +- apps/wolfsshd/configuration.h | 8 + apps/wolfsshd/test/run_all_sshd_tests.sh | 4 +- .../test/sshd_permitroot_forced_cmd.sh | 129 ++++++++++ .../test/sshd_permitroot_prohibit_password.sh | 90 +++++++ apps/wolfsshd/test/test_configuration.c | 235 +++++++++++++++++- apps/wolfsshd/wolfsshd.c | 2 +- 9 files changed, 532 insertions(+), 43 deletions(-) create mode 100755 apps/wolfsshd/test/sshd_permitroot_forced_cmd.sh create mode 100755 apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 37e2ca088..daf12cf82 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -368,13 +368,13 @@ static int ExtractSalt(char* hash, char** salt, int saltSz) #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) #ifdef WOLFSSHD_UNIT_TEST -int CheckPasswordHashUnix(const char* input, char* stored) +int CheckPasswordHashUnix(const char* input, const char* stored) #else -static int CheckPasswordHashUnix(const char* input, char* stored) +static int CheckPasswordHashUnix(const char* input, const char* stored) #endif { int ret = WSSHD_AUTH_SUCCESS; - char* hashedInput; + char* hashedInput = NULL; word32 hashedInputSz = 0, storedSz = 0; if (input == NULL || stored == NULL) { @@ -429,7 +429,7 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS } if (ret == WS_SUCCESS) { - pwStr = (char*)WMALLOC(pwSz + 1, NULL, DYNTYPE_STRING); + pwStr = (char*)WMALLOC(pwSz + 1, NULL, DYNAMIC_TYPE_STRING); if (pwStr == NULL) { ret = WS_MEMORY_E; } @@ -478,7 +478,7 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS } } if (ret == WS_SUCCESS) { - storedHashCpy = WSTRDUP(storedHash, NULL, DYNTYPE_STRING); + storedHashCpy = WSTRDUP(storedHash, NULL, DYNAMIC_TYPE_STRING); if (storedHashCpy == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error getting stored hash copy"); @@ -497,11 +497,11 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS if (pwStr != NULL) { WS_FORCEZERO(pwStr, pwSz + 1); - WFREE(pwStr, NULL, DYNTYPE_STRING); + WFREE(pwStr, NULL, DYNAMIC_TYPE_STRING); } if (storedHashCpy != NULL) { WS_FORCEZERO(storedHashCpy, (word32)WSTRLEN(storedHashCpy) + 1); - WFREE(storedHashCpy, NULL, DYNTYPE_STRING); + WFREE(storedHashCpy, NULL, DYNAMIC_TYPE_STRING); } WOLFSSH_UNUSED(authCtx); @@ -1458,37 +1458,44 @@ static int CheckPublicKeyWIN(const char* usr, } #endif /* _WIN32*/ -/* return WOLFSSH_USERAUTH_SUCCESS on success */ -static int DoCheckUser(const char* usr, WOLFSSHD_AUTH* auth) +/* Returns 1 when 'usr' is root-equivalent for PermitRootLogin purposes, 0 + * otherwise. On Unix this covers every uid 0 account (so an alias like + * "toor" cannot bypass PermitRootLogin) as well as the literal name "root", + * so a transient getpwnam failure cannot skip the check for root. On + * Windows there is no uid 0 and no logon token yet at this pre-auth stage, + * so this falls back to the literal name; a token based Administrators + * membership check would belong after authentication. Shared by DoCheckUser + * and RequestAuthentication so every PermitRootLogin enforcement point + * (login gate, password block, forced-command requirement) agrees on what + * counts as root. */ +static int IsRootUser(const char* usr) { - int ret = WOLFSSH_USERAUTH_SUCCESS; - int rc; int isRoot = 0; - WOLFSSHD_CONFIG* usrConf; #ifndef _WIN32 struct passwd* pwInfo; -#endif - - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Checking user name %s", usr); -#ifndef _WIN32 - /* PermitRootLogin covers every uid 0 account (so an alias like "toor" - * cannot bypass it) and the literal name "root", so a transient getpwnam - * failure cannot skip the check for root. */ pwInfo = getpwnam(usr); if ((pwInfo != NULL && pwInfo->pw_uid == 0) || XSTRCMP(usr, "root") == 0) { isRoot = 1; } #else - /* No uid 0 on Windows and no logon token yet at this pre-auth stage, so - * fall back to the literal name; a token based Administrators membership - * check would belong after authentication. */ if (XSTRCMP(usr, "root") == 0) { isRoot = 1; } #endif + return isRoot; +} + +/* return WOLFSSH_USERAUTH_SUCCESS on success */ +static int DoCheckUser(const char* usr, WOLFSSHD_AUTH* auth) +{ + int ret = WOLFSSH_USERAUTH_SUCCESS; + int rc; + WOLFSSHD_CONFIG* usrConf; + + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Checking user name %s", usr); - if (isRoot == 1) { + if (IsRootUser(usr) == 1) { /* Resolve per-user config so a Match override is honored; a NULL * result is unresolvable, so fail closed and reject. */ usrConf = wolfSSHD_AuthGetUserConf(auth, usr, NULL, NULL, NULL, NULL, @@ -1647,6 +1654,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, usr = (const char*)authData->username; ret = DoCheckUser(usr, authCtx); + /* temporarily elevate permissions */ if (ret == WOLFSSH_USERAUTH_SUCCESS && wolfSSHD_AuthRaisePermissions(authCtx) != WS_SUCCESS) { @@ -1681,12 +1689,23 @@ static int RequestAuthentication(WS_UserAuthData* authData, if (ret == WOLFSSH_USERAUTH_SUCCESS && authData->type == WOLFSSH_USERAUTH_PASSWORD) { + int configAllowed = 0; if (wolfSSHD_ConfigGetPwAuth(usrConf) != 1) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Password authentication not " "allowed by configuration!"); ret = WOLFSSH_USERAUTH_REJECTED; } + else if (IsRootUser(usr) == 1 && + (wolfSSHD_ConfigGetPermitRoot(usrConf) == + WOLFSSHD_PERMIT_ROOT_PROHIBIT_PW || + wolfSSHD_ConfigGetPermitRoot(usrConf) == + WOLFSSHD_PERMIT_ROOT_FORCED_CMD)) { + /* prohibit-password and forced-commands-only both block this. */ + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Password authentication for " + "root not allowed by configuration!"); + ret = WOLFSSH_USERAUTH_REJECTED; + } /* Check if password is valid for this user. */ /* first handle empty password cases */ else if (authData->sf.password.passwordSz == 0 && @@ -1696,8 +1715,14 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_FAILURE; } else { + configAllowed = 1; + } + + /* Only run password check when config allows it (avoids leaks). */ + if (configAllowed) { rc = authCtx->checkPasswordCb(usr, authData->sf.password.password, - authData->sf.password.passwordSz, authCtx); + authData->sf.password.passwordSz, authCtx); + if (rc == WSSHD_AUTH_SUCCESS) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] Password ok."); } @@ -1728,6 +1753,18 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } + if (ret == WOLFSSH_USERAUTH_SUCCESS && + authData->type == WOLFSSH_USERAUTH_PUBLICKEY && + IsRootUser(usr) == 1 && + wolfSSHD_ConfigGetPermitRoot(usrConf) == + WOLFSSHD_PERMIT_ROOT_FORCED_CMD && + wolfSSHD_ConfigGetForcedCmd(usrConf) == NULL) { + /* forced-commands-only requires a forced command for root pubkey login. */ + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Public key login for root requires " + "a forced command by configuration!"); + ret = WOLFSSH_USERAUTH_REJECTED; + } + #ifdef WOLFSSL_FPKI if (ret == WOLFSSH_USERAUTH_SUCCESS && authData->type == WOLFSSH_USERAUTH_PUBLICKEY) { @@ -1956,7 +1993,7 @@ int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx) int ret = 0; if (ssh == NULL || ctx == NULL) - return WS_BAD_ARGUMENT; + return 0; authCtx = (WOLFSSHD_AUTH*)ctx; /* get configuration for user */ @@ -1964,7 +2001,7 @@ int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx) usrConf = wolfSSHD_AuthGetUserConf(authCtx, usr, NULL, NULL, NULL, NULL, NULL); if (usrConf == NULL) { - ret = WS_BAD_ARGUMENT; + ret = 0; } else { ret = wolfSSHD_GetUserAuthTypes(usrConf); diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index bbb771eb1..db5904ebc 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -107,7 +107,7 @@ int wolfSSHD_GetUserGroupNames(void* heap, const char* usr, WGID_T primaryGid, 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 CheckPasswordHashUnix(const char* input, const char* stored); #endif int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key, word32 keySz); diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index c03c95a07..3a177f3b6 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -100,7 +100,7 @@ struct WOLFSSHD_CONFIG { byte usePrivilegeSeparation:2; byte passwordAuth:1; byte pubKeyAuth:1; - byte permitRootLogin:1; + byte permitRootLogin:2; byte permitEmptyPasswords:1; byte authKeysFileSet:1; /* if not set then no explicit authorized keys */ byte strictModes:1; /* enforce file permission/ownership checks */ @@ -555,10 +555,20 @@ static int HandlePermitRoot(WOLFSSHD_CONFIG* conf, const char* value) if (ret == WS_SUCCESS) { if (WSTRCMP(value, "no") == 0) { - conf->permitRootLogin = 0; + conf->permitRootLogin = WOLFSSHD_PERMIT_ROOT_NO; } else if (WSTRCMP(value, "yes") == 0) { - conf->permitRootLogin = 1; + conf->permitRootLogin = WOLFSSHD_PERMIT_ROOT_YES; + } + else if (WSTRCMP(value, "prohibit-password") == 0 || + WSTRCMP(value, "without-password") == 0) { + conf->permitRootLogin = WOLFSSHD_PERMIT_ROOT_PROHIBIT_PW; + } + else if (WSTRCMP(value, "forced-commands-only") == 0) { + wolfSSH_Log(WS_LOG_WARN, "[SSHD] PermitRootLogin " + "forced-commands-only: authorized_keys command= " + "restrictions not enforced"); + conf->permitRootLogin = WOLFSSHD_PERMIT_ROOT_FORCED_CMD; } else { ret = WS_BAD_ARGUMENT; diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 6a84b2549..5792b4e89 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -43,6 +43,14 @@ typedef struct WOLFSSHD_CONFIG WOLFSSHD_CONFIG; #define MAX_PATH_SZ 80 #endif +/* 0 so that root login is default off after struct memset'd on init */ +#define WOLFSSHD_PERMIT_ROOT_NO 0 +#define WOLFSSHD_PERMIT_ROOT_YES 1 +#define WOLFSSHD_PERMIT_ROOT_PROHIBIT_PW 2 +/* Prohibits passwords; pubkey logins require a configured ForceCommand + * (ignores authorized_keys command="..." restrictions). */ +#define WOLFSSHD_PERMIT_ROOT_FORCED_CMD 3 + WOLFSSHD_CONFIG* wolfSSHD_ConfigNew(void* heap); void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigLoad(WOLFSSHD_CONFIG* conf, const char* filename); diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 4323f1233..1a1cb1559 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -396,12 +396,14 @@ else run_test "sshd_window_full_test.sh" run_test "sshd_empty_password_test.sh" run_test "sshd_permitroot_test.sh" + run_test "sshd_permitroot_prohibit_password.sh" + run_test "sshd_permitroot_forced_cmd.sh" run_strictmodes_negative_test run_test "sshd_login_grace_test.sh" run_test "sshd_privdrop_fail_test.sh" else printf "Skipping tests that need to setup local SSHD\n" - SKIPPED=$((SKIPPED+7)) + SKIPPED=$((SKIPPED+9)) fi # these tests run with X509 sshd-config loaded diff --git a/apps/wolfsshd/test/sshd_permitroot_forced_cmd.sh b/apps/wolfsshd/test/sshd_permitroot_forced_cmd.sh new file mode 100755 index 000000000..b8d68793d --- /dev/null +++ b/apps/wolfsshd/test/sshd_permitroot_forced_cmd.sh @@ -0,0 +1,129 @@ +#!/bin/bash + +# Test for PermitRootLogin forced-commands-only enforcement. + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "$0 127.0.0.1 22222" + exit 1 +fi + +TEST_HOST="$1" +TEST_PORT="$2" +USER="root" +PWD=`pwd` + +source ./start_sshd.sh + +# Build an authorized_keys file for "root" so we can test root public-key +# auth both with and without a ForceCommand configured. +cat ../../../keys/hansel-*.pub > authorized_keys_test_forced_cmd +sed -i.bak "s/hansel/root/" ./authorized_keys_test_forced_cmd + +TEST_CLIENT="../../../examples/client/client" + +cleanup() { + stop_wolfsshd + rm -f authorized_keys_test_forced_cmd authorized_keys_test_forced_cmd.bak \ + sshd_config_test_forced_cmd sshd_config_test_forced_cmd_with_cmd +} +trap cleanup EXIT + +### Scenario 1: forced-commands-only with no ForceCommand configured -- both +### root password auth and root pubkey auth must be rejected. +cat < sshd_config_test_forced_cmd +Port $TEST_PORT +Protocol 2 +LoginGraceTime 600 +PermitRootLogin forced-commands-only +PasswordAuthentication yes +AuthorizedKeysFile $PWD/authorized_keys_test_forced_cmd +StrictModes no +UsePrivilegeSeparation no +UseDNS no +HostKey $PWD/../../../keys/server-key.pem +EOF + +sudo rm -f ./log.txt + +start_wolfsshd "sshd_config_test_forced_cmd" +if [ -z "$PID" ]; then + echo "Failed to start wolfsshd" + exit 1 +fi + +timeout 10 $TEST_CLIENT -u "$USER" -P "somepassword" -c 'true' \ + -h "$TEST_HOST" -p "$TEST_PORT" > /dev/null 2>&1 +PASS_RESULT=$? + +if [ "$PASS_RESULT" = 0 ]; then + echo "FAIL: root password login unexpectedly succeeded under forced-commands-only" + exit 1 +fi + +timeout 10 $TEST_CLIENT -u "$USER" -c 'true' \ + -i ../../../keys/hansel-key-ecc.der -j ../../../keys/hansel-key-ecc.pub \ + -h "$TEST_HOST" -p "$TEST_PORT" > /dev/null 2>&1 +NOCMD_PUBKEY_RESULT=$? + +if [ "$NOCMD_PUBKEY_RESULT" = 0 ]; then + echo "FAIL: root pubkey login without a ForceCommand unexpectedly succeeded" + exit 1 +fi + +# Let wolfsshd flush the log before we stop it. +sleep 1 + +if ! sudo grep -q "Password authentication for root not allowed by configuration!" ./log.txt; then + echo "FAIL: forced-commands-only did not reject root password login as expected" + echo "----- log.txt -----" + sudo cat ./log.txt + exit 1 +fi + +if ! sudo grep -q "Public key login for root requires a forced command by configuration!" ./log.txt; then + echo "FAIL: forced-commands-only did not reject root pubkey login without a ForceCommand" + echo "----- log.txt -----" + sudo cat ./log.txt + exit 1 +fi + +stop_wolfsshd + +### Scenario 2: forced-commands-only WITH a ForceCommand configured -- root +### pubkey login must now be permitted. +cat < sshd_config_test_forced_cmd_with_cmd +Port $TEST_PORT +Protocol 2 +LoginGraceTime 600 +PermitRootLogin forced-commands-only +ForceCommand true +PasswordAuthentication yes +AuthorizedKeysFile $PWD/authorized_keys_test_forced_cmd +StrictModes no +UsePrivilegeSeparation no +UseDNS no +HostKey $PWD/../../../keys/server-key.pem +EOF + +sudo rm -f ./log.txt + +start_wolfsshd "sshd_config_test_forced_cmd_with_cmd" +if [ -z "$PID" ]; then + echo "Failed to start wolfsshd" + exit 1 +fi + +timeout 10 $TEST_CLIENT -u "$USER" -c 'true' \ + -i ../../../keys/hansel-key-ecc.der -j ../../../keys/hansel-key-ecc.pub \ + -h "$TEST_HOST" -p "$TEST_PORT" > /dev/null 2>&1 +WITHCMD_PUBKEY_RESULT=$? + +if [ "$WITHCMD_PUBKEY_RESULT" != 0 ]; then + echo "FAIL: root pubkey login with a ForceCommand configured was rejected" + echo "----- log.txt -----" + sudo cat ./log.txt + exit 1 +fi + +exit 0 diff --git a/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh b/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh new file mode 100755 index 000000000..d8802cb7c --- /dev/null +++ b/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# Test for PermitRootLogin prohibit-password enforcement. + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "$0 127.0.0.1 22222" + exit 1 +fi + +TEST_HOST="$1" +TEST_PORT="$2" +USER="root" +PWD=`pwd` + +source ./start_sshd.sh + +# Build an authorized_keys file for "root" so we can confirm root public-key +# auth is still permitted under prohibit-password, while root password auth +# is rejected. +cat ../../../keys/hansel-*.pub > authorized_keys_test_prohibit_pw +sed -i.bak "s/hansel/root/" ./authorized_keys_test_prohibit_pw + +cat < sshd_config_test_prohibit_pw +Port $TEST_PORT +Protocol 2 +LoginGraceTime 600 +PermitRootLogin prohibit-password +PasswordAuthentication yes +AuthorizedKeysFile $PWD/authorized_keys_test_prohibit_pw +StrictModes no +UsePrivilegeSeparation no +UseDNS no +HostKey $PWD/../../../keys/server-key.pem +EOF + +# Fresh log so we only see this run's output. +sudo rm -f ./log.txt + +cleanup() { + stop_wolfsshd + rm -f authorized_keys_test_prohibit_pw authorized_keys_test_prohibit_pw.bak \ + sshd_config_test_prohibit_pw +} +trap cleanup EXIT + +start_wolfsshd "sshd_config_test_prohibit_pw" +if [ -z "$PID" ]; then + echo "Failed to start wolfsshd" + exit 1 +fi + +TEST_CLIENT="../../../examples/client/client" + +# Attempt password login as root, which should be rejected +timeout 10 $TEST_CLIENT -u "$USER" -P "somepassword" -c 'true' \ + -h "$TEST_HOST" -p "$TEST_PORT" > /dev/null 2>&1 +PASS_RESULT=$? + +if [ "$PASS_RESULT" = 0 ]; then + echo "FAIL: root password login unexpectedly succeeded" + exit 1 +fi + +# Attempt public-key login as root, which should still be permitted under +# prohibit-password (only password auth is restricted for root). +timeout 10 $TEST_CLIENT -u "$USER" -c 'true' \ + -i ../../../keys/hansel-key-ecc.der -j ../../../keys/hansel-key-ecc.pub \ + -h "$TEST_HOST" -p "$TEST_PORT" > /dev/null 2>&1 +PUBKEY_RESULT=$? + +# Let wolfsshd flush the log before we stop it. +sleep 1 + +# log.txt is owned by root (wolfsshd ran via sudo); use sudo to read it. +if ! sudo grep -q "Password authentication for root not allowed by configuration!" ./log.txt; then + echo "FAIL: PermitRootLogin prohibit-password bypass detected or message changed" + echo "----- log.txt -----" + sudo cat ./log.txt + exit 1 +fi + +if [ "$PUBKEY_RESULT" != 0 ]; then + echo "FAIL: root public-key login was rejected under PermitRootLogin prohibit-password" + echo "----- log.txt -----" + sudo cat ./log.txt + exit 1 +fi + +exit 0 diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index f5dfa0ac0..ff928ca40 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -222,6 +222,44 @@ static int test_ConfigDefaults(void) return ret; } +/* Pins PermitRootLogin prohibit-password/without-password parsing. */ +static int test_PermitRootProhibitPassword(void) +{ + int ret = WS_SUCCESS; + WOLFSSHD_CONFIG* conf; + +#define PCL(s) ParseConfigLine(&conf, s, (int)WSTRLEN(s), 0) + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) + ret = WS_MEMORY_E; + + if (ret == WS_SUCCESS) ret = PCL("PermitRootLogin prohibit-password"); + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetPermitRoot(conf) != + WOLFSSHD_PERMIT_ROOT_PROHIBIT_PW) + ret = WS_FATAL_ERROR; + } + + if (ret == WS_SUCCESS) ret = PCL("PermitRootLogin without-password"); + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetPermitRoot(conf) != + WOLFSSHD_PERMIT_ROOT_PROHIBIT_PW) + ret = WS_FATAL_ERROR; + } + + if (ret == WS_SUCCESS) ret = PCL("PermitRootLogin forced-commands-only"); + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetPermitRoot(conf) != + WOLFSSHD_PERMIT_ROOT_FORCED_CMD) + ret = WS_FATAL_ERROR; + } +#undef PCL + + if (conf != NULL) + wolfSSHD_ConfigFree(conf); + return ret; +} + static int test_ParseConfigLine(void) { int ret = WS_SUCCESS; @@ -270,6 +308,17 @@ static int test_ParseConfigLine(void) {"Pubkey auth yes", "PubkeyAuthentication yes", 0}, {"Pubkey auth invalid", "PubkeyAuthentication wolfsshd", 1}, + /* Permit root login tests. */ + {"Permit root login no", "PermitRootLogin no", 0}, + {"Permit root login yes", "PermitRootLogin yes", 0}, + {"Permit root login prohibit-password", + "PermitRootLogin prohibit-password", 0}, + {"Permit root login without-password", + "PermitRootLogin without-password", 0}, + {"Permit root login forced-commands-only", + "PermitRootLogin forced-commands-only", 0}, + {"Permit root login invalid", "PermitRootLogin wolfsshd", 1}, + /* StrictModes tests. */ {"Strict modes no", "StrictModes no", 0}, {"Strict modes yes", "StrictModes yes", 0}, @@ -473,17 +522,20 @@ static int test_ConfigCopy(void) * * Coverage note: the new fail-closed branches in DoCheckUser and * RequestAuthentication (rejecting auth when wolfSSHD_AuthGetUserConf returns - * NULL, and the Match-aware PermitRootLogin check) are not exercised directly. - * Those paths require a populated WOLFSSHD_AUTH context (opaque to this test) - * plus real system users, group lookups, callbacks, and privilege raising, so - * they are validated here only at the config-resolution layer they depend on. - * The auth-boundary enforcement itself (a tightened Match node is honored, and - * a NULL per-user config rejects rather than falls through to the global node) - * is covered by manual/integration testing of wolfsshd against an sshd_config - * containing a Match block that disables password auth and PermitRootLogin. - * On Unix the PermitRootLogin check now resolves the account and gates on - * uid 0 (not the literal name "root"), so a non-"root" uid 0 alias is also - * rejected; that behavior is covered by sshd_permitroot_test.sh. */ + * NULL, and the Match-aware PermitRootLogin check, including its + * prohibit-password and forced-commands-only modes) are not exercised + * directly. Those paths require a populated WOLFSSHD_AUTH context (opaque to + * this test) plus real system users, group lookups, callbacks, and privilege + * raising, so they are validated here only at the config-resolution layer + * they depend on. The auth-boundary enforcement itself (a tightened Match + * node is honored, and a NULL per-user config rejects rather than falls + * through to the global node) is covered by manual/integration testing of + * wolfsshd against an sshd_config containing a Match block that disables + * password auth and PermitRootLogin. On Unix the PermitRootLogin check now + * resolves the account and gates on uid 0 (not the literal name "root"), so a + * non-"root" uid 0 alias is also rejected; that behavior is covered by + * sshd_permitroot_test.sh, sshd_permitroot_prohibit_password.sh, and + * sshd_permitroot_forced_cmd.sh. */ static int test_GetUserConfMatchOverride(void) { int ret = WS_SUCCESS; @@ -1320,6 +1372,147 @@ static int test_CheckPasswordHashUnix(void) } } + if (ret == WS_SUCCESS) { + char empty[1]; + + empty[0] = '\0'; + + Log(" Empty password + empty stored: "); + rc = CheckPasswordHashUnix(empty, empty); + if (rc == WSSHD_AUTH_SUCCESS) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + char empty[1]; + + empty[0] = '\0'; + + Log(" Empty password vs real hash: "); + rc = CheckPasswordHashUnix(empty, stored); + if (rc == WSSHD_AUTH_FAILURE) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + char emptyStored[1]; + + emptyStored[0] = '\0'; + + /* A hardened libxcrypt may reject the degenerate salt outright + * (crypt() returns NULL -> WS_FATAL_ERROR) rather than proceeding + * to a mismatched comparison (WSSHD_AUTH_FAILURE); either is a + * correct "not authenticated" outcome. */ + Log(" Non-empty password vs empty stored: "); + rc = CheckPasswordHashUnix(correct, emptyStored); + if (rc == WSSHD_AUTH_FAILURE || rc == WS_FATAL_ERROR) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + char locked[] = "*"; + + /* Same NULL-tolerant reasoning as the empty-salt case above. */ + Log(" Locked account (stored[0] == '*'): "); + rc = CheckPasswordHashUnix(correct, locked); + if (rc == WSSHD_AUTH_FAILURE || rc == WS_FATAL_ERROR) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + char lockedWithSalt[130]; + + /* A locked ('!') account with a valid hash must still fail auth, even if salt-reuse lets crypt() match. */ + lockedWithSalt[0] = '!'; + WMEMCPY(lockedWithSalt + 1, stored, WSTRLEN(stored) + 1); + + /* Same NULL-tolerant reasoning as the empty-salt case above. */ + Log(" Locked account with reusable '!' salt: "); + rc = CheckPasswordHashUnix(correct, lockedWithSalt); + if (rc == WSSHD_AUTH_FAILURE || rc == WS_FATAL_ERROR) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + } + + return ret; +} + +static int test_DefaultUserAuth_OOBRead(void) +{ + int ret = WS_SUCCESS; + WOLFSSHD_CONFIG* conf; + WOLFSSHD_AUTH* authCtx; + WS_UserAuthData authData; + char* passwordHeap; + word32 passwordSz = 16; + int rc; + + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) return WS_MEMORY_E; + + authCtx = wolfSSHD_AuthCreateUser(NULL, conf); + if (authCtx == NULL) { + wolfSSHD_ConfigFree(conf); + Log(" Skipping test: wolfSSHD_AuthCreateUser failed (likely missing 'sshd' user).\n"); + return WS_SUCCESS; + } + + passwordHeap = (char*)WMALLOC(passwordSz, NULL, DYNAMIC_TYPE_STRING); + if (passwordHeap != NULL) { + WMEMSET(passwordHeap, 'A', passwordSz); + + WMEMSET(&authData, 0, sizeof(authData)); + authData.type = WOLFSSH_USERAUTH_PASSWORD; + authData.username = (const byte*)"nonexistent_test_user_xyz"; + authData.usernameSz = (word32)WSTRLEN((const char*)authData.username); + authData.sf.password.password = (const byte*)passwordHeap; + authData.sf.password.passwordSz = passwordSz; + + Log(" Testing scenario: DefaultUserAuth with non-NUL-terminated password (OOB read check)."); + rc = DefaultUserAuth(WOLFSSH_USERAUTH_PASSWORD, &authData, authCtx); + if (rc == WOLFSSH_USERAUTH_INVALID_USER || + rc == WOLFSSH_USERAUTH_FAILURE || + rc == WOLFSSH_USERAUTH_REJECTED) { + Log(" PASSED.\n"); + } + else { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + + WFREE(passwordHeap, NULL, DYNAMIC_TYPE_STRING); + } + else { + ret = WS_MEMORY_E; + } + + wolfSSHD_AuthFreeUser(authCtx); + wolfSSHD_ConfigFree(conf); + return ret; } #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ @@ -2297,6 +2490,23 @@ static int test_GetUserAuthTypes(void) return ret; } +/* Ensures DefaultUserAuthTypes returns a safe 0 (no auth methods) on NULL args instead of corrupting the bitmask with an error code. */ +static int test_DefaultUserAuthTypesNullArgs(void) +{ + int ret = WS_SUCCESS; + + Log(" Testing scenario: DefaultUserAuthTypes with NULL ssh and ctx."); + if (DefaultUserAuthTypes(NULL, NULL) != 0) { + Log(" FAILED.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + + return ret; +} + #ifndef _WIN32 /* report a single secure-open scenario; returns WS_SUCCESS when the observed * result matches expectation (wantOk != 0 means expect acceptance) */ @@ -2972,6 +3182,7 @@ static int test_ResolveAuthKeysPath(void) const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), + TEST_DECL(test_PermitRootProhibitPassword), TEST_DECL(test_ParseConfigLine), TEST_DECL(test_ConfigCopy), TEST_DECL(test_GetUserConfMatchOverride), @@ -2988,6 +3199,7 @@ const TEST_CASE testCases[] = { TEST_DECL(test_MatchUPNToUser), TEST_DECL(test_IncludeRecursionBound), TEST_DECL(test_GetUserAuthTypes), + TEST_DECL(test_DefaultUserAuthTypesNullArgs), TEST_DECL(test_ConfigSetAuthKeysFile), TEST_DECL(test_ResolveAuthKeysPath), TEST_DECL(test_ConfigFree), @@ -3015,6 +3227,7 @@ const TEST_CASE testCases[] = { #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) TEST_DECL(test_CheckPasswordHashUnix), + TEST_DECL(test_DefaultUserAuth_OOBRead), #endif }; diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index a8e6e860a..efe6e410f 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -2884,7 +2884,7 @@ static int StartSSHD(int argc, char** argv) CleanupCTX(conf, &ctx, &banner); if (banner) { - WFREE(banner, NULL, DYNTYPE_STRING); + WFREE(banner, NULL, DYNAMIC_TYPE_STRING); } wolfSSHD_ConfigFree(conf); wolfSSHD_AuthFreeUser(auth);