Compose sshd_config Match blocks per keyword

wolfSSHD_GetUserConf returned the first matching Match block whole, so a
keyword named only in a later matching block was dropped and the outcome
depended on the order the blocks were written.

- track in a new setMask which keywords a node set itself, so a value
  inherited from the globals can be told from one the block named, with a
  compile time check that no option tag shifts out of the mask
- resolve into a fresh config seeded from the globals, letting every
  matching block contribute the keywords no earlier block claimed
- the resolved config now belongs to the caller, so wolfsshd and the auth
  paths free it and the tests compare values rather than node identity
- put sshd_match_overlap_test.sh back in the suite

Issue: ZD-22324
pull/1171/head
John Safranek 2026-08-17 13:55:23 -07:00 committed by JacobBarthelmeh
parent d9f596b8c5
commit 44bd4a06f4
8 changed files with 351 additions and 209 deletions

View File

@ -2331,6 +2331,7 @@ static int DoCheckUser(const char* usr, WOLFSSHD_AUTH* auth, int isRoot)
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Login as root not permitted");
ret = WOLFSSH_USERAUTH_REJECTED;
}
wolfSSHD_ConfigFree(usrConf);
}
if (ret == WOLFSSH_USERAUTH_SUCCESS) {
@ -2821,6 +2822,8 @@ static int RequestAuthentication(WS_UserAuthData* authData,
}
wolfSSHD_ConfigFree(usrConf);
if (wolfSSHD_AuthReducePermissions(authCtx) != WS_SUCCESS) {
/* stop everything if not able to reduce permissions level */
exit(1);
@ -2908,6 +2911,7 @@ int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx)
}
else {
ret = wolfSSHD_GetUserAuthTypes(usrConf);
wolfSSHD_ConfigFree(usrConf);
}
return ret;

View File

