Add Phase 9C security hardening

Generate exact-hash SDK source overrides without modifying dependencies.
Harden
SSH allocation and algorithm policy, tighten web authentication cleanup,
and add
focused host contract tests and documentation.
This commit is contained in:
2026-09-15 22:12:57 +02:00
parent 751dfb9ddb
commit cdc9c7335a
41 changed files with 3597 additions and 89 deletions
+135 -9
View File
@@ -67,7 +67,8 @@ static const word32 cannedKeyAlgoClientSz = sizeof(cannedKeyAlgoClient);
static char events[64];
static unsigned event_count, groups;
static int auth_return, crypto_return, result_return, send_return;
static int expect_new_password;
static int expect_new_password, poison_password_pointers;
static void inspect_password_wipe(void);
static void event(char c) { assert(event_count + 1 < sizeof(events)); events[event_count++] = c; }
static void ato32(const byte *b, word32 *v) {
*v = (word32)b[0] << 24 | (word32)b[1] << 16 | (word32)b[2] << 8 | b[3];
@@ -89,8 +90,13 @@ static byte MatchIdLists(int side, const byte *id, word32 count, const byte *lis
for (word32 i = 0; i < n; ++i) if (*id == list[i]) return *id;
return ID_UNKNOWN;
}
static int wolfSSH_SetUsernameRaw(WOLFSSH *s, const byte *u, word32 n) { return WS_SUCCESS; }
static int SendUserAuthFailure(WOLFSSH *s, byte partial) { event('F'); return WS_SUCCESS; }
static int wolfSSH_SetUsernameRaw(WOLFSSH *s, const byte *u, word32 n) {
/* Called again after the method parser: prefix must still be valid. */
assert(n == 4 && !memcmp(u, "test", 4)); return WS_SUCCESS;
}
static int SendUserAuthFailure(WOLFSSH *s, byte partial) {
inspect_password_wipe(); event('F'); return send_return;
}
static int SendUserAuthPkOk(WOLFSSH *s, const byte *a, word32 an, const byte *k, word32 kn) {
event('P'); return WS_SUCCESS;
}
@@ -129,6 +135,17 @@ static int authorize(byte method, WS_UserAuthData *a, void *ctx) {
assert(a->sf.password.hasNewPassword == expect_new_password);
assert(a->sf.password.passwordSz == 5);
assert(!memcmp(a->sf.password.password, "dummy", 5));
if (expect_new_password) {
assert(a->sf.password.newPasswordSz == 9);
assert(!memcmp(a->sf.password.newPassword, "new-dummy", 9));
}
if (poison_password_pointers) {
/* Cleanup must use checked packet bounds, not mutable authData. */
a->sf.password.password = (const byte *)(uintptr_t)1;
a->sf.password.passwordSz = UINT32_MAX;
a->sf.password.newPassword = (const byte *)(uintptr_t)1;
a->sf.password.newPasswordSz = UINT32_MAX;
}
}
return auth_return;
}
@@ -144,8 +161,23 @@ static int reject_keyboard(WS_UserAuthData_Keyboard *k, void *ctx) {
static int allowed(WOLFSSH *s, void *ctx) {
return WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY;
}
static byte output[1024], packet[1024];
static word32 length;
static byte output[1024];
static struct { byte before[16], bytes[1024], after[16]; } storage, saved;
#define packet storage.bytes
static word32 length, suffix_start, suffix_end;
static int watch_password;
static void inspect_password_wipe(void) {
if (!watch_password) return;
assert(!memcmp(storage.before, saved.before, sizeof(storage.before)));
assert(!memcmp(storage.after, saved.after, sizeof(storage.after)));
assert(!memcmp(packet, saved.bytes, suffix_start));
for (word32 i = suffix_start; i < suffix_end; ++i) assert(packet[i] == 0);
assert(!memcmp(packet + suffix_end, saved.bytes + suffix_end,
sizeof(packet) - suffix_end));
}
static void watch_suffix(word32 start) {
suffix_start = start; suffix_end = length; saved = storage; watch_password = 1;
}
static WOLFSSH_CTX context = { authorize, result, reject_keyboard, allowed };
static WOLFSSH ssh;
static void reset(void) {
@@ -154,7 +186,9 @@ static void reset(void) {
ssh.ctx = &context; ssh.outputBuffer.buffer = output; ssh.sessionIdSz = 32;
auth_return = WOLFSSH_USERAUTH_SUCCESS; crypto_return = WS_SUCCESS;
result_return = WS_SUCCESS; send_return = WS_SUCCESS; expect_new_password = 0;
length = 0;
length = 0; watch_password = 0; poison_password_pointers = 0;
memset(&storage, 0xa5, sizeof(storage));
context.userAuthCb = authorize;
}
static void blob(const void *s, word32 n) {
assert(length + 4 + n <= sizeof(packet));
@@ -178,6 +212,95 @@ static void check(const char *trace, int done) {
assert(!strcmp(events, trace));
assert((ssh.clientState == CLIENT_USERAUTH_DONE) == done); ++groups;
}
static void password_cleanup_tests(void) {
/* Every truncation of both encodings, including flag and length fields.
* No malformed input may reach the auth/database double, even if absent. */
for (int change = 0; change < 2; ++change) {
word32 payload_size = change ? 23 : 10;
for (word32 cut = 0; cut < payload_size; ++cut) {
for (int no_callback = 0; no_callback < 2; ++no_callback) {
reset(); request("password"); word32 start = length;
packet[length++] = change; string("dummy");
if (change) string("new-dummy");
length = start + cut; watch_suffix(start);
if (no_callback) context.userAuthCb = NULL;
assert(dispatch() == WS_BUFFER_E); check("", 0);
inspect_password_wipe();
}
}
}
const word32 oversized[] = { 6, 1024, UINT32_MAX };
for (unsigned i = 0; i < sizeof(oversized)/sizeof(oversized[0]); ++i) {
for (int change = 0; change < 2; ++change) {
reset(); request("password"); word32 start = length;
packet[length++] = change; string("dummy");
word32 field = start + 1;
if (change) { field = length; string("new-dummy"); }
c32toa(change && oversized[i] == 6 ? 10 : oversized[i], packet + field);
watch_suffix(start);
assert(dispatch() == WS_BUFFER_E); check("", 0); inspect_password_wipe();
}
}
/* Application bad-password/backend failure/admission denial all retain the
* library's ordinary result mapping. Include partial success and no callback. */
const int outcomes[] = { WOLFSSH_USERAUTH_SUCCESS, WOLFSSH_USERAUTH_INVALID_PASSWORD,
WOLFSSH_USERAUTH_FAILURE, WOLFSSH_USERAUTH_REJECTED,
WOLFSSH_USERAUTH_INVALID_USER, WOLFSSH_USERAUTH_INVALID_AUTHTYPE,
WOLFSSH_USERAUTH_PARTIAL_SUCCESS };
for (unsigned i = 0; i < sizeof(outcomes)/sizeof(outcomes[0]); ++i) {
for (int change = 0; change < 2; ++change) {
reset(); request("password"); word32 start = length;
packet[length++] = change; string("dummy");
if (change) string("new-dummy");
expect_new_password = change; auth_return = outcomes[i];
poison_password_pointers = 1;
/* Trailing payload is also wiped but not included in parsed idx. */
word32 parsed_end = length; packet[length++] = 0x71;
watch_suffix(start);
WS_UserAuthData data = {0}; data.username = packet + 4; data.usernameSz = 4;
word32 idx = start;
assert(DoUserAuthRequestPassword(&ssh, &data, packet, length, &idx) == WS_SUCCESS);
assert(idx == (outcomes[i] == WOLFSSH_USERAUTH_REJECTED ? start : parsed_end));
check(i == 0 ? "A" : "AF", i == 0); inspect_password_wipe();
}
}
reset(); request("password"); word32 start = length;
packet[length++] = 0; string("dummy"); watch_suffix(start);
context.userAuthCb = NULL;
assert(dispatch() == WS_SUCCESS); check("F", 0); inspect_password_wipe();
reset(); request("password"); start = length;
packet[length++] = 0; string("dummy"); watch_suffix(start);
auth_return = WOLFSSH_USERAUTH_FAILURE; send_return = WS_WANT_WRITE;
assert(dispatch() == WS_WANT_WRITE); check("AF", 0); inspect_password_wipe();
for (int change = 0; change < 2; ++change) {
reset(); request("password"); start = length;
packet[length++] = change; string("dummy");
if (change) string("new-dummy");
expect_new_password = change; watch_suffix(start);
auth_return = WOLFSSH_USERAUTH_WOULD_BLOCK;
assert(dispatch() == WS_AUTH_PENDING); check("A", 0);
assert(!memcmp(&storage, &saved, sizeof(storage)));
auth_return = WOLFSSH_USERAUTH_SUCCESS;
assert(dispatch() == WS_SUCCESS); check("AA", 1); inspect_password_wipe();
}
/* Invalid argument paths must neither dereference idx nor guess wipe bounds. */
for (int bad = 0; bad < 8; ++bad) {
reset(); request("password"); start = length;
packet[length++] = 0; string("dummy"); saved = storage;
WS_UserAuthData data = {0}; word32 idx = start;
if (bad == 5) idx = length + 1;
if (bad == 6) idx = UINT32_MAX;
if (bad == 7) ssh.ctx = NULL;
int ret = DoUserAuthRequestPassword(bad == 0 ? NULL : &ssh,
bad == 1 ? NULL : &data, bad == 2 ? NULL : packet,
bad == 3 ? 0 : length, bad == 4 ? NULL : &idx);
assert(ret == (bad == 5 || bad == 6 ? WS_BUFFER_E : WS_BAD_ARGUMENT));
assert(!memcmp(&storage, &saved, sizeof(storage))); check("", 0);
}
}
int main(void) {
const int rejected[] = { WOLFSSH_USERAUTH_INVALID_PUBLICKEY,
WOLFSSH_USERAUTH_FAILURE, WOLFSSH_USERAUTH_REJECTED,
@@ -209,10 +332,10 @@ int main(void) {
reset(); request("password"); packet[length++] = 1; string("dummy"); string("new-dummy");
expect_new_password = 1; auth_return = WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
assert(dispatch() == WS_SUCCESS); check("AF", 0);
/* Actual parser still calls auth when the new-password length is truncated. */
/* The generated parser rejects a truncated new-password length before auth. */
reset(); request("password"); packet[length++] = 1; string("dummy");
expect_new_password = 1; auth_return = WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
assert(dispatch() == WS_SUCCESS); check("AF", 0);
assert(dispatch() == WS_BUFFER_E); check("", 0);
const char *unsupported[] = { "none", "unrecognized" };
for (unsigned i = 0; i < 2; ++i) {
reset(); request(unsupported[i]); assert(dispatch() == WS_SUCCESS); check("F", 0);
@@ -241,6 +364,9 @@ int main(void) {
reset(); ssh.outputBuffer.plainSz = 2; send_return = WS_WANT_WRITE;
byte data[] = { 1, 2 }; assert(SendChannelData(&ssh, 1, data, 2) == WS_WANT_WRITE);
assert(output[9] == 0); check("S", 0);
printf("PASS: %u actual wolfSSH parser/control-flow cases\n", groups);
assert(groups == 35);
printf("PASS: %u original wolfSSH parser/control-flow cases (stricter malformed password contract)\n", groups);
password_cleanup_tests();
printf("PASS: %u additional generated password parser/cleanup cases\n", groups - 35);
return 0;
}