/* Control-flow doubles only: no cryptographic implementation or real credentials. */ #include #include #include #include typedef uint8_t byte; typedef uint32_t word32; #include "auth_types.h" /* Reviewed production feature profile. Never enable certificates/none silently. */ #define WOLFSSH_NO_RSA #if defined(WOLFSSH_CERTS) || defined(WOLFSSH_ALLOW_USERAUTH_NONE) || \ defined(WOLFSSH_NO_ECDSA) || defined(WOLFSSH_NO_ED25519) || \ defined(NO_FAILURE_ON_REJECTED) #error "Unexpected wolfSSH auth-contract feature profile" #endif #define WLOG(...) ((void)0) #define WMEMSET memset #define WMEMCPY memcpy #define WSTRNCAT strncat #define XSTRLEN strlen #define BOOLEAN_SZ 1 #define UINT32_SZ 4 #define LENGTH_SZ 4 #define MSG_ID_SZ 1 #define MAX_AUTH_STRING 80 #define WOLFSSH_MAX_PROMPTS 8 #define WC_MAX_DIGEST_SIZE 64 #define min(a,b) ((a) < (b) ? (a) : (b)) enum { WS_SUCCESS = 0, WS_ERROR = -1, WS_BUFFER_E = -2, WS_BAD_ARGUMENT = -3, WS_USER_AUTH_E = -4, WS_AUTH_PENDING = -5, WS_INVALID_ALGO_ID = -6, WS_CRYPTO_FAILED = -7, WS_BAD_USAGE = -8, WS_WANT_WRITE = -9, WS_REKEYING = -10, WS_INVALID_CHANID = -11, WS_WINDOW_FULL = -12 }; enum { ID_NONE, ID_UNKNOWN, ID_USERAUTH_PASSWORD, ID_USERAUTH_KEYBOARD, ID_USERAUTH_PUBLICKEY, ID_ED25519, ID_ECDSA_SHA2_NISTP256, ID_ECDSA_SHA2_NISTP384, ID_ECDSA_SHA2_NISTP521 }; enum { WOLFSSH_ENDPOINT_SERVER, CLIENT_USERAUTH_DONE = 20, MSGID_USERAUTH_REQUEST, MSGID_USERAUTH_INFO_REQUEST, MSGID_CHANNEL_DATA, WS_CHANNEL_ID_SELF }; enum wc_HashType { WC_HASH_TYPE_SHA }; typedef int wc_HashAlg; typedef struct WOLFSSH WOLFSSH; typedef struct { int (*userAuthCb)(byte, WS_UserAuthData *, void *); int (*userAuthResultCb)(byte, WS_UserAuthData *, void *); int (*keyboardAuthCb)(WS_UserAuthData_Keyboard *, void *); int (*userAuthTypesCb)(WOLFSSH *, void *); } WOLFSSH_CTX; struct WOLFSSH { WOLFSSH_CTX *ctx; void *userAuthCtx, *userAuthResultCtx, *keyboardAuthCtx; int clientState, isKeying, error; byte sessionId[32]; word32 sessionIdSz; struct { byte *buffer; word32 length, plainSz; } outputBuffer; struct { word32 promptCount; } kbAuth; }; typedef struct { word32 peerWindowSz, peerMaxPacketSz, maxPacketSz, peerChannel; } WOLFSSH_CHANNEL; static WOLFSSH_CHANNEL channel; static const byte cannedKeyAlgoClient[] = { ID_ED25519, ID_ECDSA_SHA2_NISTP256 }; 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, 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]; } static void c32toa(word32 v, byte *b) { b[0] = v >> 24; b[1] = v >> 16; b[2] = v >> 8; b[3] = v; } static byte NameToId(const char *s, word32 n) { static const struct { const char *s; byte id; } names[] = { {"none", ID_NONE}, {"password", ID_USERAUTH_PASSWORD}, {"keyboard-interactive", ID_USERAUTH_KEYBOARD}, {"publickey", ID_USERAUTH_PUBLICKEY}, {"ssh-ed25519", ID_ED25519}, {"ecdsa-sha2-nistp256", ID_ECDSA_SHA2_NISTP256} }; for (unsigned i = 0; i < sizeof(names)/sizeof(names[0]); ++i) if (strlen(names[i].s) == n && !memcmp(s, names[i].s, n)) return names[i].id; return ID_UNKNOWN; } static byte MatchIdLists(int side, const byte *id, word32 count, const byte *list, word32 n) { 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) { /* 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; } static int DoUserAuthRequestEd25519(WOLFSSH *s, WS_UserAuthData_PublicKey *p, WS_UserAuthData *a) { event('C'); return crypto_return; } static int DoUserAuthRequestEcc(WOLFSSH *s, WS_UserAuthData_PublicKey *p, enum wc_HashType h, byte *d, word32 n) { event('C'); return crypto_return; } static enum wc_HashType HashForId(byte id) { return WC_HASH_TYPE_SHA; } static int wc_HashGetDigestSize(enum wc_HashType h) { return 32; } static int wc_HashInit(wc_HashAlg *h, enum wc_HashType id) { event('H'); return 0; } static int HashUpdate(wc_HashAlg *h, enum wc_HashType id, const byte *b, word32 n) { return 0; } static int wc_HashFinal(wc_HashAlg *h, enum wc_HashType id, byte *b) { return 0; } static void wc_HashFree(wc_HashAlg *h, enum wc_HashType id) {} static int PrepareUserAuthRequestKeyboard(WOLFSSH *s, word32 *n, WS_UserAuthData *a) { event('Q'); return WS_SUCCESS; } static int BuildUserAuthRequestKeyboard(WOLFSSH *s, byte *b, word32 *n, WS_UserAuthData *a) { event('B'); return WS_SUCCESS; } static int PreparePacket(WOLFSSH *s, word32 n) { event('T'); return WS_SUCCESS; } static int BundlePacket(WOLFSSH *s) { event('B'); return WS_SUCCESS; } static int wolfSSH_SendPacket(WOLFSSH *s) { event('S'); s->error = send_return; return send_return; } static void PurgePacket(WOLFSSH *s) { event('X'); } static WOLFSSH_CHANNEL *ChannelFind(WOLFSSH *s, word32 id, int kind) { return &channel; } #include "actual.c" static int authorize(byte method, WS_UserAuthData *a, void *ctx) { event('A'); assert(method == a->type); assert(a->usernameSz == 4 && !memcmp(a->username, "test", 4)); if (method == WOLFSSH_USERAUTH_PASSWORD) { 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; } static int result(byte outcome, WS_UserAuthData *a, void *ctx) { assert(a->type == WOLFSSH_USERAUTH_PUBLICKEY && a->sf.publicKey.hasSignature); event(outcome == WOLFSSH_USERAUTH_SUCCESS ? 'R' : 'r'); return result_return; } /* Models the parent's registered rejecting prompt callback, not app accounting. */ static int reject_keyboard(WS_UserAuthData_Keyboard *k, void *ctx) { event('K'); memset(k, 0, sizeof(*k)); return WS_ERROR; } static int allowed(WOLFSSH *s, void *ctx) { return WOLFSSH_USERAUTH_PASSWORD | WOLFSSH_USERAUTH_PUBLICKEY; } 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) { memset(&ssh, 0, sizeof(ssh)); memset(output, 0, sizeof(output)); memset(events, 0, sizeof(events)); event_count = 0; 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; 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)); c32toa(n, packet + length); length += 4; memcpy(packet + length, s, n); length += n; } static void string(const char *s) { blob(s, (word32)strlen(s)); } static void request(const char *method) { string("test"); string("ssh-connection"); string(method); } static void key_request(int signed_key, const char *algorithm) { request("publickey"); packet[length++] = signed_key; string(algorithm); byte nested[128]; word32 n = (word32)strlen(algorithm); c32toa(n, nested); memcpy(nested + 4, algorithm, n); nested[4 + n] = 42; blob(nested, n + 5); if (signed_key) { c32toa(1, nested + 4 + n); nested[8 + n] = 42; blob(nested, n + 9); } } static int dispatch(void) { word32 idx = 0; return DoUserAuthRequest(&ssh, packet, length, &idx); } 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, WOLFSSH_USERAUTH_INVALID_USER, WOLFSSH_USERAUTH_INVALID_AUTHTYPE }; const char *algorithms[] = { "ssh-ed25519", "ecdsa-sha2-nistp256" }; for (unsigned a = 0; a < 2; ++a) { for (unsigned r = 0; r < sizeof(rejected)/sizeof(rejected[0]); ++r) { reset(); key_request(1, algorithms[a]); auth_return = rejected[r]; assert(dispatch() == WS_SUCCESS); check("AF", 0); } reset(); key_request(0, algorithms[a]); assert(dispatch() == WS_SUCCESS); check("AP", 0); reset(); key_request(0, algorithms[a]); auth_return = WOLFSSH_USERAUTH_INVALID_PUBLICKEY; assert(dispatch() == WS_SUCCESS); check("AF", 0); reset(); key_request(1, algorithms[a]); crypto_return = WS_CRYPTO_FAILED; result_return = WS_ERROR; /* Failure-result return is ignored. */ assert(dispatch() == WS_SUCCESS); check(a ? "AHCrF" : "ACrF", 0); reset(); key_request(1, algorithms[a]); assert(dispatch() == WS_SUCCESS); check(a ? "AHCR" : "ACR", 1); reset(); key_request(1, algorithms[a]); result_return = WS_ERROR; assert(dispatch() == WS_SUCCESS); check(a ? "AHCRF" : "ACRF", 0); reset(); key_request(1, algorithms[a]); auth_return = WOLFSSH_USERAUTH_WOULD_BLOCK; assert(dispatch() == WS_AUTH_PENDING); check("A", 0); } for (int fail = 0; fail < 2; ++fail) { reset(); request("password"); packet[length++] = 0; string("dummy"); auth_return = fail ? WOLFSSH_USERAUTH_INVALID_PASSWORD : WOLFSSH_USERAUTH_SUCCESS; assert(dispatch() == WS_SUCCESS); check(fail ? "AF" : "A", !fail); } 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); /* 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_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); } reset(); key_request(1, "unsupported-key"); assert(dispatch() == WS_SUCCESS); check("F", 0); reset(); key_request(1, "ssh-ed25519"); --length; assert(dispatch() == WS_BUFFER_E); check("", 0); reset(); request("keyboard-interactive"); string(""); string(""); assert(context.keyboardAuthCb != NULL); assert(dispatch() == WS_ERROR); check("KX", 0); /* The library writes the message byte even on rejection, but does not send. */ assert(ssh.outputBuffer.length == 0 && output[0] == MSGID_USERAUTH_INFO_REQUEST); char methods[MAX_AUTH_STRING]; int n = GetAllowedAuth(&ssh, methods); methods[n] = '\0'; assert(!strcmp(methods, "publickey,password")); ++groups; /* Execute actual copy/positive-consumed path, including deferred network send. */ for (int deferred = 0; deferred < 2; ++deferred) { reset(); byte data[] = { 11, 22, 33, 44, 55 }; channel = (WOLFSSH_CHANNEL){ 100, 3, 10, 7 }; send_return = deferred ? WS_WANT_WRITE : WS_SUCCESS; assert(SendChannelData(&ssh, 1, data, sizeof(data)) == 3); assert(!memcmp(output + 9, data, 3)); memset(data, 0, 3); assert(output[9] == 11 && output[10] == 22 && output[11] == 33); assert(data[3] == 44 && channel.peerWindowSz == 97); assert(ssh.outputBuffer.plainSz == (deferred ? 3U : 0U)); check("TBS", 0); } 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); 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; }