Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/windows-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ jobs:
timeout-minutes: 2
run: .\sshd_dash_d_test.ps1 -SshdExe "$env:SSHD_PATH"

- name: Test wolfsshd refuses Match Group on Windows
working-directory: ${{ github.workspace }}\wolfssh\apps\wolfsshd\test
shell: pwsh
timeout-minutes: 2
run: .\sshd_match_group_win_test.ps1 -SshdExe "$env:SSHD_PATH"

# Build and run the self-contained unit tests with the MSVC AddressSanitizer.
# This is the only job that executes wolfSSH tests under a sanitizer on
# Windows, where the USE_WINDOWS_API console code (e.g. wolfSSH_DoOSC) is
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,9 @@ rejected inside a `Match` block):
`AuthorizedUPNDomains` or a per-user `AuthorizedKeysFile`; note
`AuthorizedUPNDomains` constrains only the certificate's UPN realm, not
which trusted CA issued it, so use it only when the OS trust store holds
solely your organization's CA.
solely your organization's CA. A config node with `AuthorizedKeysFile none`
also satisfies either check, since X.509 certificate login is refused
outright for the accounts it covers.

Note that the pre-existing `HostKey` and `HostCertificate` directives are now
also rejected when they appear after a `Match` block (matching OpenSSH); they
Expand Down
21 changes: 17 additions & 4 deletions apps/wolfsshd/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,16 @@ WOLFSSHD_STATIC int SearchForPubKey(const char* path,
char authKeysPath[MAX_PATH_SZ];
int rc = 0;

/* AuthorizedKeysFile none: no file is searched, so no key matches. The
* config's authKeysFileNone flag is authoritative; the setter stores
* "none" whenever it sets that flag, so this string check sees the same
* state and keeps a relative "none" from ever resolving to ~/none. */
if (wolfSSHD_ConfigIsNoneKeyword(authKeysFile)) {
wolfSSH_Log(WS_LOG_INFO,
"[SSHD] AuthorizedKeysFile is none, no keys to check");
return WSSHD_AUTH_FAILURE;
}

WMEMSET(authKeysPath, 0, sizeof(authKeysPath));
rc = ResolveAuthKeysPath(path, authKeysFile, user, authKeysPath);
if (rc != WS_SUCCESS) {
Expand Down Expand Up @@ -2921,9 +2931,12 @@ static int RequestAuthentication(WS_UserAuthData* authData,
if (ret == WOLFSSH_USERAUTH_SUCCESS &&
authData->type == WOLFSSH_USERAUTH_PUBLICKEY) {
/* if this is a certificate and no specific authorized keys file has
* been set then rely on CA to have verified the cert */
* been set then rely on CA to have verified the cert. With
* AuthorizedKeysFile none the certificate goes to the key check
* below and is refused, so none never widens access. */
if (authData->sf.publicKey.isCert &&
!wolfSSHD_ConfigGetAuthKeysFileSet(usrConf)) {
!wolfSSHD_ConfigGetAuthKeysFileSet(usrConf) &&
!wolfSSHD_ConfigGetAuthKeysFileNone(usrConf)) {
/* The cert chain was already verified by wolfSSH against the CA
* store loaded once from the global TrustedUserCAKeys. A per-user
* Match override of TrustedUserCAKeys is not part of that store and
Expand Down Expand Up @@ -3705,8 +3718,8 @@ WOLFSSHD_CONFIG* wolfSSHD_AuthGetUserConf(const WOLFSSHD_AUTH* auth,
if (auth != NULL) {
if (usr != NULL) {
#ifdef _WIN32
/* LogonUserEx(): group lookup is not implemented on Windows, so
* Match Group directives do not apply here */
/* group lookup is not implemented on Windows; config load
* refuses Match Group, so no block here depends on groups */
#else
struct passwd* p_passwd;

Expand Down
54 changes: 51 additions & 3 deletions apps/wolfsshd/configuration.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ struct WOLFSSHD_CONFIG {
byte permitRootLogin:2;
byte permitEmptyPasswords:1;
byte authKeysFileSet:1; /* if not set then no explicit authorized keys */
byte authKeysFileNone:1; /* AuthorizedKeysFile none: no file is read */
byte strictModes:1; /* enforce file permission/ownership checks */
byte useSystemCA:1;
byte useUserCAStore:1;
Expand Down Expand Up @@ -408,6 +409,7 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf)
newConf->permitRootLogin = conf->permitRootLogin;
newConf->permitEmptyPasswords = conf->permitEmptyPasswords;
newConf->authKeysFileSet = conf->authKeysFileSet;
newConf->authKeysFileNone = conf->authKeysFileNone;
newConf->strictModes = conf->strictModes;
newConf->head = conf->head;
newConf->useSystemCA = conf->useSystemCA;
Expand Down Expand Up @@ -1122,8 +1124,12 @@ static int HandleChrootDir(WOLFSSHD_CONFIG* conf, const char* value)
FreeString(&conf->chrootDir, conf->heap);
conf->chrootDir = NULL;
}
ret = CreateString(&conf->chrootDir, value,
/* "none" means no chroot, as in OpenSSH. The keyword is still
* recorded as set, so a Match block clears an inherited path. */
if (!wolfSSHD_ConfigIsNoneKeyword(value)) {
ret = CreateString(&conf->chrootDir, value,
(int)WSTRLEN(value), conf->heap);
}
}

return ret;
Expand Down Expand Up @@ -1220,7 +1226,8 @@ static int ParseMatchCriteria(WOLFSSHD_CONFIG* config, const char* value)
* is not User or Group (Address, Host, LocalAddress, LocalPort, RDomain, ...),
* and a Match with no selector at all (bare "Match", "Match all"), is rejected.
* This also catches mixed forms such as "User alice Address 10.0.0.0/8" where
* the unsupported selector would otherwise be silently dropped. */
* the unsupported selector would otherwise be silently dropped. Group is
* rejected too under WOLFSSHD_NO_MATCH_GROUP. */
static int CheckMatchSelectors(const char* value)
{
int ret = WS_SUCCESS;
Expand All @@ -1246,7 +1253,11 @@ static int CheckMatchSelectors(const char* value)
}
else if (len == (int)(sizeof("Group") - 1) &&
WSTRNCMP(value + i, "Group", sizeof("Group") - 1) == 0) {
#ifdef WOLFSSHD_NO_MATCH_GROUP
ret = WS_FATAL_ERROR;
#else
found = 1;
#endif
}
else {
ret = WS_FATAL_ERROR;
Expand Down Expand Up @@ -1288,9 +1299,15 @@ static int HandleMatch(WOLFSSHD_CONFIG** conf, const char* value, int valueSz)
if (ret == WS_SUCCESS) {
ret = CheckMatchSelectors(value);
if (ret != WS_SUCCESS) {
#ifdef WOLFSSHD_NO_MATCH_GROUP
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Unsupported Match selector, only User is handled on "
"this platform");
#else
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Unsupported Match selector, only User and Group are "
"handled");
#endif
}
}

Expand Down Expand Up @@ -1816,6 +1833,7 @@ static int ConfigComposeFrom(WOLFSSHD_CONFIG* dst, const WOLFSSHD_CONFIG* src)
if (take & OPT_BIT(OPT_AUTH_KEYS_FILE)) {
ret = ComposeString(&dst->authKeysFile, src->authKeysFile, dst->heap);
dst->authKeysFileSet = src->authKeysFileSet;
dst->authKeysFileNone = src->authKeysFileNone;
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_BANNER))) {
ret = ComposeString(&dst->banner, src->banner, dst->heap);
Expand Down Expand Up @@ -2007,6 +2025,26 @@ int wolfSSHD_ConfigGetAuthKeysFileSet(const WOLFSSHD_CONFIG* conf)
return ret;
}

/* returns 1 if value is the keyword "none", in any case, as OpenSSH accepts
* for AuthorizedKeysFile and ChrootDirectory, and 0 if not */
int wolfSSHD_ConfigIsNoneKeyword(const char* value)
{
return (value != NULL && WSTRLEN(value) == 4 &&
WSTRNCASECMP(value, "none", 4) == 0) ? 1 : 0;
}

/* returns 1 if AuthorizedKeysFile is "none" and 0 if not */
int wolfSSHD_ConfigGetAuthKeysFileNone(const WOLFSSHD_CONFIG* conf)
{
int ret = 0;

if (conf != NULL) {
ret = conf->authKeysFileNone;
}

return ret;
}

/* returns 1 if StrictModes is enabled and 0 if not. Defaults to enabled (fail
* safe) when conf is NULL. */
int wolfSSHD_ConfigGetStrictModes(const WOLFSSHD_CONFIG* conf)
Expand All @@ -2024,11 +2062,20 @@ int wolfSSHD_ConfigSetAuthKeysFile(WOLFSSHD_CONFIG* conf, const char* file)
{
int ret = WS_SUCCESS;
char* newFile = NULL;
byte none = 0;

if (conf == NULL) {
ret = WS_BAD_ARGUMENT;
}

/* "none" disables the file, as in OpenSSH, rather than naming ~/none.
* The value is kept as "none", not NULL, because NULL selects the
* default ~/.ssh/authorized_keys. */
if (ret == WS_SUCCESS && wolfSSHD_ConfigIsNoneKeyword(file)) {
none = 1;
file = "none";
}

/* allocate the replacement string first so a failure leaves the existing
* authKeysFile and authKeysFileSet untouched rather than half updated */
if (ret == WS_SUCCESS && file != NULL) {
Expand All @@ -2045,7 +2092,8 @@ int wolfSSHD_ConfigSetAuthKeysFile(WOLFSSHD_CONFIG* conf, const char* file)
* set when a file is explicitly configured so certificate public-key
* logins are still checked against it, cleared when removed */
conf->authKeysFile = newFile;
conf->authKeysFileSet = (file != NULL) ? 1 : 0;
conf->authKeysFileSet = (file != NULL && !none) ? 1 : 0;
conf->authKeysFileNone = none;
}

return ret;
Expand Down
9 changes: 9 additions & 0 deletions apps/wolfsshd/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ typedef struct WOLFSSHD_CONFIG WOLFSSHD_CONFIG;
#define WOLFSSHD_WIN_STORE_CONFIG
#endif

/* Windows wolfsshd does not look up a user's groups, so a Match Group block
* could never apply there. Such a config is refused rather than run without
* the block's restrictions. Defined here so the unit tests see it too. */
#if defined(_WIN32) && !defined(WOLFSSHD_NO_MATCH_GROUP)
#define WOLFSSHD_NO_MATCH_GROUP
#endif

#include "auth.h"

/* 0 so that privilege separation is default on after struct memset'd on init */
Expand Down Expand Up @@ -94,6 +101,8 @@ int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file);
word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf);
char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf);
int wolfSSHD_ConfigGetAuthKeysFileSet(const WOLFSSHD_CONFIG* conf);
int wolfSSHD_ConfigGetAuthKeysFileNone(const WOLFSSHD_CONFIG* conf);
int wolfSSHD_ConfigIsNoneKeyword(const char* value);
int wolfSSHD_ConfigSetAuthKeysFile(WOLFSSHD_CONFIG* conf, const char* file);
int wolfSSHD_ConfigGetStrictModes(const WOLFSSHD_CONFIG* conf);
byte wolfSSHD_ConfigGetPermitEmptyPw(const WOLFSSHD_CONFIG* conf);
Expand Down
4 changes: 4 additions & 0 deletions apps/wolfsshd/test/run_all_sshd_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ extra_test_cases=(
"strictmodes_authkeys_negative"
"sshd_forcedcmd_test.sh"
"sshd_match_overlap_test.sh"
"sshd_authkeys_none_test.sh"
"sshd_window_full_test.sh"
"sshd_stderr_eof_test.sh"
"sshd_stderr_drain_test.sh"
Expand All @@ -47,6 +48,7 @@ extra_test_cases=(
"sshd_login_grace_test.sh"
"sshd_privdrop_fail_test.sh"
"sshd_chroot_fail_test.sh"
"sshd_chroot_none_test.sh"
"sshd_fork_fail_test.sh"
"sshd_exit_status_test.sh"
"sshd_x509_test.sh"
Expand Down Expand Up @@ -760,6 +762,7 @@ fi
local_sshd_tests=(
"sshd_forcedcmd_test.sh"
"sshd_match_overlap_test.sh"
"sshd_authkeys_none_test.sh"
"sshd_window_full_test.sh"
"sshd_stderr_eof_test.sh"
"sshd_stderr_drain_test.sh"
Expand All @@ -772,6 +775,7 @@ local_sshd_tests=(
"sshd_login_grace_test.sh"
"sshd_privdrop_fail_test.sh"
"sshd_chroot_fail_test.sh"
"sshd_chroot_none_test.sh"
"sshd_fork_fail_test.sh"
"sshd_exit_status_test.sh"
)
Expand Down
Loading
Loading