From ad3840e74f4b31fcd3f07fbdd64453106b085c85 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 | 76 ++++-- 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 | 251 ++++++++++++++++-- apps/wolfsshd/wolfsshd.c | 62 ++++- 9 files changed, 588 insertions(+), 50 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..ae334526b 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) { @@ -1458,37 +1458,37 @@ 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 if 'usr' is root-equivalent for PermitRootLogin (any uid 0 + * account, or the literal name "root"; name-only on Windows). Shared by + * DoCheckUser and RequestAuthentication so all enforcement points agree. */ +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, @@ -1632,6 +1632,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, { int ret; int rc; + int isRoot; const char* usr; WOLFSSHD_CONFIG* usrConf = NULL; @@ -1646,7 +1647,9 @@ static int RequestAuthentication(WS_UserAuthData* authData, } usr = (const char*)authData->username; + isRoot = IsRootUser(usr); ret = DoCheckUser(usr, authCtx); + /* temporarily elevate permissions */ if (ret == WOLFSSH_USERAUTH_SUCCESS && wolfSSHD_AuthRaisePermissions(authCtx) != WS_SUCCESS) { @@ -1681,12 +1684,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 (isRoot == 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 +1710,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 +1748,18 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } + if (ret == WOLFSSH_USERAUTH_SUCCESS && + authData->type == WOLFSSH_USERAUTH_PUBLICKEY && + isRoot == 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 +1988,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 +1996,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..1bbb0df5d 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -23,6 +23,7 @@ #endif #include +#include #include #include #include @@ -222,6 +223,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 +309,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}, @@ -463,27 +513,16 @@ static int test_ConfigCopy(void) return ret; } -/* Verifies that a Match block override of the auth-relevant settings is the - * value returned by wolfSSHD_GetUserConf, and that it differs from the global - * node. RequestAuthentication and DoCheckUser resolve the per-user config via - * wolfSSHD_AuthGetUserConf (a wrapper around wolfSSHD_GetUserConf) before - * consulting PwAuth, PermitEmptyPw, PermitRootLogin and AuthKeysFileSet, so - * this locks in that resolution: a regression that reverts to the global node - * would be caught here. +/* Verifies a Match block override is returned by wolfSSHD_GetUserConf and + * differs from the global node; RequestAuthentication/DoCheckUser depend on + * this resolution for PwAuth, PermitEmptyPw, PermitRootLogin, and + * AuthKeysFileSet. * - * 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. */ + * Not covered here: the fail-closed NULL-config branches and the Match-aware + * PermitRootLogin modes (prohibit-password, forced-commands-only) need a + * real WOLFSSHD_AUTH context and system users, so they're covered instead 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 +1359,158 @@ 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; + /* Privilege separation off so RequestAuthentication's unconditional + * wolfSSHD_AuthReducePermissions() -> exit(1) on failure never fires: + * SetDefaultUserID then uses this process's own uid/gid, making the + * permission drop a no-op regardless of whether an 'sshd' account + * exists on the test machine. */ + static const char line[] = "UsePrivilegeSeparation no"; + + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) return WS_MEMORY_E; + + if (ParseConfigLine(&conf, line, (int)WSTRLEN(line), 0) != WS_SUCCESS) { + wolfSSHD_ConfigFree(conf); + return WS_FATAL_ERROR; + } + + 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, DYNTYPE_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, DYNTYPE_STRING); + } + else { + ret = WS_MEMORY_E; + } + + wolfSSHD_AuthFreeUser(authCtx); + wolfSSHD_ConfigFree(conf); + return ret; } #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ @@ -2297,6 +2488,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 +3180,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 +3197,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 +3225,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..1a382bb11 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -1995,6 +1995,34 @@ static int UserAuthResult(byte result, return WS_SUCCESS; } +#if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP) +/* Returns 1 if root's forced-commands-only ForceCommand isn't + * "internal-sftp": SFTP/SCP never honor ForceCommand (only SHELL_Subsystem + * does), so this blocks the confinement bypass. No-op on Windows + * (pPasswd is NULL there). */ +static int IsRootTransferBlocked(WPASSWD* pPasswd, WOLFSSHD_CONFIG* usrConf) +{ +#ifdef _WIN32 + (void)pPasswd; + (void)usrConf; + return 0; +#else + char* forcedCmd; + + if (pPasswd == NULL || pPasswd->pw_uid != 0) { + return 0; + } + if (wolfSSHD_ConfigGetPermitRoot(usrConf) != + WOLFSSHD_PERMIT_ROOT_FORCED_CMD) { + return 0; + } + + forcedCmd = wolfSSHD_ConfigGetForcedCmd(usrConf); + return (forcedCmd == NULL || WSTRCMP(forcedCmd, "internal-sftp") != 0); +#endif +} +#endif /* WOLFSSH_SFTP || WOLFSSH_SCP */ + /* handle wolfSSH accept and directing to correct subsystem */ #ifdef _WIN32 static DWORD HandleConnection(void* arg) @@ -2161,7 +2189,17 @@ static void* HandleConnection(void* arg) switch (ret) { case WS_SFTP_COMPLETE: #ifdef WOLFSSH_SFTP - ret = SFTP_Subsystem(conn, ssh, pPasswd, usrConf); + if (IsRootTransferBlocked(pPasswd, usrConf)) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] SFTP " + "subsystem for root requires " + "ForceCommand internal-sftp under " + "forced-commands-only"); + ret = WS_FATAL_ERROR; + } + else { + ret = SFTP_Subsystem(conn, ssh, pPasswd, + usrConf); + } #else err_sys("SFTP not compiled in. Please use " "--enable-sftp"); @@ -2170,7 +2208,17 @@ static void* HandleConnection(void* arg) case WS_SCP_INIT: #ifdef WOLFSSH_SCP - ret = SCP_Subsystem(conn, ssh, pPasswd, usrConf); + if (IsRootTransferBlocked(pPasswd, usrConf)) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] SCP " + "session for root requires ForceCommand " + "internal-sftp under " + "forced-commands-only"); + ret = WS_FATAL_ERROR; + } + else { + ret = SCP_Subsystem(conn, ssh, pPasswd, + usrConf); + } #else err_sys("SCP not compiled in. Please use " "--enable-scp"); @@ -2201,7 +2249,15 @@ static void* HandleConnection(void* arg) /* SCP can be an exec type */ if (ret == WS_SCP_INIT) { #ifdef WOLFSSH_SCP - ret = SCP_Subsystem(conn, ssh, pPasswd, usrConf); + if (IsRootTransferBlocked(pPasswd, usrConf)) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] SCP session " + "for root requires ForceCommand " + "internal-sftp under forced-commands-only"); + ret = WS_FATAL_ERROR; + } + else { + ret = SCP_Subsystem(conn, ssh, pPasswd, usrConf); + } #else err_sys("SCP not compiled in. Please use " "--enable-scp");