@ -93,6 +93,8 @@ const char* wolfSSHD_AuthMergeForcedCmd(const char* configCmd,
int wolfSSHD_AuthSetCertForcedCmd(WOLFSSHD_AUTH* auth, const byte* cmd,
word32 cmdSz);
#endif
/* Wraps wolfSSHD_GetUserConf with this user's resolved group set. The result is
* a newly allocated config the caller frees with wolfSSHD_ConfigFree(). */
WOLFSSHD_CONFIG* wolfSSHD_AuthGetUserConf(const WOLFSSHD_AUTH* auth,
const char* usr, const char* host,
const char* localAdr, word16* localPort, const char* RDomain,

View File

@ -96,6 +96,10 @@ struct WOLFSSHD_CONFIG {
char* authorizedUPNDomains; /* allowlist of UPN realms for cert auth */
WOLFSSHD_CONFIG* next; /* next config in list */
WOLFSSHD_CONFIG* head; /* global config the Match nodes branch from */
/* one bit per OPT_ tag, set when this node set that keyword itself rather
* than inheriting it. Drives the per keyword Match composition done by
* wolfSSHD_GetUserConf. */
word32 setMask;
long loginTimer;
word16 port;
byte usePrivilegeSeparation:2;
@ -254,7 +258,8 @@ WOLFSSHD_CONFIG* wolfSSHD_ConfigNew(void* heap)
/* on success return a newly create WOLFSSHD_CONFIG structure that has the
* same values set as the input 'conf'. User and group match values are not
* copied */
* copied, and neither is setMask: the copy inherits values but has set nothing
* of its own, which is what lets composition tell the two apart. */
static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf)
{
int ret = WS_SUCCESS;
@ -434,6 +439,17 @@ enum {
NUM_OPTIONS = 27
};
/* bit in WOLFSSHD_CONFIG.setMask recording that option 'o' was set on a node.
* setMask is a word32, so NUM_OPTIONS must stay at or below 32. */
#define OPT_BIT(o) ((word32)1 << (o))
/* A tag past bit 31 shifts out of setMask. The shift count wraps on the usual
* targets rather than trapping, so OPT_BIT(32) would quietly alias onto
* OPT_AUTH_KEYS_FILE and let composition claim a keyword no Match block named.
* Break the build instead. Kept local rather than using wc_static_assert so
* this file does not gain a minimum wolfSSL version. */
typedef char wolfsshd_opt_bits_fit[(NUM_OPTIONS <= 32) ? 1 : -1];
static const CONFIG_OPTION options[NUM_OPTIONS] = {
{OPT_AUTH_KEYS_FILE, "AuthorizedKeysFile"},
{OPT_PRIV_SEP, "UsePrivilegeSeparation"},
@ -1233,6 +1249,15 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt,
const char* value, const char* full, int fullSz, int depth)
{
int ret = WS_BAD_ARGUMENT;
WOLFSSHD_CONFIG* target;
if (conf == NULL || *conf == NULL) {
return WS_BAD_ARGUMENT;
}
/* the node the setting lands on. Match replaces the caller's cursor, so
* remember where we started. */
target = *conf;
switch (opt) {
case OPT_AUTH_KEYS_FILE:
@ -1334,6 +1359,12 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt,
break;
}
/* Match sets no keyword of its own, and Include's own lines were already
* recorded on whichever node they landed on. */
if (ret == WS_SUCCESS && opt != OPT_MATCH && opt != OPT_INCLUDE) {
target->setMask |= OPT_BIT(opt);
}
return ret;
}
@ -1483,61 +1514,174 @@ static int ConfigLoad(WOLFSSHD_CONFIG* conf, const char* filename, int depth)
}
/* returns the config associated with the user */
/* Replaces *dst with a copy of 'src', freeing whatever *dst held. A NULL 'src'
* just clears *dst. Returns WS_SUCCESS on success. */
static int ComposeString(char** dst, const char* src, void* heap)
{
int ret = WS_SUCCESS;
FreeString(dst, heap);
if (src != NULL) {
ret = CreateString(dst, src, (int)WSTRLEN(src), heap);
}
return ret;
}
/* Applies to 'dst' every keyword that 'src' set itself and that 'dst' has not
* already taken from an earlier block. Returns WS_SUCCESS on success. */
static int ConfigComposeFrom(WOLFSSHD_CONFIG* dst, const WOLFSSHD_CONFIG* src)
{
int ret = WS_SUCCESS;
word32 take = src->setMask & ~dst->setMask;
if (take & OPT_BIT(OPT_AUTH_KEYS_FILE)) {
ret = ComposeString(&dst->authKeysFile, src->authKeysFile, dst->heap);
dst->authKeysFileSet = src->authKeysFileSet;
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_BANNER))) {
ret = ComposeString(&dst->banner, src->banner, dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_CHROOT_DIR))) {
ret = ComposeString(&dst->chrootDir, src->chrootDir, dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_HOST_KEY))) {
ret = ComposeString(&dst->hostKeyFile, src->hostKeyFile, dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_HOST_CERT))) {
ret = ComposeString(&dst->hostCertFile, src->hostCertFile, dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_TRUSTED_USER_CA_KEYS))) {
ret = ComposeString(&dst->userCAKeysFile, src->userCAKeysFile,
dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_FORCE_CMD))) {
ret = ComposeString(&dst->forceCmd, src->forceCmd, dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_PIDFILE))) {
ret = ComposeString(&dst->pidFile, src->pidFile, dst->heap);
}
if (ret == WS_SUCCESS && (take & OPT_BIT(OPT_AUTHORIZED_UPN_DOMAINS))) {
ret = ComposeString(&dst->authorizedUPNDomains,
src->authorizedUPNDomains, dst->heap);
}
if (ret == WS_SUCCESS) {
if (take & OPT_BIT(OPT_LOGIN_GRACE_TIME)) {
dst->loginTimer = src->loginTimer;
}
if (take & OPT_BIT(OPT_PORT)) {
dst->port = src->port;
}
if (take & OPT_BIT(OPT_PRIV_SEP)) {
dst->usePrivilegeSeparation = src->usePrivilegeSeparation;
}
if (take & OPT_BIT(OPT_PASSWORD_AUTH)) {
dst->passwordAuth = src->passwordAuth;
}
if (take & OPT_BIT(OPT_PUBKEY_AUTH)) {
dst->pubKeyAuth = src->pubKeyAuth;
}
if (take & OPT_BIT(OPT_PERMIT_ROOT)) {
dst->permitRootLogin = src->permitRootLogin;
}
if (take & OPT_BIT(OPT_PERMIT_EMPTY_PW)) {
dst->permitEmptyPasswords = src->permitEmptyPasswords;
}
if (take & OPT_BIT(OPT_STRICT_MODES)) {
dst->strictModes = src->strictModes;
}
dst->setMask |= take;
}
return ret;
}
/* returns 1 when the Match block 'node' applies to the connection described by
* 'usr' and the group list, and 0 otherwise. A node carrying no selector at all
* is the global config, never a Match candidate. */
static int MatchApplies(const WOLFSSHD_CONFIG* node, const char* usr,
const char** grps, word32 grpCount)
{
int matches = 0;
word32 i;
/* Every non-NULL selector on the node must match, so a combined
* 'Match User X Group Y' is a conjunction the same way OpenSSH treats a
* Match line. A NULL selector acts as a wildcard. */
if (node->usrAppliesTo != NULL || node->groupAppliesTo != NULL) {
matches = 1;
if (node->usrAppliesTo != NULL) {
if (usr == NULL || XSTRCMP(node->usrAppliesTo, usr) != 0) {
matches = 0;
}
}
/* The group selector matches when it equals any group the user belongs
* to, primary or supplementary, mirroring how OpenSSH evaluates
* 'Match Group'. An empty group list matches no group selector, so
* combined blocks fail closed. */
if (matches && node->groupAppliesTo != NULL) {
matches = 0;
if (grps != NULL) {
for (i = 0; i < grpCount; i++) {
if (grps[i] == NULL)
continue;
if (XSTRCMP(node->groupAppliesTo, grps[i]) == 0) {
matches = 1;
break;
}
}
}
}
}
return matches;
}
/* returns the config associated with the user, or NULL on failure. The result
* is a new config the caller frees with wolfSSHD_ConfigFree(). */
WOLFSSHD_CONFIG* wolfSSHD_GetUserConf(const WOLFSSHD_CONFIG* conf,
const char* usr, const char** grps, word32 grpCount, const char* host,
const char* localAdr, word16* localPort, const char* RDomain,
const char* adr)
{
WOLFSSHD_CONFIG* ret;
WOLFSSHD_CONFIG* current;
int matches;
word32 i;
const WOLFSSHD_CONFIG* current;
int rc = WS_SUCCESS;
/* default to return head of list */
ret = current = (WOLFSSHD_CONFIG*)conf;
while (current != NULL) {
/* A node is a Match candidate only if it carries at least one
* selector. Every non-NULL selector on the node must match for the
* node to apply, so a combined 'Match User X Group Y' is treated as a
* conjunction the same way OpenSSH treats a Match line. A NULL
* selector acts as a wildcard. */
matches = 0;
if (current->usrAppliesTo != NULL || current->groupAppliesTo != NULL) {
matches = 1;
if (conf == NULL) {
return NULL;
}
if (current->usrAppliesTo != NULL) {
if (usr == NULL ||
XSTRCMP(current->usrAppliesTo, usr) != 0) {
matches = 0;
}
}
/* Start from the global values. They carry setMask 0, so any keyword a
* matching block sets outranks them. */
ret = wolfSSHD_ConfigCopy((WOLFSSHD_CONFIG*)conf);
if (ret == NULL) {
return NULL;
}
/* The group selector matches when it equals any group the user
* belongs to, primary or supplementary, mirroring how OpenSSH
* evaluates 'Match Group'. An empty group list matches no group
* selector, so combined blocks fail closed. */
if (matches && current->groupAppliesTo != NULL) {
matches = 0;
if (grps != NULL) {
for (i = 0; i < grpCount; i++) {
if (grps[i] == NULL)
continue;
if (XSTRCMP(current->groupAppliesTo, grps[i]) == 0) {
matches = 1;
break;
}
}
}
/* OpenSSH resolves one keyword at a time over every Match block that
* applies, "the first obtained value will be used". Walking the list in
* order and only taking keywords not already taken does the same, so a
* setting made in just one of several matching blocks still applies. */
for (current = conf; current != NULL; current = current->next) {
if (MatchApplies(current, usr, grps, grpCount)) {
rc = ConfigComposeFrom(ret, current);
if (rc != WS_SUCCESS) {
break;
}
}
}
if (matches) {
ret = current;
break;
}
current = current->next;
if (rc != WS_SUCCESS) {
wolfSSHD_ConfigFree(ret);
ret = NULL;
}
/* @TODO */

View File

@ -76,6 +76,11 @@ byte wolfSSHD_ConfigGetPrivilegeSeparation(const WOLFSSHD_CONFIG* conf);
long wolfSSHD_ConfigGetGraceTime(const WOLFSSHD_CONFIG* conf);
byte wolfSSHD_ConfigGetPwAuth(const WOLFSSHD_CONFIG* conf);
byte wolfSSHD_ConfigGetPubKeyAuth(const WOLFSSHD_CONFIG* conf);
/* Resolves the configuration for one connection. Every Match block that applies
* contributes, one keyword at a time, and the first block to name a keyword
* wins it; keywords no matching block names keep the global value. Returns a
* newly allocated config the caller frees with wolfSSHD_ConfigFree(), or NULL
* on failure. */
WOLFSSHD_CONFIG* wolfSSHD_GetUserConf(const WOLFSSHD_CONFIG* conf,
const char* usr, const char** grps, word32 grpCount, const char* host,
const char* localAdr, word16* localPort, const char* RDomain,

View File

@ -406,10 +406,7 @@ else
# these tests require setting up an sshd
if [ "$USING_LOCAL_HOST" == 1 ]; then
run_test "sshd_forcedcmd_test.sh"
# Left out of the suite on purpose: it asserts OpenSSH's per keyword
# Match composition, which wolfSSHD_GetUserConf does not implement yet,
# so it fails today. Add it back once that lands.
# run_test "sshd_match_overlap_test.sh"
run_test "sshd_match_overlap_test.sh"
run_test "sshd_window_full_test.sh"
run_test "sshd_empty_password_test.sh"
run_test "sshd_permitroot_test.sh"

View File

@ -9,9 +9,9 @@
# on which block is written first.
#
# This test writes the same two blocks in both orders. Only the group block sets
# ForceCommand, so a shell exec must be refused either way. wolfsshd resolves
# the whole first matching block instead of composing per keyword, so the
# user-first order currently drops the forced command and the exec succeeds.
# ForceCommand, so a shell exec must be refused either way. Resolving the whole
# first matching block instead of composing per keyword would drop the forced
# command in the user-first order and let the exec through.
#
# Expects ./authorized_keys_test to exist, as the other tests here do. Create it
# with ./create_authorized_test_file.sh when running this script on its own.

View File

@ -383,7 +383,7 @@ static int test_ConfigCopy(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* match = NULL;
head = wolfSSHD_ConfigNew(NULL);
if (head == NULL)
@ -425,11 +425,12 @@ static int test_ConfigCopy(void)
if (ret == WS_SUCCESS) ret = PCL("Match User testuser");
#undef PCL
/* retrieve match node from the list head */
/* resolve the match node; the Match block sets nothing of its own, so
* every global value must come through the copy */
if (ret == WS_SUCCESS) {
match = wolfSSHD_GetUserConf(head, "testuser", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL || match == head)
if (match == NULL)
ret = WS_FATAL_ERROR;
}
@ -524,14 +525,15 @@ static int test_ConfigCopy(void)
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
wolfSSHD_ConfigFree(head);
return ret;
}
/* 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.
/* Verifies a Match block override reaches the config wolfSSHD_GetUserConf
* resolves, and that the global node keeps its own permissive values;
* RequestAuthentication/DoCheckUser depend on this resolution for PwAuth,
* PermitEmptyPw, PermitRootLogin, and AuthKeysFileSet.
*
* Not covered here: the fail-closed NULL-config branches and the Match-aware
* PermitRootLogin modes (prohibit-password, forced-commands-only) need a
@ -543,8 +545,8 @@ static int test_GetUserConfMatchOverride(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* other;
WOLFSSHD_CONFIG* match = NULL;
WOLFSSHD_CONFIG* other = NULL;
head = wolfSSHD_ConfigNew(NULL);
if (head == NULL)
@ -578,11 +580,11 @@ static int test_GetUserConfMatchOverride(void)
ret = WS_FATAL_ERROR;
}
/* resolving testuser must return the per-user node, not the global head */
/* resolving testuser must pick up the per-user block */
if (ret == WS_SUCCESS) {
match = wolfSSHD_GetUserConf(head, "testuser", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL || match == head)
if (match == NULL)
ret = WS_FATAL_ERROR;
}
@ -606,15 +608,23 @@ static int test_GetUserConfMatchOverride(void)
ret = WS_FATAL_ERROR;
}
/* a user with no Match block must fall back to the permissive global head,
* confirming the default behavior is unchanged for non-Match users */
/* a user with no Match block must fall back to the permissive global
* values, confirming the default behavior is unchanged for non-Match
* users */
if (ret == WS_SUCCESS) {
other = wolfSSHD_GetUserConf(head, "otheruser", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (other != head)
if (other == NULL ||
wolfSSHD_ConfigGetPwAuth(other) != 1 ||
wolfSSHD_ConfigGetPubKeyAuth(other) != 1 ||
wolfSSHD_ConfigGetPermitEmptyPw(other) != 1 ||
wolfSSHD_ConfigGetPermitRoot(other) != 1 ||
wolfSSHD_ConfigGetAuthKeysFileSet(other) != 0)
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(other);
wolfSSHD_ConfigFree(match);
wolfSSHD_ConfigFree(head);
return ret;
}
@ -692,9 +702,59 @@ static int test_MatchUnsupportedSelector(void)
return ret;
}
/* Resolves 'usr' against 'head' and compares the resolved ForceCommand with
* 'expect', where NULL expects no forced command. wolfSSHD_GetUserConf composes
* a config the caller owns, so this frees it. Returns WS_SUCCESS on a match. */
static int CheckForcedCmd(WOLFSSHD_CONFIG* head, const char* usr,
const char** grps, word32 grpCount, const char* expect)
{
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* match;
const char* cmd;
match = wolfSSHD_GetUserConf(head, usr, grps, grpCount, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL) {
return WS_FATAL_ERROR;
}
cmd = wolfSSHD_ConfigGetForcedCmd(match);
if (expect == NULL) {
if (cmd != NULL)
ret = WS_FATAL_ERROR;
}
else if (cmd == NULL || XSTRCMP(cmd, expect) != 0) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
return ret;
}
/* Same as CheckForcedCmd for the resolved PasswordAuthentication setting. */
static int CheckPwAuth(WOLFSSHD_CONFIG* head, const char* usr,
const char** grps, word32 grpCount, int expect)
{
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* match;
match = wolfSSHD_GetUserConf(head, usr, grps, grpCount, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL) {
return WS_FATAL_ERROR;
}
if (wolfSSHD_ConfigGetPwAuth(match) != expect) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
return ret;
}
/* A combined 'Match User X Group Y' directive is a conjunction: it applies only
* to a user who satisfies BOTH selectors, matching OpenSSH semantics. This
* locks in that wolfSSHD_GetUserConf does not return such a block for a user
* locks in that wolfSSHD_GetUserConf does not apply such a block to a user
* who satisfies only one selector (the policy-bypass case), while single
* selector 'Match User' and 'Match Group' blocks keep applying on their one
* selector alone. */
@ -703,10 +763,6 @@ static int test_GetUserConfMatchGroupAnd(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* combined;
WOLFSSHD_CONFIG* userOnly;
WOLFSSHD_CONFIG* groupOnly;
WOLFSSHD_CONFIG* match;
const char* grps[1];
head = wolfSSHD_ConfigNew(NULL);
@ -722,74 +778,53 @@ static int test_GetUserConfMatchGroupAnd(void)
* 'alice' AND in group 'admins' */
if (ret == WS_SUCCESS) ret = PCL("Match User alice Group admins");
if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/sh");
if (ret == WS_SUCCESS) combined = conf;
/* single selector blocks must keep matching on their one selector */
if (ret == WS_SUCCESS) ret = PCL("Match User bob");
if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/bob");
if (ret == WS_SUCCESS) userOnly = conf;
if (ret == WS_SUCCESS) ret = PCL("Match Group staff");
if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/staff");
if (ret == WS_SUCCESS) groupOnly = conf;
#undef PCL
/* alice in admins satisfies both selectors -> gets the combined block */
if (ret == WS_SUCCESS) {
grps[0] = "admins";
match = wolfSSHD_GetUserConf(head, "alice", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != combined)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "alice", grps, 1, "/bin/sh");
}
/* alice in a different group satisfies only the user selector -> must NOT
* get the combined block; falls back to the restrictive global head */
* get the combined block; keeps the restrictive global value */
if (ret == WS_SUCCESS) {
grps[0] = "users";
match = wolfSSHD_GetUserConf(head, "alice", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != head)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "alice", grps, 1, "internal-sftp");
}
/* carol in admins satisfies only the group selector -> must NOT get the
* combined block; falls back to the restrictive global head */
* combined block; keeps the restrictive global value */
if (ret == WS_SUCCESS) {
grps[0] = "admins";
match = wolfSSHD_GetUserConf(head, "carol", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != head)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "carol", grps, 1, "internal-sftp");
}
/* alice with no resolved group must NOT get the combined block: an empty
* group set cannot satisfy the group selector, so it fails closed to the
* global head. This is the WIN32 / failed group-lookup path where
* global value. This is the WIN32 / failed group-lookup path where
* wolfSSHD_AuthGetUserConf passes an empty group list. */
if (ret == WS_SUCCESS) {
match = wolfSSHD_GetUserConf(head, "alice", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (match != head)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "alice", NULL, 0, "internal-sftp");
}
/* single selector 'Match User bob' still applies on the user alone */
if (ret == WS_SUCCESS) {
grps[0] = "anygroup";
match = wolfSSHD_GetUserConf(head, "bob", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != userOnly)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "bob", grps, 1, "/bin/bob");
}
/* single selector 'Match Group staff' still applies on the group alone */
if (ret == WS_SUCCESS) {
grps[0] = "staff";
match = wolfSSHD_GetUserConf(head, "anyuser", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != groupOnly)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "anyuser", grps, 1, "/bin/staff");
}
wolfSSHD_ConfigFree(head);
@ -806,9 +841,6 @@ static int test_GetUserConfMatchSecondaryGroup(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* blockWS;
WOLFSSHD_CONFIG* blockComb;
WOLFSSHD_CONFIG* match;
const char* grps[3];
head = wolfSSHD_ConfigNew(NULL);
@ -823,12 +855,10 @@ static int test_GetUserConfMatchSecondaryGroup(void)
/* single group selector */
if (ret == WS_SUCCESS) ret = PCL("Match Group wireshark");
if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/ws");
if (ret == WS_SUCCESS) blockWS = conf;
/* combined user AND group selector */
if (ret == WS_SUCCESS) ret = PCL("Match User john Group admins");
if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/adm");
if (ret == WS_SUCCESS) blockComb = conf;
#undef PCL
/* wireshark present only in a secondary slot must still select the block */
@ -836,49 +866,34 @@ static int test_GetUserConfMatchSecondaryGroup(void)
grps[0] = "alice";
grps[1] = "staff";
grps[2] = "wireshark";
match = wolfSSHD_GetUserConf(head, "alice", grps, 3, NULL, NULL,
NULL, NULL, NULL);
if (match != blockWS)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "alice", grps, 3, "/bin/ws");
}
/* combined block satisfied with the group in a secondary slot */
if (ret == WS_SUCCESS) {
grps[0] = "john";
grps[1] = "admins";
match = wolfSSHD_GetUserConf(head, "john", grps, 2, NULL, NULL,
NULL, NULL, NULL);
if (match != blockComb)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "john", grps, 2, "/bin/adm");
}
/* combined block: user matches but group absent -> falls back to head */
/* combined block: user matches but group absent -> keeps the global */
if (ret == WS_SUCCESS) {
grps[0] = "john";
grps[1] = "users";
match = wolfSSHD_GetUserConf(head, "john", grps, 2, NULL, NULL,
NULL, NULL, NULL);
if (match != head)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "john", grps, 2, "internal-sftp");
}
/* combined block: group matches but user differs -> falls back to head */
/* combined block: group matches but user differs -> keeps the global */
if (ret == WS_SUCCESS) {
grps[0] = "bob";
grps[1] = "admins";
match = wolfSSHD_GetUserConf(head, "bob", grps, 2, NULL, NULL,
NULL, NULL, NULL);
if (match != head)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "bob", grps, 2, "internal-sftp");
}
/* user in none of the selector groups -> falls back to head */
/* user in none of the selector groups -> keeps the global */
if (ret == WS_SUCCESS) {
grps[0] = "carol";
match = wolfSSHD_GetUserConf(head, "carol", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != head)
ret = WS_FATAL_ERROR;
ret = CheckForcedCmd(head, "carol", grps, 1, "internal-sftp");
}
wolfSSHD_ConfigFree(head);
@ -897,8 +912,6 @@ static int test_GetUserConfMatchSubstring(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* ghost;
const char* grps[1];
head = wolfSSHD_ConfigNew(NULL);
@ -914,26 +927,16 @@ static int test_GetUserConfMatchSubstring(void)
if (ret == WS_SUCCESS) ret = PCL("PasswordAuthentication yes");
#undef PCL
/* lookup by the real user name must resolve to the Match node */
/* lookup by the real user name must pick up the Match block */
if (ret == WS_SUCCESS) {
match = wolfSSHD_GetUserConf(head, "GroupAdmin", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL || match == head)
ret = WS_FATAL_ERROR;
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(match) != 1)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, "GroupAdmin", NULL, 0, 1);
}
/* lookup by the ghost group token ("Admin") must NOT match; it falls back
* to the permissive-denied global head */
/* lookup by the ghost group token ("Admin") must NOT match; it keeps the
* permissive-denied global value */
if (ret == WS_SUCCESS) {
grps[0] = "Admin";
ghost = wolfSSHD_GetUserConf(head, NULL, grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (ghost != head)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, NULL, grps, 1, 0);
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(head) != 0)
@ -955,8 +958,6 @@ static int test_GetUserConfMatchSubstringGroup(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* ghost;
const char* grps[1];
head = wolfSSHD_ConfigNew(NULL);
@ -972,26 +973,16 @@ static int test_GetUserConfMatchSubstringGroup(void)
if (ret == WS_SUCCESS) ret = PCL("PasswordAuthentication yes");
#undef PCL
/* lookup by the real group name must resolve to the Match node */
/* lookup by the real group name must pick up the Match block */
if (ret == WS_SUCCESS) {
grps[0] = "UserStaff";
match = wolfSSHD_GetUserConf(head, NULL, grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL || match == head)
ret = WS_FATAL_ERROR;
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(match) != 1)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, NULL, grps, 1, 1);
}
/* lookup by the ghost user token ("Staff") must NOT match; it falls back
* to the permissive-denied global head */
/* lookup by the ghost user token ("Staff") must NOT match; it keeps the
* permissive-denied global value */
if (ret == WS_SUCCESS) {
ghost = wolfSSHD_GetUserConf(head, "Staff", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (ghost != head)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, "Staff", NULL, 0, 0);
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(head) != 0)
@ -1016,8 +1007,6 @@ static int test_GetUserConfMatchLiteralKeywordName(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* ghost;
const char* grps[1];
head = wolfSSHD_ConfigNew(NULL);
@ -1033,26 +1022,16 @@ static int test_GetUserConfMatchLiteralKeywordName(void)
if (ret == WS_SUCCESS) ret = PCL("PasswordAuthentication yes");
#undef PCL
/* lookup by the user name "Group" must resolve to the Match node */
/* lookup by the user name "Group" must pick up the Match block */
if (ret == WS_SUCCESS) {
match = wolfSSHD_GetUserConf(head, "Group", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL || match == head)
ret = WS_FATAL_ERROR;
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(match) != 1)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, "Group", NULL, 0, 1);
}
/* the user name must NOT have leaked into groupAppliesTo: a lookup by group
* "Group" must fall back to the permissive-denied global head */
* "Group" must keep the permissive-denied global value */
if (ret == WS_SUCCESS) {
grps[0] = "Group";
ghost = wolfSSHD_GetUserConf(head, NULL, grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (ghost != head)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, NULL, grps, 1, 0);
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(head) != 0)
@ -1111,8 +1090,6 @@ static int test_GetUserConfMatchRepeatedKeyword(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* old;
head = wolfSSHD_ConfigNew(NULL);
if (head == NULL)
@ -1127,21 +1104,14 @@ static int test_GetUserConfMatchRepeatedKeyword(void)
if (ret == WS_SUCCESS) ret = PCL("PasswordAuthentication yes");
#undef PCL
/* lookup by the replacement name "b" must resolve to the Match node */
/* lookup by the replacement name "b" must pick up the Match block */
if (ret == WS_SUCCESS) {
match = wolfSSHD_GetUserConf(head, "b", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (match == NULL || match == head ||
wolfSSHD_ConfigGetPwAuth(match) != 1)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, "b", NULL, 0, 1);
}
/* lookup by the replaced name "a" must NOT match; it falls back to head */
/* lookup by the replaced name "a" must NOT match; it keeps the global */
if (ret == WS_SUCCESS) {
old = wolfSSHD_GetUserConf(head, "a", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
if (old != head)
ret = WS_FATAL_ERROR;
ret = CheckPwAuth(head, "a", NULL, 0, 0);
}
if (ret == WS_SUCCESS) {
if (wolfSSHD_ConfigGetPwAuth(head) != 0)
@ -1248,8 +1218,6 @@ static int test_GetUserConfMatchNoInherit(void)
int ret = WS_SUCCESS;
WOLFSSHD_CONFIG* head;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* aliceConf = NULL;
WOLFSSHD_CONFIG* staffConf = NULL;
WOLFSSHD_CONFIG* match = NULL;
const char* cmd;
const char* grps[1];
@ -1268,13 +1236,11 @@ static int test_GetUserConfMatchNoInherit(void)
if (ret == WS_SUCCESS) ret = PCL("ForceCommand /bin/alice");
if (ret == WS_SUCCESS) ret = PCL("AuthorizedKeysFile .ssh/alice_keys");
if (ret == WS_SUCCESS) ret = PCL("PermitEmptyPasswords no");
if (ret == WS_SUCCESS) aliceConf = conf;
/* the staff block sets one option, everything else must resolve to the
* global value rather than to alice's */
if (ret == WS_SUCCESS) ret = PCL("Match Group staff");
if (ret == WS_SUCCESS) ret = PCL("PubkeyAuthentication no");
if (ret == WS_SUCCESS) staffConf = conf;
#undef PCL
if (ret == WS_SUCCESS) {
@ -1282,7 +1248,7 @@ static int test_GetUserConfMatchNoInherit(void)
grps[0] = "staff";
match = wolfSSHD_GetUserConf(head, "bob", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != staffConf)
if (match == NULL)
ret = WS_FATAL_ERROR;
if (ret == WS_SUCCESS) {
@ -1302,6 +1268,8 @@ static int test_GetUserConfMatchNoInherit(void)
if (ret == WS_SUCCESS && wolfSSHD_ConfigGetPubKeyAuth(match) != 0) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
if (ret == WS_SUCCESS) {
Log(" PASSED.\n");
}
@ -1315,7 +1283,7 @@ static int test_GetUserConfMatchNoInherit(void)
grps[0] = "users";
match = wolfSSHD_GetUserConf(head, "alice", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != aliceConf)
if (match == NULL)
ret = WS_FATAL_ERROR;
if (ret == WS_SUCCESS) {
@ -1335,6 +1303,8 @@ static int test_GetUserConfMatchNoInherit(void)
if (ret == WS_SUCCESS && wolfSSHD_ConfigGetPubKeyAuth(match) != 1) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
if (ret == WS_SUCCESS) {
Log(" PASSED.\n");
}
@ -1343,6 +1313,7 @@ static int test_GetUserConfMatchNoInherit(void)
}
}
wolfSSHD_ConfigFree(match);
wolfSSHD_ConfigFree(head);
return ret;
}
@ -1403,7 +1374,7 @@ static int test_GetUserConfMatchOverlapCompose(void)
int fail = WS_SUCCESS;
WOLFSSHD_CONFIG* head = NULL;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* match = NULL;
const char* keys;
const char* cmd;
const char* grps[1];
@ -1438,6 +1409,8 @@ static int test_GetUserConfMatchOverlapCompose(void)
ret = WS_FATAL_ERROR;
}
}
wolfSSHD_ConfigFree(match);
match = NULL;
}
Log((ret == WS_SUCCESS) ? " PASSED.\n" : " FAILED.\n");
wolfSSHD_ConfigFree(head);
@ -1476,6 +1449,8 @@ static int test_GetUserConfMatchOverlapCompose(void)
ret = WS_FATAL_ERROR;
}
}
wolfSSHD_ConfigFree(match);
match = NULL;
}
Log((ret == WS_SUCCESS) ? " PASSED.\n" : " FAILED.\n");
wolfSSHD_ConfigFree(head);
@ -1509,16 +1484,22 @@ static int test_GetUserConfMatchOverlapCompose(void)
if (keys == NULL || XSTRCMP(keys, ".ssh/alice_keys") != 0)
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
}
/* and a user who matches neither keeps the global values */
/* and a user who matches neither keeps the global values, with none of
* alice's settings leaking across */
if (ret == WS_SUCCESS) {
grps[0] = "users";
match = wolfSSHD_GetUserConf(head, "bob", grps, 1, NULL, NULL,
NULL, NULL, NULL);
if (match != head || wolfSSHD_ConfigGetPwAuth(match) != 1) {
if (match == NULL || wolfSSHD_ConfigGetPwAuth(match) != 1 ||
wolfSSHD_ConfigGetAuthKeysFile(match) != NULL) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
}
Log((ret == WS_SUCCESS) ? " PASSED.\n" : " FAILED.\n");
wolfSSHD_ConfigFree(head);
@ -1563,6 +1544,8 @@ static int test_GetUserConfMatchOverlapCompose(void)
ret = WS_FATAL_ERROR;
}
}
wolfSSHD_ConfigFree(match);
match = NULL;
}
Log((ret == WS_SUCCESS) ? " PASSED.\n" : " FAILED.\n");
wolfSSHD_ConfigFree(head);
@ -1605,6 +1588,8 @@ static int test_GetUserConfMatchOverlapCompose(void)
if (cmd == NULL || XSTRCMP(cmd, "/bin/alice") != 0)
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
}
Log((ret == WS_SUCCESS) ? " PASSED.\n" : " FAILED.\n");
wolfSSHD_ConfigFree(head);
@ -1629,7 +1614,7 @@ static int test_ConfigIncludeMatchChain(void)
{
int ret;
WOLFSSHD_CONFIG* head = NULL;
WOLFSSHD_CONFIG* match;
WOLFSSHD_CONFIG* match = NULL;
const char* cmd;
const char* incPath = "./include_match.conf";
const char* topPath = "./include_match_top.conf";
@ -1667,10 +1652,12 @@ static int test_ConfigIncludeMatchChain(void)
match = wolfSSHD_GetUserConf(head, "alice", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
cmd = wolfSSHD_ConfigGetForcedCmd(match);
if (match == head || cmd == NULL ||
if (match == NULL || cmd == NULL ||
XSTRCMP(cmd, "/bin/alice") != 0) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
if (ret == WS_SUCCESS) {
Log(" PASSED.\n");
}
@ -1684,9 +1671,11 @@ static int test_ConfigIncludeMatchChain(void)
match = wolfSSHD_GetUserConf(head, "bob", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
cmd = wolfSSHD_ConfigGetForcedCmd(match);
if (match == head || cmd == NULL || XSTRCMP(cmd, "/bin/bob") != 0) {
if (match == NULL || cmd == NULL || XSTRCMP(cmd, "/bin/bob") != 0) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
if (ret == WS_SUCCESS) {
Log(" PASSED.\n");
}
@ -1700,7 +1689,7 @@ static int test_ConfigIncludeMatchChain(void)
match = wolfSSHD_GetUserConf(head, "carol", NULL, 0, NULL, NULL,
NULL, NULL, NULL);
cmd = wolfSSHD_ConfigGetForcedCmd(match);
if (match != head || cmd == NULL ||
if (match == NULL || cmd == NULL ||
XSTRCMP(cmd, "/bin/global") != 0) {
ret = WS_FATAL_ERROR;
}
@ -1710,6 +1699,8 @@ static int test_ConfigIncludeMatchChain(void)
wolfSSHD_ConfigGetPermitEmptyPw(match) != 1) {
ret = WS_FATAL_ERROR;
}
wolfSSHD_ConfigFree(match);
match = NULL;
if (ret == WS_SUCCESS) {
Log(" PASSED.\n");
}
@ -1718,6 +1709,7 @@ static int test_ConfigIncludeMatchChain(void)
}
}
wolfSSHD_ConfigFree(match);
wolfSSHD_ConfigFree(head);
(void)WREMOVE(NULL, incPath);
(void)WREMOVE(NULL, topPath);
@ -6530,6 +6522,7 @@ const TEST_CASE testCases[] = {
TEST_DECL(test_MatchUPNToUser),
TEST_DECL(test_IncludeRecursionBound),
TEST_DECL(test_GetUserConfMatchNoInherit),
TEST_DECL(test_GetUserConfMatchOverlapCompose),
TEST_DECL(test_ConfigIncludeMatchChain),
TEST_DECL(test_GetUserAuthTypes),
TEST_DECL(test_DefaultUserAuthTypesNullArgs),
@ -6612,11 +6605,6 @@ const TEST_CASE testCases[] = {
TEST_DECL(test_CheckPublicKeyUnixOrdering),
#endif
#endif
/* Runs last: the first failing case stops the run, and this one is a known
* gap. wolfSSHD_GetUserConf returns the first matching Match block whole
* instead of composing per keyword, so a setting made only in a later
* matching block is dropped. */
TEST_DECL(test_GetUserConfMatchOverlapCompose),
};
int main(int argc, char** argv)

View File

@ -2584,6 +2584,8 @@ static void* HandleConnection(void* arg)
ret = WS_NOT_COMPILED;
}
}
wolfSSHD_ConfigFree(usrConf);
}
error = wolfSSH_get_error(ssh);