diff --git a/apps/wolfssh-options.c b/apps/wolfssh-options.c index 2f91713a9..926abcd6c 100644 --- a/apps/wolfssh-options.c +++ b/apps/wolfssh-options.c @@ -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) && \ diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 1b7a74155..86093547b 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -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); } @@ -1069,6 +1070,11 @@ 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. * @@ -1076,7 +1082,7 @@ WOLFSSHD_STATIC int ResolveAuthKeysPath(const char* homeDir, * 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; @@ -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; @@ -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++) { @@ -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) { @@ -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; diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 637cdda7b..16d30c86f 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -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 + diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 65cc89aa9..46f0c8aeb 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -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 @@ -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 diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 326843663..c41cefcab 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -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 */ @@ -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; @@ -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); diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index a294661af..d75df1916 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -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) { @@ -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; @@ -273,7 +292,7 @@ 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(), @@ -281,7 +300,7 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap, 0, #endif loadClass == WOLFSSHD_LOAD_SECRET /* rejectReadable */, - heap, &file) != WS_SUCCESS) { + relaxPerms, heap, &file) != WS_SUCCESS) { return NULL; } } @@ -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) {