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 apps/wolfssh-options.c
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ int main(void)
#endif
#ifdef WOLFSSH_ALLOW_NONE_CIPHER
printf("NONE_CIPHER\n");
#endif
/* Same guard as WOLFSSHD_HOSTKEY_RELAX_PERMS in wolfsshd.c; keep the two in
* step. The sshd tests use this to skip the host key negative cases. */
#if defined(WOLFSSH_NO_HOSTKEY_PERMS) && \
(defined(__QNX__) || defined(__QNXNTO__))
printf("HOSTKEY_RELAX_PERMS\n");
#endif
/* Same guard as wIsSymlink in port.h. */
#if defined(WOLFSSH_HAVE_SYMLINK) && \
Expand Down
25 changes: 17 additions & 8 deletions apps/wolfsshd/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,8 @@ void wolfSSHD_AuthInit(void)
#ifndef WOLFSSHD_UNIT_TEST
/* /etc/shadow is commonly root:shadow 0640; don't reject group-readable. */
if (wolfSSHD_OpenSecureFile(WSSHD_SHADOW_FILE, 0 /* ownerUid: root */,
0 /* rejectReadable */, NULL, &f) == WS_SUCCESS && f != NULL) {
0 /* rejectReadable */, 0 /* relaxPerms */, NULL, &f)
== WS_SUCCESS && f != NULL) {
ScanShadowFile(f);
WFCLOSE(NULL, f);
}
Expand Down Expand Up @@ -1069,14 +1070,19 @@ WOLFSSHD_STATIC int ResolveAuthKeysPath(const char* homeDir,
* service account's tree).
* rejectReadable - when set, also refuse a file that is group or world
* readable. Used for secrets such as the host private key.
* relaxPerms - skip the ownership, mode and directory checks, ignoring
* ownerUid and rejectReadable. A symlinked leaf, non-regular
* file or swap during the open is still refused, but a
* writable ancestor can substitute the file. Host key only,
* see WOLFSSHD_HOSTKEY_RELAX_PERMS in wolfsshd.c.
* heap - heap hint for the temporary path buffer.
* out - set to the open stream on success, WBADFILE otherwise.
*
* Returns WS_SUCCESS and sets *out on success; a specific reason is logged on
* failure. On platforms without POSIX ownership semantics (_WIN32) the checks
* are skipped and the file is opened directly, relying on filesystem ACLs. */
int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
int rejectReadable, void* heap, WFILE** out)
int rejectReadable, int relaxPerms, void* heap, WFILE** out)
{
#ifndef _WIN32
int ret = WS_SUCCESS;
Expand Down Expand Up @@ -1149,18 +1155,19 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
"[SSHD] Refusing to load (not a regular file): %s", path);
ret = WS_BAD_FILE_E;
}
else if (st.st_uid != ownerUid && st.st_uid != 0) {
else if (!relaxPerms && st.st_uid != ownerUid && st.st_uid != 0) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Refusing to load (not owned by the user or root): %s",
path);
ret = WS_BAD_FILE_E;
}
else if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
else if (!relaxPerms && (st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Refusing to load (group or world writable): %s", path);
ret = WS_BAD_FILE_E;
}
else if (rejectReadable && (st.st_mode & (S_IRGRP | S_IROTH)) != 0) {
else if (!relaxPerms && rejectReadable &&
(st.st_mode & (S_IRGRP | S_IROTH)) != 0) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Refusing to load (group or world readable): %s", path);
ret = WS_BAD_FILE_E;
Expand All @@ -1180,8 +1187,8 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
* owned by a third party from being loaded. Since realpath() resolved all
* intermediate symlinks, this is the same chain open() traversed. The walk
* trims components from 'resolved' in place, which is fine now that the file
* is already open. */
while (ret == WS_SUCCESS) {
* is already open. Skipped under relaxPerms. */
while (ret == WS_SUCCESS && !relaxPerms) {
/* trim the last component to move up one directory */
slash = NULL;
for (i = 0; resolved[i] != '\0'; i++) {
Expand Down Expand Up @@ -1256,6 +1263,7 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
#else
WOLFSSH_UNUSED(ownerUid);
WOLFSSH_UNUSED(rejectReadable);
WOLFSSH_UNUSED(relaxPerms);
WOLFSSH_UNUSED(heap);

if (path == NULL || out == NULL) {
Expand Down Expand Up @@ -1293,7 +1301,8 @@ static int SearchKeysFile(const char* keysFilePath, const byte* key,
* Otherwise fall back to a plain open. */
if (strictModes) {
if (wolfSSHD_OpenSecureFile(keysFilePath, uid,
0 /* rejectReadable */, NULL, &f) != WS_SUCCESS) {
0 /* rejectReadable */, 0 /* relaxPerms */, NULL, &f)
!= WS_SUCCESS) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Keys file failed StrictModes check: %s", keysFilePath);
ret = WSSHD_AUTH_FAILURE;
Expand Down
2 changes: 1 addition & 1 deletion apps/wolfsshd/auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int
* and the trust-anchor loads in wolfsshd.c (host key, host cert, user CA keys).
* See the definition in auth.c for the meaning of each argument. */
int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
int rejectReadable, void* heap, WFILE** out);
int rejectReadable, int relaxPerms, void* heap, WFILE** out);

/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER.
* *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer to WS_FORCEZERO +
Expand Down
18 changes: 18 additions & 0 deletions apps/wolfsshd/test/run_all_sshd_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ run_test() {
# separation is off and a high port is used, so no root is needed.
run_strictmodes_negative_test() {
printf "Host key trust-anchor negative test ... "
# WOLFSSH_NO_HOSTKEY_PERMS hands the mode to the platform, so the readable
# key loads and there is nothing to assert.
if wolfssh_has HOSTKEY_RELAX_PERMS; then
TOTAL=$((TOTAL+1))
SKIPPED=$((SKIPPED+1))
printf "SKIPPED (built with WOLFSSH_NO_HOSTKEY_PERMS)\n"
return
fi
# A local copy of the host key, made group/world readable.
cp ../../../keys/server-key.pem strictmodes_hostkey.pem
chmod 644 strictmodes_hostkey.pem
Expand Down Expand Up @@ -341,6 +349,16 @@ EOF
grep -q "Refusing to load" "$HK_WORK/log.txt" || hk_fail "FIFO host key was not refused"
fi

# The mode and owner cases below are the ones WOLFSSH_NO_HOSTKEY_PERMS hands
# to the platform, so on such a build the key loads and the assertions would
# invert. Skip them there; the symlink and FIFO cases above still hold.
if wolfssh_has HOSTKEY_RELAX_PERMS; then
rm -rf "$HK_WORK"
printf "PASSED (mode and owner cases skipped, built with "
printf "WOLFSSH_NO_HOSTKEY_PERMS)\n"
return
fi

# group/world-writable file must be refused
cp "$HK_KEY" "$HK_WORK/ww.pem"; chmod 666 "$HK_WORK/ww.pem"
hk_cfg "$HK_WORK/ww.pem"; hk_run
Expand Down
48 changes: 46 additions & 2 deletions apps/wolfsshd/test/test_configuration.c
Original file line number Diff line number Diff line change
Expand Up @@ -4640,10 +4640,12 @@ static int smChmod(const char* path, mode_t mode)

/* open 'path' through the secure gate and immediately close it, returning the
* gate's verdict so a scenario can assert acceptance or rejection */
static int smOpen(const char* path, WUID_T ownerUid, int rejectReadable)
static int smOpenEx(const char* path, WUID_T ownerUid, int rejectReadable,
int relaxPerms)
{
WFILE* f = WBADFILE;
int ret = wolfSSHD_OpenSecureFile(path, ownerUid, rejectReadable, NULL, &f);
int ret = wolfSSHD_OpenSecureFile(path, ownerUid, rejectReadable,
relaxPerms, NULL, &f);

if (ret == WS_SUCCESS && f != WBADFILE) {
/* read-only handle; a close failure has no bearing on the verdict */
Expand All @@ -4652,6 +4654,11 @@ static int smOpen(const char* path, WUID_T ownerUid, int rejectReadable)
return ret;
}

static int smOpen(const char* path, WUID_T ownerUid, int rejectReadable)
{
return smOpenEx(path, ownerUid, rejectReadable, 0 /* relaxPerms */);
}

static int test_OpenSecureFile(void)
{
int ret = WS_SUCCESS;
Expand Down Expand Up @@ -4817,6 +4824,43 @@ static int test_OpenSecureFile(void)
ret = smExpect("NULL path rejected", smOpen(NULL, uid, 0), 0);
}

/* relaxPerms: ownership, modes and directories are the platform's, but the
* structural checks still hold. Only wolfsshd's host key load sets this,
* and only where WOLFSSHD_HOSTKEY_RELAX_PERMS is on. */
if (ret == WS_SUCCESS)
ret = smChmod(hostkey, 0666);
if (ret == WS_SUCCESS) {
ret = smExpect("relaxed: world-writable host key accepted",
smOpenEx(hostkey, uid, 1, 1), 1);
}
/* No uid != 0 guard: relaxPerms ignores ownerUid, so this holds as root. */
if (ret == WS_SUCCESS) {
ret = smExpect("relaxed: wrong owner accepted",
smOpenEx(hostkey, uid + 1, 1, 1), 1);
}
if (ret == WS_SUCCESS)
ret = smChmod(hostkey, 0600);
if (ret == WS_SUCCESS)
ret = smChmod(wopen, 0777);
if (ret == WS_SUCCESS) {
ret = smExpect("relaxed: world-writable ancestor accepted",
smOpenEx(wopenKeys, uid, 1, 1), 1);
}
if (ret == WS_SUCCESS)
ret = smChmod(wopen, 0700);
if (ret == WS_SUCCESS) {
ret = smExpect("relaxed: symlinked leaf still rejected",
smOpenEx(linkKeys, uid, 1, 1), 0);
}
if (ret == WS_SUCCESS) {
ret = smExpect("relaxed: directory target still rejected",
smOpenEx(ssh, uid, 1, 1), 0);
}
if (ret == WS_SUCCESS) {
ret = smExpect("relaxed: missing file still rejected",
smOpenEx("/tmp/wolfsshd_sm_dne_xyz", uid, 1, 1), 0);
}

/* cleanup */
unlink(linkKeys);
unlink(keys);
Expand Down
31 changes: 28 additions & 3 deletions apps/wolfsshd/wolfsshd.c
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,18 @@ static void wolfSSHDLoggingCb(enum wolfSSH_LogLevel lvl, const char *const str)
}


/* QNX fixes the host key's owner and modes in the system image, so
* WOLFSSH_NO_HOSTKEY_PERMS hands that policy to the platform. Host key only, and
* only ownership, modes and directories. Kept outside the NO_FILESYSTEM guard
* below because SetupCTX() tests it and -Wundef is on. wolfssh-options.c repeats
* this guard so the tests can skip the negative cases; keep the two in step. */
#if defined(WOLFSSH_NO_HOSTKEY_PERMS) && \
(defined(__QNX__) || defined(__QNXNTO__))
#define WOLFSSHD_HOSTKEY_RELAX_PERMS 1
#else
#define WOLFSSHD_HOSTKEY_RELAX_PERMS 0
#endif

#ifndef NO_FILESYSTEM
static void freeBufferFromFile(byte* buf, void* heap)
{
Expand Down Expand Up @@ -260,11 +272,18 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap,
byte* buf = NULL;
long fileSz;
word32 readSz;
int relaxPerms = 0;

WOLFSSH_UNUSED(heap);

if (fileName == NULL) return NULL;

#if WOLFSSHD_HOSTKEY_RELAX_PERMS
if (loadClass == WOLFSSHD_LOAD_SECRET) {
relaxPerms = 1;
}
#endif

if (loadClass == WOLFSSHD_LOAD_NORMAL) {
if (WFOPEN(NULL, &file, fileName, "rb") != 0)
return NULL;
Expand All @@ -273,15 +292,15 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap,
/* Trust anchors always go through the secure gate, regardless of
* StrictModes. The owner is the daemon's effective user (or root), and
* the host private key (SECRET) is also refused if group/world
* readable. */
* readable, unless WOLFSSHD_HOSTKEY_RELAX_PERMS. */
if (wolfSSHD_OpenSecureFile(fileName,
#ifndef _WIN32
geteuid(),
#else
0,
#endif
loadClass == WOLFSSHD_LOAD_SECRET /* rejectReadable */,
heap, &file) != WS_SUCCESS) {
relaxPerms, heap, &file) != WS_SUCCESS) {
return NULL;
}
}
Expand Down Expand Up @@ -395,7 +414,13 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx,

/* The host private key is a secret trust anchor: refuse a symlink,
* an unsafe owner or path, or a group/world readable/writable
* file. */
* file. WOLFSSHD_HOSTKEY_RELAX_PERMS keeps only the symlink and
* regular-file checks. */
#if WOLFSSHD_HOSTKEY_RELAX_PERMS
wolfSSH_Log(WS_LOG_INFO, "[SSHD] Built with "
"WOLFSSH_NO_HOSTKEY_PERMS, host key ownership and permissions "
"are left to the platform");
#endif
data = getBufferFromFile(hostKey, &dataSz, heap,
WOLFSSHD_LOAD_SECRET);
if (data == NULL) {
Expand Down
Loading