/* regress.c * * Regression coverage for message ordering / keying state handling. * * Copyright (C) 2014-2026 wolfSSL Inc. * * This file is part of wolfSSH. * * wolfSSH is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * wolfSSH is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with wolfSSH. If not, see . */ #ifdef HAVE_CONFIG_H #include #endif #ifdef WOLFSSL_USER_SETTINGS #include #else #include #endif #include #include #include #include #include #include #include #include #include #ifdef WOLFSSH_SFTP #include #endif #include "apps/wolfssh/common.h" #ifndef WOLFSSH_NO_ABORT #define WABORT() abort() #else #define WABORT() #endif #define PrintError(description, result) do { \ printf("\nERROR - %s line %d failed with:", __FILE__, __LINE__); \ printf("\n expected: "); printf description; \ printf("\n result: "); printf result; printf("\n\n"); \ } while(0) #define Fail(description, result) do { \ PrintError(description, result); \ WABORT(); \ } while(0) #define Assert(test, description, result) if (!(test)) Fail(description, result) #define AssertTrue(x) Assert((x), ("%s is true", #x), (#x " => FALSE")) #define AssertFalse(x) Assert(!(x), ("%s is false", #x), (#x " => TRUE")) #define AssertNotNull(x) Assert((x), ("%s is not null", #x), (#x " => NULL")) #define AssertIntEQ(x, y) do { int _x = (int)(x); int _y = (int)(y); \ Assert(_x == _y, ("%s == %s", #x, #y), ("%d != %d", _x, _y)); } while (0) static void ResetSession(WOLFSSH* ssh) { if (ssh->handshake != NULL) { WFREE(ssh->handshake, ssh->ctx->heap, DYNTYPE_HS); ssh->handshake = NULL; } ssh->isKeying = 0; ssh->connectState = CONNECT_BEGIN; ssh->error = 0; } static HandshakeInfo* AllocHandshake(WOLFSSH* ssh) { HandshakeInfo* hs; hs = (HandshakeInfo*)WMALLOC(sizeof(HandshakeInfo), ssh->ctx->heap, DYNTYPE_HS); AssertNotNull(hs); WMEMSET(hs, 0, sizeof(HandshakeInfo)); hs->blockSz = MIN_BLOCK_SZ; hs->eSz = (word32)sizeof(hs->e); hs->xSz = (word32)sizeof(hs->x); return hs; } /* Build a minimal SSH binary packet carrying only a message ID. * Layout: uint32 packetLen, byte padLen, payload[msgId], pad[padLen]. * Choose padLen so total is 8-byte aligned for the clear transport case. */ static word32 BuildPacket(byte msgId, byte* out, word32 outSz) { byte padLen = 6; /* 1 (msgId) +1 (padLen) +6 = 8 */ word32 packetLen = 1 + 1 + padLen; /* payload + padLen field + pad */ word32 need = 4 + packetLen; AssertTrue(outSz >= need); out[0] = (byte)(packetLen >> 24); out[1] = (byte)(packetLen >> 16); out[2] = (byte)(packetLen >> 8); out[3] = (byte)(packetLen); out[4] = padLen; out[5] = msgId; WMEMSET(out + 6, 0, padLen); return need; } static byte ParseMsgId(const byte* pkt, word32 sz) { AssertTrue(sz >= 6); return pkt[5]; } static word32 AppendByte(byte* buf, word32 bufSz, word32 idx, byte value) { AssertTrue(idx < bufSz); buf[idx++] = value; return idx; } static word32 AppendUint32(byte* buf, word32 bufSz, word32 idx, word32 value) { word32 netValue = htonl(value); AssertTrue(idx + UINT32_SZ <= bufSz); WMEMCPY(buf + idx, &netValue, UINT32_SZ); idx += UINT32_SZ; return idx; } static word32 AppendData(byte* buf, word32 bufSz, word32 idx, const byte* data, word32 dataSz) { AssertTrue(idx + dataSz <= bufSz); if (dataSz > 0) { WMEMCPY(buf + idx, data, dataSz); idx += dataSz; } return idx; } static word32 AppendString(byte* buf, word32 bufSz, word32 idx, const char* value) { word32 valueSz = (word32)WSTRLEN(value); idx = AppendUint32(buf, bufSz, idx, valueSz); return AppendData(buf, bufSz, idx, (const byte*)value, valueSz); } static word32 WrapPacket(byte msgId, const byte* payload, word32 payloadSz, byte* out, word32 outSz) { word32 idx = 0; word32 packetLen; word32 need; byte padLen = MIN_PAD_LENGTH; while (((UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ + payloadSz + padLen) % MIN_BLOCK_SZ) != 0) { padLen++; } packetLen = PAD_LENGTH_SZ + MSG_ID_SZ + payloadSz + padLen; need = UINT32_SZ + packetLen; AssertTrue(outSz >= need); idx = AppendUint32(out, outSz, idx, packetLen); idx = AppendByte(out, outSz, idx, padLen); idx = AppendByte(out, outSz, idx, msgId); idx = AppendData(out, outSz, idx, payload, payloadSz); AssertTrue(idx + padLen <= outSz); WMEMSET(out + idx, 0, padLen); idx += padLen; return idx; } static word32 BuildChannelOpenPacket(const char* type, word32 peerChannelId, word32 peerInitialWindowSz, word32 peerMaxPacketSz, const byte* extra, word32 extraSz, byte* out, word32 outSz) { byte payload[256]; word32 idx = 0; idx = AppendString(payload, sizeof(payload), idx, type); idx = AppendUint32(payload, sizeof(payload), idx, peerChannelId); idx = AppendUint32(payload, sizeof(payload), idx, peerInitialWindowSz); idx = AppendUint32(payload, sizeof(payload), idx, peerMaxPacketSz); idx = AppendData(payload, sizeof(payload), idx, extra, extraSz); return WrapPacket(MSGID_CHANNEL_OPEN, payload, idx, out, outSz); } static word32 BuildDisconnectPacket(word32 reason, byte* out, word32 outSz) { byte payload[64]; word32 idx = 0; idx = AppendUint32(payload, sizeof(payload), idx, reason); idx = AppendUint32(payload, sizeof(payload), idx, 0); idx = AppendUint32(payload, sizeof(payload), idx, 0); return WrapPacket(MSGID_DISCONNECT, payload, idx, out, outSz); } #ifdef WOLFSSH_FWD static word32 BuildDirectTcpipExtra(const char* host, word32 hostPort, const char* origin, word32 originPort, byte* out, word32 outSz) { word32 idx = 0; idx = AppendString(out, outSz, idx, host); idx = AppendUint32(out, outSz, idx, hostPort); idx = AppendString(out, outSz, idx, origin); idx = AppendUint32(out, outSz, idx, originPort); return idx; } static word32 BuildGlobalRequestFwdPacket(const char* bindAddr, word32 bindPort, int isCancel, byte wantReply, byte* out, word32 outSz) { byte payload[256]; word32 idx = 0; const char* reqName = isCancel ? "cancel-tcpip-forward" : "tcpip-forward"; idx = AppendString(payload, sizeof(payload), idx, reqName); idx = AppendByte (payload, sizeof(payload), idx, wantReply); idx = AppendString(payload, sizeof(payload), idx, bindAddr); idx = AppendUint32(payload, sizeof(payload), idx, bindPort); return WrapPacket(MSGID_GLOBAL_REQUEST, payload, idx, out, outSz); } #endif /* Simple in-memory transport harness */ typedef struct { byte* in; /* data to feed into client */ word32 inSz; word32 inOff; byte* out; /* data written by client */ word32 outSz; word32 outCap; } MemIo; static int MemRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; MemIo* io = (MemIo*)ctx; word32 remain = io->inSz - io->inOff; if (remain == 0) return WS_CBIO_ERR_WANT_READ; if (sz > remain) sz = remain; WMEMCPY(buf, io->in + io->inOff, sz); io->inOff += sz; return (int)sz; } static int MemSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; MemIo* io = (MemIo*)ctx; if (io->outSz + sz > io->outCap) { return WS_CBIO_ERR_GENERAL; } WMEMCPY(io->out + io->outSz, buf, sz); io->outSz += sz; return (int)sz; } static void MemIoInit(MemIo* io, byte* in, word32 inSz, byte* out, word32 outCap) { io->in = in; io->inSz = inSz; io->inOff = 0; io->out = out; io->outSz = 0; io->outCap = outCap; } typedef struct { WOLFSSH_CTX* ctx; WOLFSSH* ssh; MemIo io; byte out[256]; } ChannelOpenHarness; static void InitChannelOpenHarness(ChannelOpenHarness* harness, byte* in, word32 inSz) { WMEMSET(harness, 0, sizeof(*harness)); harness->ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); AssertNotNull(harness->ctx); wolfSSH_SetIORecv(harness->ctx, MemRecv); wolfSSH_SetIOSend(harness->ctx, MemSend); harness->ssh = wolfSSH_new(harness->ctx); AssertNotNull(harness->ssh); MemIoInit(&harness->io, in, inSz, harness->out, sizeof(harness->out)); wolfSSH_SetIOReadCtx(harness->ssh, &harness->io); wolfSSH_SetIOWriteCtx(harness->ssh, &harness->io); harness->ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT; } static void FreeChannelOpenHarness(ChannelOpenHarness* harness) { if (harness->ssh != NULL) wolfSSH_free(harness->ssh); if (harness->ctx != NULL) wolfSSH_CTX_free(harness->ctx); } #if !defined(NO_WOLFSSH_SERVER) && !defined(NO_WOLFSSH_CLIENT) && \ !defined(WOLFSSH_NO_RSA) && !defined(NO_FILESYSTEM) #if !defined(WOLFSSH_NO_DH_GROUP14_SHA256) #define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group14-sha256" #elif !defined(WOLFSSH_NO_DH_GROUP16_SHA512) #define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group16-sha512" #elif !defined(WOLFSSH_NO_DH_GROUP14_SHA1) #define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group14-sha1" #elif !defined(WOLFSSH_NO_DH_GROUP1_SHA1) #define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group1-sha1" #endif #endif #ifdef KEXDH_REPLY_REGRESS_KEX_ALGO #define REGRESS_DUPLEX_QUEUE_SZ 32768U #define REGRESS_MUTATION_SCRATCH_SZ 4096U #define REGRESS_SERVER_KEY_PATH "keys/server-key-rsa.der" #define REGRESS_USERNAME "jill" #define REGRESS_PASSWORD "upthehill" #define REGRESS_MAX_HANDSHAKE_STEPS 2048 #define REGRESS_SSH_PROTO_PREFIX "SSH-" #define REGRESS_SSH_PROTO_PREFIX_SZ 4U typedef struct { byte data[REGRESS_DUPLEX_QUEUE_SZ]; word32 len; } DuplexQueue; typedef struct { byte enabled; int parseError; word32 matchedPackets; word32 mutatedPackets; byte scratch[REGRESS_MUTATION_SCRATCH_SZ]; word32 scratchSz; } KexReplyMutator; typedef struct DuplexEndpoint { DuplexQueue inbound; struct DuplexEndpoint* peer; KexReplyMutator* mutator; byte isServer; } DuplexEndpoint; typedef struct { WOLFSSH_CTX* clientCtx; WOLFSSH_CTX* serverCtx; WOLFSSH* client; WOLFSSH* server; DuplexEndpoint clientIo; DuplexEndpoint serverIo; KexReplyMutator mutator; } KexReplyHarness; typedef struct { int clientRet; int clientErr; int serverRet; int serverErr; int clientSuccess; int serverSuccess; word32 steps; } KexReplyRunResult; static word32 ReadUint32(const byte* buf) { return ((word32)buf[0] << 24) | ((word32)buf[1] << 16) | ((word32)buf[2] << 8) | (word32)buf[3]; } static int ReadStringRef(word32* strSz, const byte** str, const byte* buf, word32 len, word32* idx) { if (strSz == NULL || str == NULL || buf == NULL || idx == NULL) { return WS_BAD_ARGUMENT; } if (*idx > len || len - *idx < LENGTH_SZ) { return WS_PARSE_E; } *strSz = ReadUint32(buf + *idx); *idx += LENGTH_SZ; if (*strSz > len - *idx) { return WS_PARSE_E; } *str = buf + *idx; *idx += *strSz; return WS_SUCCESS; } static word32 AppendBlob(byte* buf, word32 bufSz, word32 idx, const byte* data, word32 dataSz) { idx = AppendUint32(buf, bufSz, idx, dataSz); return AppendData(buf, bufSz, idx, data, dataSz); } static word32 LoadFileBuffer(const char* path, byte* buf, word32 bufSz) { WFILE* file; long fileSz; word32 readSz; if (path == NULL || buf == NULL || bufSz == 0) { return 0; } if (WFOPEN(NULL, &file, path, "rb") != 0 || file == WBADFILE) { return 0; } WFSEEK(NULL, file, 0, WSEEK_END); fileSz = WFTELL(NULL, file); WREWIND(NULL, file); if (fileSz <= 0 || (word32)fileSz > bufSz) { WFCLOSE(NULL, file); return 0; } readSz = (word32)WFREAD(NULL, buf, 1, fileSz, file); WFCLOSE(NULL, file); if (readSz != (word32)fileSz) { return 0; } return readSz; } static int RegressionClientUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) { static const char password[] = REGRESS_PASSWORD; (void)ctx; if (authType != WOLFSSH_USERAUTH_PASSWORD || authData == NULL) { return WOLFSSH_USERAUTH_INVALID_AUTHTYPE; } authData->sf.password.password = (byte*)password; authData->sf.password.passwordSz = (word32)WSTRLEN(password); return WOLFSSH_USERAUTH_SUCCESS; } static int RegressionServerUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) { static const char password[] = REGRESS_PASSWORD; word32 passwordSz = (word32)WSTRLEN(password); (void)ctx; if (authType != WOLFSSH_USERAUTH_PASSWORD || authData == NULL) { return WOLFSSH_USERAUTH_FAILURE; } if (authData->sf.password.password == NULL || authData->sf.password.passwordSz != passwordSz) { return WOLFSSH_USERAUTH_FAILURE; } if (WMEMCMP(authData->sf.password.password, password, passwordSz) != 0) { return WOLFSSH_USERAUTH_FAILURE; } return WOLFSSH_USERAUTH_SUCCESS; } static int AcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz, void* ctx) { (void)pubKey; (void)pubKeySz; (void)ctx; return 0; } static int QueueAppend(DuplexQueue* queue, const byte* data, word32 dataSz) { if (queue == NULL || data == NULL) { return WS_BAD_ARGUMENT; } if (dataSz > sizeof(queue->data) - queue->len) { return WS_BUFFER_E; } WMEMCPY(queue->data + queue->len, data, dataSz); queue->len += dataSz; return WS_SUCCESS; } static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz, const char* replacement, byte* out, word32 outSz, word32* outLen) { const byte* payload; const byte* pubKey; const byte* f; const byte* sigBlob; const byte* sigName; const byte* sigData; word32 packetLen, padLen, payloadSz; word32 pubKeySz, fSz, sigBlobSz; word32 sigNameSz, sigDataSz; word32 idx = 0; word32 innerIdx = 0; word32 outerIdx = 0; word32 innerSigSz; byte payloadBuf[REGRESS_MUTATION_SCRATCH_SZ]; byte innerSig[REGRESS_MUTATION_SCRATCH_SZ]; if (replacement == NULL || out == NULL || outLen == NULL) { return WS_BAD_ARGUMENT; } if (packetSz < UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ) { return 0; } packetLen = ReadUint32(packet); if (packetLen + UINT32_SZ != packetSz) { return 0; } padLen = packet[UINT32_SZ]; if (packetLen < PAD_LENGTH_SZ + MSG_ID_SZ + padLen) { return WS_PARSE_E; } if (packet[UINT32_SZ + PAD_LENGTH_SZ] != MSGID_KEXDH_REPLY) { return 0; } payload = packet + UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ; payloadSz = packetSz - UINT32_SZ - PAD_LENGTH_SZ - MSG_ID_SZ - padLen; if (ReadStringRef(&pubKeySz, &pubKey, payload, payloadSz, &idx) != WS_SUCCESS) { return WS_PARSE_E; } if (ReadStringRef(&fSz, &f, payload, payloadSz, &idx) != WS_SUCCESS) { return WS_PARSE_E; } if (ReadStringRef(&sigBlobSz, &sigBlob, payload, payloadSz, &idx) != WS_SUCCESS) { return WS_PARSE_E; } if (ReadStringRef(&sigNameSz, &sigName, sigBlob, sigBlobSz, &innerIdx) != WS_SUCCESS) { return WS_PARSE_E; } if (ReadStringRef(&sigDataSz, &sigData, sigBlob, sigBlobSz, &innerIdx) != WS_SUCCESS) { return WS_PARSE_E; } if (innerIdx != sigBlobSz) { return WS_PARSE_E; } (void)sigName; (void)sigNameSz; innerSigSz = 0; innerSigSz = AppendString(innerSig, sizeof(innerSig), innerSigSz, replacement); innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz, sigData, sigDataSz); outerIdx = 0; outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, pubKey, pubKeySz); outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, f, fSz); outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, innerSig, innerSigSz); *outLen = WrapPacket(MSGID_KEXDH_REPLY, payloadBuf, outerIdx, out, outSz); return 1; } static int RewriteKexDhReplySignatureName(const byte* packet, word32 packetSz, const char* replacement, byte* out, word32 outSz, word32* outLen) { word32 offset = 0; if (packet == NULL || replacement == NULL || out == NULL || outLen == NULL) { return WS_BAD_ARGUMENT; } while (packetSz - offset >= UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ) { word32 curPacketSz = ReadUint32(packet + offset) + UINT32_SZ; int rewriteRet; if (curPacketSz > packetSz - offset) { return 0; } if (packet[offset + UINT32_SZ + PAD_LENGTH_SZ] == MSGID_KEXDH_REPLY) { rewriteRet = RewriteSingleKexDhReplyPacket(packet + offset, curPacketSz, replacement, out, outSz, outLen); if (rewriteRet <= 0) { return rewriteRet; } if (packetSz - offset - curPacketSz > outSz - *outLen) { return WS_BUFFER_E; } WMEMCPY(out + *outLen, packet + offset + curPacketSz, packetSz - offset - curPacketSz); *outLen += packetSz - offset - curPacketSz; return 1; } offset += curPacketSz; } return 0; } static int DuplexRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { DuplexEndpoint* endpoint = (DuplexEndpoint*)ctx; word32 readSz; (void)ssh; if (endpoint == NULL || buf == NULL) { return WS_CBIO_ERR_GENERAL; } if (endpoint->inbound.len == 0) { return WS_CBIO_ERR_WANT_READ; } readSz = sz; if (readSz > endpoint->inbound.len) { readSz = endpoint->inbound.len; } WMEMCPY(buf, endpoint->inbound.data, readSz); endpoint->inbound.len -= readSz; if (endpoint->inbound.len > 0) { WMEMMOVE(endpoint->inbound.data, endpoint->inbound.data + readSz, endpoint->inbound.len); } return (int)readSz; } static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { DuplexEndpoint* endpoint = (DuplexEndpoint*)ctx; const byte* output = (const byte*)buf; word32 outputSz = sz; int ret; (void)ssh; if (endpoint == NULL || endpoint->peer == NULL || buf == NULL) { return WS_CBIO_ERR_GENERAL; } if (endpoint->isServer && endpoint->mutator != NULL && endpoint->mutator->enabled && endpoint->mutator->mutatedPackets == 0 && outputSz >= UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ && !(outputSz >= REGRESS_SSH_PROTO_PREFIX_SZ && WMEMCMP(output, REGRESS_SSH_PROTO_PREFIX, REGRESS_SSH_PROTO_PREFIX_SZ) == 0)) { word32 mutatedSz = 0; int mutateRet; mutateRet = RewriteKexDhReplySignatureName(output, outputSz, "ssh-rsa", endpoint->mutator->scratch, (word32)sizeof(endpoint->mutator->scratch), &mutatedSz); if (mutateRet < 0) { endpoint->mutator->parseError = mutateRet; return WS_CBIO_ERR_GENERAL; } if (mutateRet > 0) { endpoint->mutator->matchedPackets++; endpoint->mutator->mutatedPackets++; endpoint->mutator->scratchSz = mutatedSz; output = endpoint->mutator->scratch; outputSz = mutatedSz; } } ret = QueueAppend(&endpoint->peer->inbound, output, outputSz); if (ret != WS_SUCCESS) { return WS_CBIO_ERR_GENERAL; } return (int)sz; } static void InitDuplexPair(DuplexEndpoint* client, DuplexEndpoint* server, KexReplyMutator* mutator) { WMEMSET(client, 0, sizeof(*client)); WMEMSET(server, 0, sizeof(*server)); client->peer = server; server->peer = client; server->mutator = mutator; server->isServer = 1; } static void FreeKexReplyHarness(KexReplyHarness* harness) { if (harness->client != NULL) { wolfSSH_free(harness->client); } if (harness->server != NULL) { wolfSSH_free(harness->server); } if (harness->clientCtx != NULL) { wolfSSH_CTX_free(harness->clientCtx); } if (harness->serverCtx != NULL) { wolfSSH_CTX_free(harness->serverCtx); } } static void InitKexReplyHarnessEx(KexReplyHarness* harness, const char* keyAlgo, byte mutateReply, byte skipPublicKeyCheck) { byte keyBuf[2048]; word32 keySz; WMEMSET(harness, 0, sizeof(*harness)); InitDuplexPair(&harness->clientIo, &harness->serverIo, &harness->mutator); harness->mutator.enabled = mutateReply; harness->clientCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(harness->clientCtx); harness->serverCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); AssertNotNull(harness->serverCtx); AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(harness->clientCtx, KEXDH_REPLY_REGRESS_KEX_ALGO), WS_SUCCESS); AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(harness->serverCtx, KEXDH_REPLY_REGRESS_KEX_ALGO), WS_SUCCESS); AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(harness->clientCtx, keyAlgo), WS_SUCCESS); AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(harness->serverCtx, keyAlgo), WS_SUCCESS); wolfSSH_SetIORecv(harness->clientCtx, DuplexRecv); wolfSSH_SetIOSend(harness->clientCtx, DuplexSend); wolfSSH_SetIORecv(harness->serverCtx, DuplexRecv); wolfSSH_SetIOSend(harness->serverCtx, DuplexSend); wolfSSH_SetUserAuth(harness->clientCtx, RegressionClientUserAuth); wolfSSH_SetUserAuth(harness->serverCtx, RegressionServerUserAuth); if (!skipPublicKeyCheck) { wolfSSH_CTX_SetPublicKeyCheck(harness->clientCtx, AcceptAnyServerHostKey); } keySz = LoadFileBuffer(REGRESS_SERVER_KEY_PATH, keyBuf, sizeof(keyBuf)); AssertTrue(keySz > 0); AssertIntEQ(wolfSSH_CTX_UsePrivateKey_buffer(harness->serverCtx, keyBuf, keySz, WOLFSSH_FORMAT_ASN1), WS_SUCCESS); harness->client = wolfSSH_new(harness->clientCtx); AssertNotNull(harness->client); harness->server = wolfSSH_new(harness->serverCtx); AssertNotNull(harness->server); wolfSSH_SetIOReadCtx(harness->client, &harness->clientIo); wolfSSH_SetIOWriteCtx(harness->client, &harness->clientIo); wolfSSH_SetIOReadCtx(harness->server, &harness->serverIo); wolfSSH_SetIOWriteCtx(harness->server, &harness->serverIo); AssertIntEQ(wolfSSH_SetUsername(harness->client, REGRESS_USERNAME), WS_SUCCESS); } static void InitKexReplyHarness(KexReplyHarness* harness, const char* keyAlgo, byte mutateReply) { InitKexReplyHarnessEx(harness, keyAlgo, mutateReply, 0); } static int IsHandshakeRetryable(int err) { return err == WS_WANT_READ || err == WS_WANT_WRITE || err == WS_AUTH_PENDING; } static void RunKexReplyHandshake(KexReplyHarness* harness, KexReplyRunResult* result) { word32 step; WMEMSET(result, 0, sizeof(*result)); result->clientRet = WS_FATAL_ERROR; result->serverRet = WS_FATAL_ERROR; for (step = 0; step < REGRESS_MAX_HANDSHAKE_STEPS; step++) { if (!result->clientSuccess) { result->clientRet = wolfSSH_connect(harness->client); result->clientErr = wolfSSH_get_error(harness->client); if (result->clientRet == WS_SUCCESS) { result->clientSuccess = 1; } else if (!IsHandshakeRetryable(result->clientErr)) { result->steps = step + 1; return; } } if (!result->serverSuccess) { result->serverRet = wolfSSH_accept(harness->server); result->serverErr = wolfSSH_get_error(harness->server); if (result->serverRet == WS_SUCCESS) { result->serverSuccess = 1; } else if (!IsHandshakeRetryable(result->serverErr)) { result->steps = step + 1; return; } } if (result->clientSuccess && result->serverSuccess) { result->steps = step + 1; return; } } result->steps = REGRESS_MAX_HANDSHAKE_STEPS; } static void AssertHandshakeSucceeds(const char* keyAlgo) { KexReplyHarness harness; KexReplyRunResult result; InitKexReplyHarness(&harness, keyAlgo, 0); RunKexReplyHandshake(&harness, &result); AssertTrue(result.clientSuccess); AssertTrue(result.serverSuccess); AssertIntEQ(harness.mutator.mutatedPackets, 0); AssertIntEQ(harness.client->connectState, CONNECT_SERVER_CHANNEL_REQUEST_DONE); AssertIntEQ(harness.server->acceptState, ACCEPT_CLIENT_SESSION_ESTABLISHED); FreeKexReplyHarness(&harness); } static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo) { KexReplyHarness harness; KexReplyRunResult result; InitKexReplyHarness(&harness, keyAlgo, 1); RunKexReplyHandshake(&harness, &result); AssertIntEQ(harness.mutator.parseError, 0); AssertIntEQ(harness.mutator.matchedPackets, 1); AssertIntEQ(harness.mutator.mutatedPackets, 1); AssertFalse(result.clientSuccess); AssertFalse(harness.client->connectState >= CONNECT_KEYED); AssertTrue(result.clientRet == WS_FATAL_ERROR); AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE); FreeKexReplyHarness(&harness); } #ifndef WOLFSSH_NO_RSA_SHA2_256 static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void) { AssertHandshakeSucceeds("rsa-sha2-256"); AssertHandshakeRejectsMutatedReply("rsa-sha2-256"); } #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 static void TestKexDhReplyRejectsRsaSha2_512SigNameDowngrade(void) { AssertHandshakeSucceeds("rsa-sha2-512"); AssertHandshakeRejectsMutatedReply("rsa-sha2-512"); } #endif static void AssertHandshakeRejectsWithNoPublicKeyCheck(const char* keyAlgo) { KexReplyHarness harness; KexReplyRunResult result; InitKexReplyHarnessEx(&harness, keyAlgo, 0, 1 /* skipPublicKeyCheck */); RunKexReplyHandshake(&harness, &result); AssertFalse(result.clientSuccess); AssertTrue(result.clientRet == WS_FATAL_ERROR); AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE); AssertIntEQ(result.clientErr, WS_PUBKEY_REJECTED_E); AssertFalse(harness.client->connectState >= CONNECT_KEYED); FreeKexReplyHarness(&harness); } static void TestKexDhReplyRejectsNoPublicKeyCheck(void) { #ifndef WOLFSSH_NO_RSA_SHA2_256 AssertHandshakeRejectsWithNoPublicKeyCheck("rsa-sha2-256"); #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 AssertHandshakeRejectsWithNoPublicKeyCheck("rsa-sha2-512"); #endif } #endif /* KEXDH_REPLY_REGRESS_KEX_ALGO */ static void AssertChannelOpenFailResponse(const ChannelOpenHarness* harness, int ret) { byte msgId; AssertIntEQ(ret, WS_SUCCESS); AssertIntEQ(harness->io.inOff, harness->io.inSz); AssertTrue(harness->io.outSz > 0); AssertTrue(harness->io.outSz <= harness->io.outCap); msgId = ParseMsgId(harness->io.out, harness->io.outSz); AssertIntEQ(msgId, MSGID_CHANNEL_OPEN_FAIL); AssertFalse(msgId == MSGID_REQUEST_FAILURE); AssertIntEQ(harness->ssh->channelListSz, 0); AssertTrue(harness->ssh->channelList == NULL); } #ifdef WOLFSSH_FWD static word32 ParsePayloadLen(const byte* packet, word32 packetSz) { word32 packetLen; byte padLen; AssertNotNull(packet); AssertTrue(packetSz >= 6); WMEMCPY(&packetLen, packet, sizeof(packetLen)); packetLen = ntohl(packetLen); padLen = packet[4]; AssertTrue(packetLen >= (word32)padLen + 1); AssertTrue(packetSz >= packetLen + 4); return packetLen - padLen - 1; } static const byte* ParseGlobalRequestName(const byte* packet, word32 packetSz, word32* nameSz) { word32 packetLen; word32 payloadLen; word32 strSz; const byte* payload; AssertNotNull(packet); AssertNotNull(nameSz); AssertTrue(packetSz >= 10); WMEMCPY(&packetLen, packet, sizeof(packetLen)); packetLen = ntohl(packetLen); AssertTrue(packetSz >= packetLen + 4); payloadLen = ParsePayloadLen(packet, packetSz); payload = packet + 5; AssertTrue(payloadLen >= 1 + sizeof(word32)); AssertIntEQ(payload[0], MSGID_GLOBAL_REQUEST); WMEMCPY(&strSz, payload + 1, sizeof(strSz)); strSz = ntohl(strSz); AssertTrue(payloadLen >= 1 + sizeof(word32) + strSz); *nameSz = strSz; return payload + 1 + sizeof(word32); } static void AssertGlobalRequestReply(const ChannelOpenHarness* harness, byte expectedMsgId) { byte msgId; word32 payloadLen; AssertTrue(harness->io.outSz > 0); msgId = ParseMsgId(harness->io.out, harness->io.outSz); AssertIntEQ(msgId, expectedMsgId); payloadLen = ParsePayloadLen(harness->io.out, harness->io.outSz); if (expectedMsgId == MSGID_REQUEST_FAILURE) { AssertIntEQ(payloadLen, 1); } else if (expectedMsgId == MSGID_REQUEST_SUCCESS) { const byte* reqName; word32 reqNameSz; reqName = ParseGlobalRequestName(harness->io.in, harness->io.inSz, &reqNameSz); if (reqNameSz == sizeof("tcpip-forward") - 1 && WMEMCMP(reqName, "tcpip-forward", sizeof("tcpip-forward") - 1) == 0) { AssertIntEQ(payloadLen, 5); } else if (reqNameSz == sizeof("cancel-tcpip-forward") - 1 && WMEMCMP(reqName, "cancel-tcpip-forward", sizeof("cancel-tcpip-forward") - 1) == 0) { AssertIntEQ(payloadLen, 1); } else { Fail(("unexpected global request name"), ("%.*s", (int)reqNameSz, reqName)); } } } #endif static int RejectChannelOpenCb(WOLFSSH_CHANNEL* channel, void* ctx) { (void)channel; (void)ctx; return WS_BAD_ARGUMENT; } #ifdef WOLFSSH_FWD static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx, const char* host, word32 port) { (void)ctx; (void)host; (void)port; if (action == WOLFSSH_FWD_LOCAL_SETUP) return WS_FWD_SETUP_E; return WS_SUCCESS; } static int AcceptFwdCb(WS_FwdCbAction action, void* ctx, const char* host, word32 port) { (void)action; (void)ctx; (void)host; (void)port; return WS_SUCCESS; } #endif /* Reject auth messages while the peer is still keying and the client * expects the KEX reply. */ static void TestAuthMessageBlockedDuringKeying(WOLFSSH* ssh) { int allowed; ResetSession(ssh); ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; ssh->handshake = AllocHandshake(ssh); ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE, WS_MSG_RECV); AssertFalse(allowed); /* The expected message must be allowed and clear the expectation. */ allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXDH_REPLY, WS_MSG_RECV); AssertTrue(allowed); AssertIntEQ(ssh->handshake->expectMsgId, MSGID_NONE); } /* Reject USERAUTH_FAILURE with password list during keying (password-leak PoC). */ static void TestUserauthFailureDuringKeying(WOLFSSH* ssh) { byte buf[32]; word32 sz; int allowed; ResetSession(ssh); ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; ssh->handshake = AllocHandshake(ssh); ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); } /* Expect an abort/error to be set when password-leak sequence hits during keying. */ static void TestPasswordLeakAborts(WOLFSSH* ssh) { byte buf[32]; word32 sz; int allowed; ResetSession(ssh); ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; ssh->handshake = AllocHandshake(ssh); ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); AssertTrue(ssh->error != 0); /* should set an error / abort path */ } /* Reject USERAUTH_SUCCESS before the client has even sent a userauth request. */ static void TestPrematureUserauthSuccess(WOLFSSH* ssh) { int allowed; ResetSession(ssh); ssh->connectState = CONNECT_KEYED; allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS, WS_MSG_RECV); AssertFalse(allowed); } /* Reject a spoofed sequence: bogus USERAUTH_SUCCESS followed by channel msgs. */ static void TestChannelSpoofSequence(WOLFSSH* ssh) { byte buf[32]; word32 sz; int allowed; ResetSession(ssh); ssh->connectState = CONNECT_KEYED; sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); sz = BuildPacket(MSGID_CHANNEL_SUCCESS, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); sz = BuildPacket(MSGID_CHANNEL_DATA, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); } /* Expect abort/error on spoofed auth+channel sequence. */ static void TestChannelSpoofAborts(WOLFSSH* ssh) { byte buf[32]; word32 sz; int allowed; ResetSession(ssh); ssh->connectState = CONNECT_KEYED; sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); AssertTrue(ssh->error != 0); } /* Reject USERAUTH_FAILURE(publickey) before any auth request (static-signature PoC). */ static void TestPublicKeyFailureBeforeRequest(WOLFSSH* ssh) { byte buf[32]; word32 sz; int allowed; ResetSession(ssh); ssh->connectState = CONNECT_KEYED; sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); } /* Expect abort/error when publickey failure arrives before any request. */ static void TestPublicKeyFailureAborts(WOLFSSH* ssh) { byte buf[32]; word32 sz; int allowed; ResetSession(ssh); ssh->connectState = CONNECT_KEYED; sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), WS_MSG_RECV); AssertFalse(allowed); AssertTrue(ssh->error != 0); } /* Reject channel messages before user authentication completes. */ static void TestChannelBlockedBeforeAuth(WOLFSSH* ssh) { int allowed; ResetSession(ssh); ssh->connectState = CONNECT_KEYED; allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, WS_MSG_RECV); AssertFalse(allowed); } /* Allow channel messages after user authentication completes. */ static void TestChannelAllowedAfterAuth(WOLFSSH* ssh) { int allowed; ResetSession(ssh); ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, WS_MSG_RECV); AssertTrue(allowed); } static void TestChannelOpenCallbackRejectSendsOpenFail(void) { ChannelOpenHarness harness; byte in[128]; word32 inSz; int ret; inSz = BuildChannelOpenPacket("session", 7, 0x4000, 0x8000, NULL, 0, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); AssertIntEQ(wolfSSH_CTX_SetChannelOpenCb(harness.ctx, RejectChannelOpenCb), WS_SUCCESS); ret = DoReceive(harness.ssh); AssertChannelOpenFailResponse(&harness, ret); FreeChannelOpenHarness(&harness); } #ifdef WOLFSSH_FWD static void TestDirectTcpipRejectSendsOpenFail(void) { ChannelOpenHarness harness; byte extra[128]; byte in[192]; word32 extraSz; word32 inSz; int ret; extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222, extra, sizeof(extra)); inSz = BuildChannelOpenPacket("direct-tcpip", 9, 0x4000, 0x8000, extra, extraSz, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, RejectDirectTcpipSetup, NULL), WS_SUCCESS); ret = DoReceive(harness.ssh); AssertChannelOpenFailResponse(&harness, ret); FreeChannelOpenHarness(&harness); } static void TestDirectTcpipNoFwdCbSendsOpenFail(void) { ChannelOpenHarness harness; byte extra[128]; byte in[192]; word32 extraSz; word32 inSz; int ret; extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222, extra, sizeof(extra)); inSz = BuildChannelOpenPacket("direct-tcpip", 9, 0x4000, 0x8000, extra, extraSz, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); /* Intentionally do NOT register fwdCb */ ret = DoReceive(harness.ssh); AssertChannelOpenFailResponse(&harness, ret); FreeChannelOpenHarness(&harness); } static void TestGlobalRequestFwdNoCbSendsFailure(void) { ChannelOpenHarness harness; byte in[256]; word32 inSz; int ret; inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); /* no fwdCb registered */ ret = DoReceive(harness.ssh); AssertIntEQ(ret, WS_SUCCESS); AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE); FreeChannelOpenHarness(&harness); } static void TestGlobalRequestFwdNoCbNoReplyKeepsConnection(void) { ChannelOpenHarness harness; byte in[256]; word32 inSz; int ret; /* wantReply=0: no reply sent, connection must stay alive */ inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 0, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); /* no fwdCb registered */ ret = DoReceive(harness.ssh); AssertIntEQ(ret, WS_SUCCESS); AssertIntEQ(harness.io.outSz, 0); /* no reply sent */ FreeChannelOpenHarness(&harness); } static void TestGlobalRequestFwdWithCbSendsSuccess(void) { ChannelOpenHarness harness; byte in[256]; word32 inSz; int ret; inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS); ret = DoReceive(harness.ssh); AssertIntEQ(ret, WS_SUCCESS); AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS); FreeChannelOpenHarness(&harness); } static void TestGlobalRequestFwdCancelNoCbSendsFailure(void) { ChannelOpenHarness harness; byte in[256]; word32 inSz; int ret; inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); ret = DoReceive(harness.ssh); AssertIntEQ(ret, WS_SUCCESS); AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE); FreeChannelOpenHarness(&harness); } static void TestGlobalRequestFwdCancelWithCbSendsSuccess(void) { ChannelOpenHarness harness; byte in[256]; word32 inSz; int ret; inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS); ret = DoReceive(harness.ssh); AssertIntEQ(ret, WS_SUCCESS); AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS); FreeChannelOpenHarness(&harness); } #endif #ifdef WOLFSSH_AGENT static void TestAgentChannelNullAgentSendsOpenFail(void) { ChannelOpenHarness harness; byte in[128]; word32 inSz; int ret; inSz = BuildChannelOpenPacket("auth-agent@openssh.com", 11, 0x4000, 0x8000, NULL, 0, in, sizeof(in)); InitChannelOpenHarness(&harness, in, inSz); AssertTrue(harness.ssh->agent == NULL); ret = DoReceive(harness.ssh); AssertChannelOpenFailResponse(&harness, ret); FreeChannelOpenHarness(&harness); } #endif /* Reject a peer KEXINIT once keying is in progress. */ static void TestKexInitRejectedWhenKeying(WOLFSSH* ssh) { int allowed; ResetSession(ssh); ssh->isKeying = WOLFSSH_PEER_IS_KEYING; ssh->connectState = CONNECT_SERVER_KEXINIT_DONE; allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXINIT, WS_MSG_RECV); AssertFalse(allowed); } static void TestDisconnectSetsDisconnectError(void) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; MemIo io; byte in[128]; byte out[32]; word32 inSz; int ret; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(ctx); wolfSSH_SetIORecv(ctx, MemRecv); wolfSSH_SetIOSend(ctx, MemSend); ssh = wolfSSH_new(ctx); AssertNotNull(ssh); inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION, in, sizeof(in)); MemIoInit(&io, in, inSz, out, sizeof(out)); wolfSSH_SetIOReadCtx(ssh, &io); wolfSSH_SetIOWriteCtx(ssh, &io); ret = DoReceive(ssh); AssertIntEQ(ret, WS_FATAL_ERROR); AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT); AssertIntEQ(io.inOff, io.inSz); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } #ifdef WOLFSSH_SFTP static void TestOct2DecRejectsInvalidNonLeadingDigit(void) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; byte invalidOct[] = "0718"; int ret; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(ctx); ssh = wolfSSH_new(ctx); AssertNotNull(ssh); ret = wolfSSH_oct2dec(ssh, invalidOct, (word32)WSTRLEN((char*)invalidOct)); AssertIntEQ(ret, WS_BAD_ARGUMENT); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } #ifdef WOLFSSH_STOREHANDLE static void TestSftpRemoveHandleHeadUpdate(void) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; byte firstHandle[] = { 0x01, 0x02, 0x03, 0x04 }; byte secondHandle[] = { 0x10, 0x20, 0x30, 0x40 }; int ret; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(ctx); ssh = wolfSSH_new(ctx); AssertNotNull(ssh); ret = SFTP_AddHandleNode(ssh, firstHandle, sizeof(firstHandle), "first"); AssertIntEQ(ret, WS_SUCCESS); ret = SFTP_AddHandleNode(ssh, secondHandle, sizeof(secondHandle), "second"); AssertIntEQ(ret, WS_SUCCESS); ret = SFTP_RemoveHandleNode(ssh, secondHandle, sizeof(secondHandle)); AssertIntEQ(ret, WS_SUCCESS); AssertNotNull(ssh->handleList); AssertTrue(ssh->handleList->prev == NULL); AssertIntEQ(ssh->handleList->handleSz, (int)sizeof(firstHandle)); AssertIntEQ(WMEMCMP(ssh->handleList->handle, firstHandle, sizeof(firstHandle)), 0); ret = SFTP_RemoveHandleNode(ssh, firstHandle, sizeof(firstHandle)); AssertIntEQ(ret, WS_SUCCESS); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } #endif #endif /* Ensure client buffer cleanup tolerates multiple invocations after allocs. */ static void TestClientBuffersIdempotent(void) { int ret; ret = ClientUsePubKey("keys/gretel-key-rsa.pub"); AssertIntEQ(ret, 0); ret = ClientSetPrivateKey("keys/gretel-key-rsa.pem"); AssertIntEQ(ret, 0); ClientFreeBuffers(); /* Should be safe to call again without double free. */ ClientFreeBuffers(); } /* Simulate Ctrl+D (stdin EOF) during password prompt; expect failure but no crash. */ static void TestPasswordEofNoCrash(void) { WS_UserAuthData auth; int savedStdin, devNull, ret; if (!isatty(STDIN_FILENO)) { return; /* headless/CI: skip tty-dependent check */ } WMEMSET(&auth, 0, sizeof(auth)); savedStdin = dup(STDIN_FILENO); devNull = open("/dev/null", O_RDONLY); AssertTrue(devNull >= 0); AssertTrue(dup2(devNull, STDIN_FILENO) >= 0); ret = ClientUserAuth(WOLFSSH_USERAUTH_PASSWORD, &auth, NULL); printf("TestPasswordEofNoCrash ret=%d\n", ret); AssertIntEQ(ret, WOLFSSH_USERAUTH_FAILURE); close(devNull); dup2(savedStdin, STDIN_FILENO); close(savedStdin); ClientFreeBuffers(); } /* When the send path is back-pressured (WANT_WRITE), wolfSSH_worker() * still needs to service Receive() so window-adjusts can arrive and * unblock the flow control. Verify the receive callback is invoked even * when the first send attempt would block. */ #ifndef WOLFSSH_TEST_BLOCK static int recvCallCount; static int WantWriteSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; (void)buf; (void)sz; (void)ctx; return WS_CBIO_ERR_WANT_WRITE; } static int WantReadRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; (void)buf; (void)sz; (void)ctx; recvCallCount++; return WS_CBIO_ERR_WANT_READ; } #ifndef WOLFSSH_TEST_BLOCK static void TestWorkerReadsWhenSendWouldBlock(void) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; int ret; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(ctx); wolfSSH_SetIOSend(ctx, WantWriteSend); wolfSSH_SetIORecv(ctx, WantReadRecv); ssh = wolfSSH_new(ctx); AssertNotNull(ssh); /* prime with pending outbound data so wolfSSH_SendPacket() is hit */ ssh->outputBuffer.length = 1; ssh->outputBuffer.idx = 0; ssh->outputBuffer.buffer[0] = 0; recvCallCount = 0; /* call worker; expect it to attempt send, notice back-pressure, and have * invoked recv once. Depending on how DoReceive handles WANT_READ, the * return may be WANT_WRITE or a fatal error; the important part is that * recv was exercised. */ ret = wolfSSH_worker(ssh, NULL); AssertTrue(ret == WS_WANT_WRITE || ret == WS_FATAL_ERROR); AssertIntEQ(recvCallCount, 1); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } #endif /* !WOLFSSH_TEST_BLOCK */ #endif #ifdef WOLFSSH_SFTP /* Test that wolfSSH_SFTP_buffer_send() properly handles WS_WANT_WRITE when * SSH output buffer has pending data. This is a regression test for * the SFTP hang issue with non-blocking sockets. * * The fix checks for pending data in ssh->outputBuffer at the start of * wolfSSH_SFTP_buffer_send() and returns WS_WANT_WRITE if the flush fails. */ static int sftpWantWriteCallCount = 0; static int SftpWantWriteSendCb(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; (void)buf; (void)ctx; sftpWantWriteCallCount++; /* First call returns WANT_WRITE, subsequent calls succeed */ if (sftpWantWriteCallCount == 1) { return WS_CBIO_ERR_WANT_WRITE; } return (int)sz; } static int SftpDummyRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; (void)buf; (void)sz; (void)ctx; return WS_CBIO_ERR_WANT_READ; } static void TestSftpBufferSendPendingOutput(void) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; byte testData[16]; int ret; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); AssertNotNull(ctx); wolfSSH_SetIOSend(ctx, SftpWantWriteSendCb); wolfSSH_SetIORecv(ctx, SftpDummyRecv); ssh = wolfSSH_new(ctx); AssertNotNull(ssh); WMEMSET(testData, 0x42, sizeof(testData)); /* Simulate pending data in SSH output buffer (as if previous send * returned WS_WANT_WRITE and data was buffered). * Note: outputBuffer is initialized by BufferInit() with bufferSz set * to at least STATIC_BUFFER_LEN (16 bytes), so we use a smaller value. */ ssh->outputBuffer.length = 8; /* 8 bytes pending */ ssh->outputBuffer.idx = 0; /* none sent yet */ sftpWantWriteCallCount = 0; /* Call wolfSSH_TestSftpBufferSend - should return WS_WANT_WRITE because * the fix detects pending data in outputBuffer and tries to flush it, * which fails with WS_WANT_WRITE from our callback. * * Before the fix, the function would ignore the pending SSH output buffer * data and proceed to send new SFTP data, leading to a hang because the * pending data was never flushed. */ ret = wolfSSH_TestSftpBufferSend(ssh, testData, sizeof(testData), 0); AssertIntEQ(ret, WS_WANT_WRITE); /* Verify the SSH output buffer still has pending data */ AssertTrue(ssh->outputBuffer.length > ssh->outputBuffer.idx); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); } #if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME) static void TestNucleusMonthConversion(void) { AssertIntEQ(wolfSSH_TestNucleusMonthFromDate((word16)(1U << 5)), 0); AssertIntEQ(wolfSSH_TestNucleusMonthFromDate((word16)(12U << 5)), 11); } #endif #endif /* WOLFSSH_SFTP */ #ifdef WOLFSSH_KEYBOARD_INTERACTIVE static int KbPreparePacketFailUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) { static byte* responses[1]; static word32 responseLens[1]; static byte response[] = "regress"; (void)ctx; if (authType != WOLFSSH_USERAUTH_KEYBOARD || authData == NULL) { return WOLFSSH_USERAUTH_INVALID_AUTHTYPE; } if (authData->sf.keyboard.promptCount != 1 || authData->sf.keyboard.prompts == NULL) { return WOLFSSH_USERAUTH_INVALID_PASSWORD; } responses[0] = response; responseLens[0] = (word32)sizeof(response) - 1; authData->sf.keyboard.responseCount = 1; authData->sf.keyboard.responseLengths = responseLens; authData->sf.keyboard.responses = responses; return WOLFSSH_USERAUTH_SUCCESS; } static void TestKeyboardResponsePreparePacketFailure(WOLFSSH* ssh, WOLFSSH_CTX* ctx) { byte* prompt; byte** prompts; byte* promptEcho; int ret; AssertNotNull(ssh); AssertNotNull(ctx); ResetSession(ssh); wolfSSH_SetUserAuth(ctx, KbPreparePacketFailUserAuth); prompt = (byte*)WMALLOC(9, ctx->heap, DYNTYPE_STRING); /* "Password" */ prompts = (byte**)WMALLOC(sizeof(byte*), ctx->heap, DYNTYPE_STRING); promptEcho = (byte*)WMALLOC(1, ctx->heap, DYNTYPE_STRING); AssertNotNull(prompt); AssertNotNull(prompts); AssertNotNull(promptEcho); WMEMCPY(prompt, "Password", 8); prompt[8] = '\0'; prompts[0] = prompt; promptEcho[0] = 0; ssh->kbAuth.promptCount = 1; ssh->kbAuth.prompts = prompts; ssh->kbAuth.promptEcho = promptEcho; ssh->kbAuth.promptName = NULL; ssh->kbAuth.promptInstruction = NULL; ssh->kbAuth.promptLanguage = NULL; /* Force PreparePacket() to fail with WS_OVERFLOW_E. */ ssh->outputBuffer.length = 0; ssh->outputBuffer.idx = 1; ret = SendUserAuthKeyboardResponse(ssh); AssertIntEQ(ret, WS_OVERFLOW_E); /* Ensure packet purge/reset happened cleanly. */ AssertIntEQ(ssh->outputBuffer.idx, 0); AssertIntEQ(ssh->outputBuffer.length, 0); /* Verify SendUserAuthKeyboardResponse() cleaned up kbAuth state. */ AssertIntEQ(ssh->kbAuth.promptCount, 0); AssertTrue(ssh->kbAuth.prompts == NULL); AssertTrue(ssh->kbAuth.promptEcho == NULL); } static void TestKeyboardResponseNoUserAuthCallback(WOLFSSH* ssh, WOLFSSH_CTX* ctx) { int ret; AssertNotNull(ssh); AssertNotNull(ctx); ResetSession(ssh); wolfSSH_SetUserAuth(ctx, NULL); ret = SendUserAuthKeyboardResponse(ssh); AssertIntEQ(ret, WS_INVALID_STATE_E); /* No packet should have been started. */ AssertIntEQ(ssh->outputBuffer.length, 0); AssertIntEQ(ssh->outputBuffer.idx, 0); } static void TestKeyboardResponseNullSsh(void) { int ret; ret = SendUserAuthKeyboardResponse(NULL); AssertIntEQ(ret, WS_BAD_ARGUMENT); } static void TestKeyboardResponseNullCtx(WOLFSSH* ssh) { WOLFSSH_CTX* savedCtx; int ret; AssertNotNull(ssh); savedCtx = ssh->ctx; ssh->ctx = NULL; ret = SendUserAuthKeyboardResponse(ssh); AssertIntEQ(ret, WS_BAD_ARGUMENT); ssh->ctx = savedCtx; } #endif /* WOLFSSH_KEYBOARD_INTERACTIVE */ int main(int argc, char** argv) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; (void)argc; (void)argv; wolfSSH_Init(); ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(ctx); ssh = wolfSSH_new(ctx); AssertNotNull(ssh); TestAuthMessageBlockedDuringKeying(ssh); TestUserauthFailureDuringKeying(ssh); TestPasswordLeakAborts(ssh); TestPrematureUserauthSuccess(ssh); TestChannelSpoofSequence(ssh); TestChannelSpoofAborts(ssh); TestPublicKeyFailureBeforeRequest(ssh); TestPublicKeyFailureAborts(ssh); TestChannelBlockedBeforeAuth(ssh); TestChannelAllowedAfterAuth(ssh); TestChannelOpenCallbackRejectSendsOpenFail(); #ifdef WOLFSSH_FWD TestDirectTcpipRejectSendsOpenFail(); TestDirectTcpipNoFwdCbSendsOpenFail(); TestGlobalRequestFwdNoCbSendsFailure(); TestGlobalRequestFwdNoCbNoReplyKeepsConnection(); TestGlobalRequestFwdWithCbSendsSuccess(); TestGlobalRequestFwdCancelNoCbSendsFailure(); TestGlobalRequestFwdCancelWithCbSendsSuccess(); #endif #ifdef WOLFSSH_AGENT TestAgentChannelNullAgentSendsOpenFail(); #endif TestKexInitRejectedWhenKeying(ssh); TestDisconnectSetsDisconnectError(); TestClientBuffersIdempotent(); TestPasswordEofNoCrash(); #ifndef WOLFSSH_TEST_BLOCK TestWorkerReadsWhenSendWouldBlock(); #endif #ifdef KEXDH_REPLY_REGRESS_KEX_ALGO #ifndef WOLFSSH_NO_RSA_SHA2_256 TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(); #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 TestKexDhReplyRejectsRsaSha2_512SigNameDowngrade(); #endif TestKexDhReplyRejectsNoPublicKeyCheck(); #endif #ifdef WOLFSSH_SFTP TestOct2DecRejectsInvalidNonLeadingDigit(); #ifdef WOLFSSH_STOREHANDLE TestSftpRemoveHandleHeadUpdate(); #endif TestSftpBufferSendPendingOutput(); #if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME) TestNucleusMonthConversion(); #endif #endif #ifdef WOLFSSH_KEYBOARD_INTERACTIVE TestKeyboardResponsePreparePacketFailure(ssh, ctx); TestKeyboardResponseNoUserAuthCallback(ssh, ctx); TestKeyboardResponseNullSsh(); TestKeyboardResponseNullCtx(ssh); #endif /* TODO: add app-level regressions that simulate stdin EOF/password * prompts and mid-session socket closes once the test harness can * drive the wolfssh client without real sockets/tty. */ ResetSession(ssh); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); wolfSSH_Cleanup(); printf("regress: PASS\n"); return 0; }