Skip to content

Commit f494688

Browse files
ejohnstownJacobBarthelmeh
authored andcommitted
wolfsshd: let QNX own the host key's permissions
QNX system images fix the host key's owner and modes, and the daemon cannot change either, so the secure gate refuses to load a key the integrator has no way to correct. Add a hand-defined WOLFSSH_NO_HOSTKEY_PERMS, further conditional on QNX, that hands only that policy to the platform. - Fold the macro and the QNX test into the internal WOLFSSHD_HOSTKEY_RELAX_PERMS in wolfsshd.c. - Add a relaxPerms argument to wolfSSHD_OpenSecureFile() that skips the owner, mode and ancestor-directory checks. - Keep the structural checks: lstat, O_NOFOLLOW, S_ISREG and the dev/ino recheck, so a symlink, a non-regular file or a swap during the open is still refused. - Set it only on the host key load, leaving the host cert, UserCAKeysFile, authorized_keys and shadow gates unchanged. - Log at startup when the guard is built in. - Add six test_OpenSecureFile scenarios for the relaxed path, unreachable at runtime off QNX and so otherwise uncovered. Issue: ZD-22308
1 parent 44bd4a0 commit f494688

6 files changed

Lines changed: 116 additions & 14 deletions

File tree

apps/wolfssh-options.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ int main(void)
109109
#endif
110110
#ifdef WOLFSSH_ALLOW_NONE_CIPHER
111111
printf("NONE_CIPHER\n");
112+
#endif
113+
/* Same guard as WOLFSSHD_HOSTKEY_RELAX_PERMS in wolfsshd.c; keep the two in
114+
* step. The sshd tests use this to skip the host key negative cases. */
115+
#if defined(WOLFSSH_NO_HOSTKEY_PERMS) && \
116+
(defined(__QNX__) || defined(__QNXNTO__))
117+
printf("HOSTKEY_RELAX_PERMS\n");
112118
#endif
113119
/* Same guard as wIsSymlink in port.h. */
114120
#if defined(WOLFSSH_HAVE_SYMLINK) && \

