From 601d6452cbda74d4b2668d54c41a9fb0feedfe04 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Mon, 10 Aug 2026 15:45:06 -0700 Subject: [PATCH] F-8824: enable config files without trailing newline --- apps/wolfssh/common.c | 22 +++++--- tests/regress.c | 118 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/apps/wolfssh/common.c b/apps/wolfssh/common.c index f5e1883ca..1127f5913 100644 --- a/apps/wolfssh/common.c +++ b/apps/wolfssh/common.c @@ -101,7 +101,8 @@ static int load_der_file(const char* filename, byte** out, word32* outSz) return -1; } - in = (byte*)WMALLOC(inSz, NULL, 0); + /* Ensure trailing null so buffer is a string, even without a newline */ + in = (byte*)WMALLOC(inSz + 1, NULL, 0); if (in == NULL) { WFCLOSE(NULL, file); return -1; @@ -114,8 +115,10 @@ static int load_der_file(const char* filename, byte** out, word32* outSz) in = 0; inSz = 0; } - else + else { + in[inSz] = 0; ret = 0; + } *out = in; *outSz = (word32)inSz; @@ -370,11 +373,9 @@ int ClientPublicKeyCheck(const byte* pubKey, word32 pubKeySz, void* ctx) } if (ret == 0) { - /* load_der_file() loads exactly what's in the file. Since it is - * NL terminated lines of known host data, and the last line ends - * in a NL, overwrite that with a nul to terminate the new string. */ - knownHosts[sz - 1] = 0; - + /* load_der_file() nul terminates one byte past the file contents, so + * the buffer is already a string whether or not the last line ends + * in a newline. */ encodedKey = (char*)WMALLOC(WOLFSSH_CLIENT_ENCKEY_SIZE_ESTIMATE + WOLFSSH_CLIENT_PUBKEYTYPE_SIZE_ESTIMATE + WOLFSSH_CLIENT_FINGERPRINT_SIZE_ESTIMATE, NULL, 0); @@ -421,6 +422,13 @@ int ClientPublicKeyCheck(const byte* pubKey, word32 pubKeySz, void* ctx) lineCount++; line = WSTRSEP(&cursor, "\n"); if (line != NULL && *line) { + size_t lineSz = WSTRLEN(line); + + /* Remove trailing CR if present for comparison below */ + if (lineSz > 0 && line[lineSz - 1] == '\r') { + line[lineSz - 1] = 0; + } + name = WSTRSEP(&line, " "); keyType = WSTRSEP(&line, " "); key = WSTRSEP(&line, " "); diff --git a/tests/regress.c b/tests/regress.c index f601c61d0..5ffad13d8 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -36,7 +36,9 @@ #include #include #include +#include +#include #include #include #include @@ -5797,6 +5799,119 @@ static void TestAppendKeyToFile(void) #endif /* WOLFSSH_TEST_INTERNAL */ +#ifdef WOLFSSL_BASE64_ENCODE + +static void WriteKnownHosts(const char* path, const char* contents) +{ + WFILE* f = WBADFILE; + word32 sz = (word32)WSTRLEN(contents); + + AssertIntEQ(WFOPEN(NULL, &f, path, "wb"), 0); + AssertTrue(f != WBADFILE); + AssertIntEQ((word32)WFWRITE(NULL, contents, 1, sz, f), sz); + AssertIntEQ(WFCLOSE(NULL, f), 0); +} + + +/* known_hosts is a text file and POSIX lets its last line end without a + * newline. The parser used to nul out the final byte of the file, which ate + * the last base64 character of the last entry and made that host read as + * unknown. Match the last entry with a trailing newline, without one, and + * with CRLF line endings. */ +static void TestKnownHostsLastEntry(void) +{ + /* string("ssh-rsa"), then a zero certificate count so the RFC 6187 parse + * declines this blob, then filler. Only the name and the base64 of the + * whole blob matter to the known_hosts search. */ + static const byte pubKey[] = { + 0x00, 0x00, 0x00, 0x07, 's', 's', 'h', '-', 'r', 's', 'a', + 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04 + }; + static const struct { + const char* sep; + const char* tail; + const char* label; + } cases[] = { + { "\n", "\n", "trailing newline" }, + { "\n", "", "no trailing newline" }, + { "\r\n", "\r\n", "CRLF endings" }, + }; + char targetName[] = "last.example.com"; + char homeDir[64]; + char sshDir[80]; + char hostsPath[112]; + char encoded[64]; + char contents[256]; + char* savedHome = NULL; + const char* home; + word32 encodedSz = (word32)sizeof(encoded); + int savedStdin, devNull; + unsigned int i; + + WSNPRINTF(homeDir, sizeof(homeDir), "wolfssh_kh_%d.tmp", (int)getpid()); + WSNPRINTF(sshDir, sizeof(sshDir), "%s/.ssh", homeDir); + WSNPRINTF(hostsPath, sizeof(hostsPath), "%s/known_hosts", sshDir); + + AssertIntEQ(Base64_Encode_NoNl(pubKey, (word32)sizeof(pubKey), + (byte*)encoded, &encodedSz), 0); + AssertTrue(encodedSz < sizeof(encoded)); + encoded[encodedSz] = 0; + + home = getenv("HOME"); + if (home != NULL) { + savedHome = (char*)WMALLOC(WSTRLEN(home) + 1, NULL, 0); + AssertNotNull(savedHome); + WSTRCPY(savedHome, home); + } + + /* Plain mkdir/rmdir rather than WMKDIR/WRMDIR: those only exist in + * builds that compile the SCP or SFTP file system layer. */ + AssertIntEQ(mkdir(homeDir, 0700), 0); + AssertIntEQ(mkdir(sshDir, 0700), 0); + AssertIntEQ(setenv("HOME", homeDir, 1), 0); + + /* A regression falls through to the "add it to known hosts?" prompt, so + * point stdin at EOF: the test then fails rather than waiting forever. */ + savedStdin = dup(STDIN_FILENO); + devNull = open("/dev/null", O_RDONLY); + if (devNull >= 0) { + dup2(devNull, STDIN_FILENO); + } + + for (i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { + /* An entry for a different host goes first, so the match lands on the + * last line, the one the terminator used to overwrite. */ + WSNPRINTF(contents, sizeof(contents), + "other.example.com ssh-rsa AAAA%s%s ssh-rsa %s%s", + cases[i].sep, targetName, encoded, cases[i].tail); + WriteKnownHosts(hostsPath, contents); + + printf(" known_hosts with %s.\n", cases[i].label); + AssertIntEQ(ClientPublicKeyCheck(pubKey, (word32)sizeof(pubKey), + targetName), 0); + } + + if (devNull >= 0) { + dup2(savedStdin, STDIN_FILENO); + close(devNull); + } + close(savedStdin); + + if (savedHome != NULL) { + AssertIntEQ(setenv("HOME", savedHome, 1), 0); + WFREE(savedHome, NULL, 0); + } + else { + unsetenv("HOME"); + } + + (void)remove(hostsPath); + (void)rmdir(sshDir); + (void)rmdir(homeDir); +} +#endif /* WOLFSSL_BASE64_ENCODE */ + + int main(int argc, char** argv) { WOLFSSH_CTX* ctx; @@ -5828,6 +5943,9 @@ int main(int argc, char** argv) TestClientParseDestination(); #ifdef WOLFSSH_TEST_INTERNAL TestAppendKeyToFile(); +#endif +#ifdef WOLFSSL_BASE64_ENCODE + TestKnownHostsLastEntry(); #endif TestAuthMessageBlockedDuringKeying(ssh); TestUserauthFailureDuringKeying(ssh);