apps/wolfsshd/auth.c

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,8 @@ void wolfSSHD_AuthInit(void)
596596
#ifndef WOLFSSHD_UNIT_TEST
597597
/* /etc/shadow is commonly root:shadow 0640; don't reject group-readable. */
598598
if (wolfSSHD_OpenSecureFile(WSSHD_SHADOW_FILE, 0 /* ownerUid: root */,
599-
0 /* rejectReadable */, NULL, &f) == WS_SUCCESS && f != NULL) {
599+
0 /* rejectReadable */, 0 /* relaxPerms */, NULL, &f)
600+
== WS_SUCCESS && f != NULL) {
600601
ScanShadowFile(f);
601602
WFCLOSE(NULL, f);
602603
}
@@ -1122,14 +1123,19 @@ WOLFSSHD_STATIC int ResolveAuthKeysPath(const char* homeDir,
11221123
* service account's tree).
11231124
* rejectReadable - when set, also refuse a file that is group or world
11241125
* readable. Used for secrets such as the host private key.
1126+
* relaxPerms - skip the ownership, mode and directory checks, ignoring
1127+
* ownerUid and rejectReadable. A symlinked leaf, non-regular
1128+
* file or swap during the open is still refused, but a
1129+
* writable ancestor can substitute the file. Host key only,
1130+
* see WOLFSSHD_HOSTKEY_RELAX_PERMS in wolfsshd.c.
11251131
* heap - heap hint for the temporary path buffer.
11261132
* out - set to the open stream on success, WBADFILE otherwise.
11271133
*
11281134
* Returns WS_SUCCESS and sets *out on success; a specific reason is logged on
11291135
* failure. On platforms without POSIX ownership semantics (_WIN32) the checks
11301136
* are skipped and the file is opened directly, relying on filesystem ACLs. */
11311137
int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
1132-
int rejectReadable, void* heap, WFILE** out)
1138+
int rejectReadable, int relaxPerms, void* heap, WFILE** out)
11331139
{
11341140
#ifndef _WIN32
11351141
int ret = WS_SUCCESS;
@@ -1202,18 +1208,19 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
12021208
"[SSHD] Refusing to load (not a regular file): %s", path);
12031209
ret = WS_BAD_FILE_E;
12041210
}
1205-
else if (st.st_uid != ownerUid && st.st_uid != 0) {
1211+
else if (!relaxPerms && st.st_uid != ownerUid && st.st_uid != 0) {
12061212
wolfSSH_Log(WS_LOG_ERROR,
12071213
"[SSHD] Refusing to load (not owned by the user or root): %s",
12081214
path);
12091215
ret = WS_BAD_FILE_E;
12101216
}
1211-
else if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
1217+
else if (!relaxPerms && (st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
12121218
wolfSSH_Log(WS_LOG_ERROR,
12131219
"[SSHD] Refusing to load (group or world writable): %s", path);
12141220
ret = WS_BAD_FILE_E;
12151221
}
1216-
else if (rejectReadable && (st.st_mode & (S_IRGRP | S_IROTH)) != 0) {
1222+
else if (!relaxPerms && rejectReadable &&
1223+
(st.st_mode & (S_IRGRP | S_IROTH)) != 0) {
12171224
wolfSSH_Log(WS_LOG_ERROR,
12181225
"[SSHD] Refusing to load (group or world readable): %s", path);
12191226
ret = WS_BAD_FILE_E;
@@ -1233,8 +1240,8 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
12331240
* owned by a third party from being loaded. Since realpath() resolved all
12341241
* intermediate symlinks, this is the same chain open() traversed. The walk
12351242
* trims components from 'resolved' in place, which is fine now that the file
1236-
* is already open. */
1237-
while (ret == WS_SUCCESS) {
1243+
* is already open. Skipped under relaxPerms. */
1244+
while (ret == WS_SUCCESS && !relaxPerms) {
12381245
/* trim the last component to move up one directory */
12391246
slash = NULL;
12401247
for (i = 0; resolved[i] != '\0'; i++) {
@@ -1309,6 +1316,7 @@ int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
13091316
#else
13101317
WOLFSSH_UNUSED(ownerUid);
13111318
WOLFSSH_UNUSED(rejectReadable);
1319+
WOLFSSH_UNUSED(relaxPerms);
13121320
WOLFSSH_UNUSED(heap);
13131321

13141322
if (path == NULL || out == NULL) {
@@ -1346,7 +1354,8 @@ static int SearchKeysFile(const char* keysFilePath, const byte* key,
13461354
* Otherwise fall back to a plain open. */
13471355
if (strictModes) {
13481356
if (wolfSSHD_OpenSecureFile(keysFilePath, uid,
1349-
0 /* rejectReadable */, NULL, &f) != WS_SUCCESS) {
1357+
0 /* rejectReadable */, 0 /* relaxPerms */, NULL, &f)
1358+
!= WS_SUCCESS) {
13501359
wolfSSH_Log(WS_LOG_ERROR,
13511360
"[SSHD] Keys file failed StrictModes check: %s", keysFilePath);
13521361
ret = WSSHD_AUTH_FAILURE;

apps/wolfsshd/auth.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ int wolfSSHD_GetHomeDirectory(WOLFSSHD_AUTH* auth, WOLFSSH* ssh, WCHAR* out, int
109109
* and the trust-anchor loads in wolfsshd.c (host key, host cert, user CA keys).
110110
* See the definition in auth.c for the meaning of each argument. */
111111
int wolfSSHD_OpenSecureFile(const char* path, WUID_T ownerUid,
112-
int rejectReadable, void* heap, WFILE** out);
112+
int rejectReadable, int relaxPerms, void* heap, WFILE** out);
113113

114114
/* classifies a loaded host private key buffer as OpenSSH or ASN1/DER.
115115
* *keyDer is a WMALLOC'd (heap, DYNTYPE_SSHD) buffer to WS_FORCEZERO +

apps/wolfsshd/test/run_all_sshd_tests.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ run_test() {
141141
# separation is off and a high port is used, so no root is needed.
142142
run_strictmodes_negative_test() {
143143
printf "Host key trust-anchor negative test ... "
144+
# WOLFSSH_NO_HOSTKEY_PERMS hands the mode to the platform, so the readable
145+
# key loads and there is nothing to assert.
146+
if wolfssh_has HOSTKEY_RELAX_PERMS; then
147+
TOTAL=$((TOTAL+1))
148+
SKIPPED=$((SKIPPED+1))
149+
printf "SKIPPED (built with WOLFSSH_NO_HOSTKEY_PERMS)\n"
150+
return
151+
fi
144152
# A local copy of the host key, made group/world readable.
145153
cp ../../../keys/server-key.pem strictmodes_hostkey.pem
146154
chmod 644 strictmodes_hostkey.pem
@@ -341,6 +349,16 @@ EOF
341349
grep -q "Refusing to load" "$HK_WORK/log.txt" || hk_fail "FIFO host key was not refused"
342350
fi
343351

352+
# The mode and owner cases below are the ones WOLFSSH_NO_HOSTKEY_PERMS hands
353+
# to the platform, so on such a build the key loads and the assertions would
354+
# invert. Skip them there; the symlink and FIFO cases above still hold.
355+
if wolfssh_has HOSTKEY_RELAX_PERMS; then
356+
rm -rf "$HK_WORK"
357+
printf "PASSED (mode and owner cases skipped, built with "
358+
printf "WOLFSSH_NO_HOSTKEY_PERMS)\n"
359+
return
360+
fi
361+
344362
# group/world-writable file must be refused
345363
cp "$HK_KEY" "$HK_WORK/ww.pem"; chmod 666 "$HK_WORK/ww.pem"
346364
hk_cfg "$HK_WORK/ww.pem"; hk_run

apps/wolfsshd/test/test_configuration.c

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4999,10 +4999,12 @@ static int smChmod(const char* path, mode_t mode)
49994999

50005000
/* open 'path' through the secure gate and immediately close it, returning the
50015001
* gate's verdict so a scenario can assert acceptance or rejection */
5002-
static int smOpen(const char* path, WUID_T ownerUid, int rejectReadable)
5002+
static int smOpenEx(const char* path, WUID_T ownerUid, int rejectReadable,
5003+
int relaxPerms)
50035004
{
50045005
WFILE* f = WBADFILE;
5005-
int ret = wolfSSHD_OpenSecureFile(path, ownerUid, rejectReadable, NULL, &f);
5006+
int ret = wolfSSHD_OpenSecureFile(path, ownerUid, rejectReadable,
5007+
relaxPerms, NULL, &f);
50065008

50075009
if (ret == WS_SUCCESS && f != WBADFILE) {
50085010
/* read-only handle; a close failure has no bearing on the verdict */
@@ -5011,6 +5013,11 @@ static int smOpen(const char* path, WUID_T ownerUid, int rejectReadable)
50115013
return ret;
50125014
}
50135015

5016+
static int smOpen(const char* path, WUID_T ownerUid, int rejectReadable)
5017+
{
5018+
return smOpenEx(path, ownerUid, rejectReadable, 0 /* relaxPerms */);
5019+
}
5020+
50145021
static int test_OpenSecureFile(void)
50155022
{
50165023
int ret = WS_SUCCESS;
@@ -5176,6 +5183,43 @@ static int test_OpenSecureFile(void)
51765183
ret = smExpect("NULL path rejected", smOpen(NULL, uid, 0), 0);
51775184
}
51785185

5186+
/* relaxPerms: ownership, modes and directories are the platform's, but the
5187+
* structural checks still hold. Only wolfsshd's host key load sets this,
5188+
* and only where WOLFSSHD_HOSTKEY_RELAX_PERMS is on. */
5189+
if (ret == WS_SUCCESS)
5190+
ret = smChmod(hostkey, 0666);
5191+
if (ret == WS_SUCCESS) {
5192+
ret = smExpect("relaxed: world-writable host key accepted",
5193+
smOpenEx(hostkey, uid, 1, 1), 1);
5194+
}
5195+
/* No uid != 0 guard: relaxPerms ignores ownerUid, so this holds as root. */
5196+
if (ret == WS_SUCCESS) {
5197+
ret = smExpect("relaxed: wrong owner accepted",
5198+
smOpenEx(hostkey, uid + 1, 1, 1), 1);
5199+
}
5200+
if (ret == WS_SUCCESS)
5201+
ret = smChmod(hostkey, 0600);
5202+
if (ret == WS_SUCCESS)
5203+
ret = smChmod(wopen, 0777);
5204+
if (ret == WS_SUCCESS) {
5205+
ret = smExpect("relaxed: world-writable ancestor accepted",
5206+
smOpenEx(wopenKeys, uid, 1, 1), 1);
5207+
}
5208+
if (ret == WS_SUCCESS)
5209+
ret = smChmod(wopen, 0700);
5210+
if (ret == WS_SUCCESS) {
5211+
ret = smExpect("relaxed: symlinked leaf still rejected",
5212+
smOpenEx(linkKeys, uid, 1, 1), 0);
5213+
}
5214+
if (ret == WS_SUCCESS) {
5215+
ret = smExpect("relaxed: directory target still rejected",
5216+
smOpenEx(ssh, uid, 1, 1), 0);
5217+
}
5218+
if (ret == WS_SUCCESS) {
5219+
ret = smExpect("relaxed: missing file still rejected",
5220+
smOpenEx("/tmp/wolfsshd_sm_dne_xyz", uid, 1, 1), 0);
5221+
}
5222+
51795223
/* cleanup */
51805224
unlink(linkKeys);
51815225
unlink(keys);

apps/wolfsshd/wolfsshd.c

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ static void wolfSSHDLoggingCb(enum wolfSSH_LogLevel lvl, const char *const str)
232232
}
233233

234234

235+
/* QNX fixes the host key's owner and modes in the system image, so
236+
* WOLFSSH_NO_HOSTKEY_PERMS hands that policy to the platform. Host key only, and
237+
* only ownership, modes and directories. Kept outside the NO_FILESYSTEM guard
238+
* below because SetupCTX() tests it and -Wundef is on. wolfssh-options.c repeats
239+
* this guard so the tests can skip the negative cases; keep the two in step. */
240+
#if defined(WOLFSSH_NO_HOSTKEY_PERMS) && \
241+
(defined(__QNX__) || defined(__QNXNTO__))
242+
#define WOLFSSHD_HOSTKEY_RELAX_PERMS 1
243+
#else
244+
#define WOLFSSHD_HOSTKEY_RELAX_PERMS 0
245+
#endif
246+
235247
#ifndef NO_FILESYSTEM
236248
static void freeBufferFromFile(byte* buf, void* heap)
237249
{
@@ -260,11 +272,18 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap,
260272
byte* buf = NULL;
261273
long fileSz;
262274
word32 readSz;
275+
int relaxPerms = 0;
263276

264277
WOLFSSH_UNUSED(heap);
265278

266279
if (fileName == NULL) return NULL;
267280

281+
#if WOLFSSHD_HOSTKEY_RELAX_PERMS
282+
if (loadClass == WOLFSSHD_LOAD_SECRET) {
283+
relaxPerms = 1;
284+
}
285+
#endif
286+
268287
if (loadClass == WOLFSSHD_LOAD_NORMAL) {
269288
if (WFOPEN(NULL, &file, fileName, "rb") != 0)
270289
return NULL;
@@ -273,15 +292,15 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap,
273292
/* Trust anchors always go through the secure gate, regardless of
274293
* StrictModes. The owner is the daemon's effective user (or root), and
275294
* the host private key (SECRET) is also refused if group/world
276-
* readable. */
295+
* readable, unless WOLFSSHD_HOSTKEY_RELAX_PERMS. */
277296
if (wolfSSHD_OpenSecureFile(fileName,
278297
#ifndef _WIN32
279298
geteuid(),
280299
#else
281300
0,
282301
#endif
283302
loadClass == WOLFSSHD_LOAD_SECRET /* rejectReadable */,
284-
heap, &file) != WS_SUCCESS) {
303+
relaxPerms, heap, &file) != WS_SUCCESS) {
285304
return NULL;
286305
}
287306
}
@@ -395,7 +414,13 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx,
395414

396415
/* The host private key is a secret trust anchor: refuse a symlink,
397416
* an unsafe owner or path, or a group/world readable/writable
398-
* file. */
417+
* file. WOLFSSHD_HOSTKEY_RELAX_PERMS keeps only the symlink and
418+
* regular-file checks. */
419+
#if WOLFSSHD_HOSTKEY_RELAX_PERMS
420+
wolfSSH_Log(WS_LOG_INFO, "[SSHD] Built with "
421+
"WOLFSSH_NO_HOSTKEY_PERMS, host key ownership and permissions "
422+
"are left to the platform");
423+
#endif
399424
data = getBufferFromFile(hostKey, &dataSz, heap,
400425
WOLFSSHD_LOAD_SECRET);
401426
if (data == NULL) {

0 commit comments

Comments
 (0)