From 09753032d00d483d2a841277da4e5fb770630047 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Fri, 18 Sep 2026 10:21:24 +0900 Subject: [PATCH 1/2] agent, apps, examples: relay whole agent messages - src/agent.c gains AgentWriteAll(), AgentReadFull(), AgentReadMessage() and AgentWriteMessage(), which loop the agentIoCb, bound a declared length by the new WOLFSSH_AGENT_MAX_MSG_SZ, and reconnect once when a write finds the socket dead. wolfSSH_AGENT_Relay() is rebuilt on them. - New WOLFSSH_API wolfSSH_AGENT_RelayChannel() frames the channel side, holding a partial request and an unfinished reply on WOLFSSH_AGENT_CTX in two WOLFSSH_BUFFERs whose whole allocation is zeroed as each message is consumed. It answers WS_WANT_WRITE, WS_WINDOW_FULL or WS_REKEYING while a reply is owed, flushes what is left queued below under those last two, and answers WS_WANT_WRITE while any of it is still queued. - readPeer() in apps/wolfssh/wolfssh.c and examples/client/client.c calls it, drops its local ato32() and 512-byte buffer, and arms a select() write set for the transport's own want-write. While a reply is owed it ends the read loop so the retry above select() runs, and examples/client/client.c times its select() out at one second. - tests/api.c adds nine wolfSSH_AGENT_Relay() cases and tests/regress.c twenty-two for wolfSSH_AGENT_RelayChannel(). Issue: F-10545 --- apps/wolfssh/wolfssh.c | 95 +++-- examples/client/client.c | 91 +++-- src/agent.c | 404 +++++++++++++++++-- tests/api.c | 324 ++++++++++++++- tests/regress.c | 847 +++++++++++++++++++++++++++++++++++++++ wolfssh/agent.h | 17 + 6 files changed, 1677 insertions(+), 101 deletions(-) diff --git a/apps/wolfssh/wolfssh.c b/apps/wolfssh/wolfssh.c index c546c5c81..79d84cd48 100644 --- a/apps/wolfssh/wolfssh.c +++ b/apps/wolfssh/wolfssh.c @@ -263,13 +263,6 @@ static void modes_reset(void) #ifndef WOLFSSL_NUCLEUS -#if defined(WOLFSSH_AGENT) -static inline void ato32(const byte* c, word32* u32) -{ - *u32 = (c[0] << 24) | (c[1] << 16) | (c[2] << 8) | c[3]; -} -#endif - typedef struct thread_args { WOLFSSH* ssh; wolfSSL_Mutex lock; @@ -582,6 +575,18 @@ static THREAD_RET readInput(void* in) } +#ifdef WOLFSSH_AGENT +/* Statuses that say the relay still owes a reply rather than failing. */ +static int AgentRelayHeld(int code, int* wantsWrite) +{ + *wantsWrite = (code == WS_WANT_WRITE); + return code == WS_WANT_WRITE || code == WS_WINDOW_FULL + || code == WS_REKEYING; +} + + +#endif + static THREAD_RET readPeer(void* in) { byte buf[256]; @@ -595,8 +600,14 @@ static THREAD_RET readPeer(void* in) HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); #endif fd_set readSet; + fd_set writeSet; fd_set errSet; struct timeval timeout; +#ifdef WOLFSSH_AGENT + word32 agentChannel = 0; + int agentOwed = 0; + int agentWantsWrite = 0; +#endif #ifdef USE_WINDOWS_API if (args->rawMode == 0) { @@ -630,13 +641,19 @@ static THREAD_RET readPeer(void* in) (void)windowMonitor(args); #endif FD_ZERO(&readSet); + FD_ZERO(&writeSet); FD_ZERO(&errSet); FD_SET(fd, &readSet); FD_SET(fd, &errSet); +#ifdef WOLFSSH_AGENT + if (agentWantsWrite) + FD_SET(fd, &writeSet); +#endif + timeout.tv_sec = 1; timeout.tv_usec = 0; - bytes = select(fd + 1, &readSet, NULL, &errSet, &timeout); + bytes = select(fd + 1, &readSet, &writeSet, &errSet, &timeout); if (bytes < 0) { #ifdef USE_WINDOWS_API if (WSAGetLastError() == WSAEINTR) @@ -659,6 +676,19 @@ static THREAD_RET readPeer(void* in) FD_SET(fd, &readSet); } wc_LockMutex(&args->lock); +#ifdef WOLFSSH_AGENT + /* Retry the owed reply on any wake */ + if (agentOwed && !stop) { + int relayRet = wolfSSH_AGENT_RelayChannel(args->ssh, agentChannel); + + agentOwed = AgentRelayHeld(relayRet, &agentWantsWrite); + if (relayRet < 0 && !agentOwed) { + args->readError = relayRet; + stop = 1; + bytes = 0; + } + } +#endif while (bytes > 0 && (FD_ISSET(fd, &readSet) || FD_ISSET(fd, &errSet))) { /* there is something to read off the wire */ WMEMSET(buf, 0, bufSz); @@ -687,36 +717,31 @@ static THREAD_RET readPeer(void* in) } #ifdef WOLFSSH_AGENT else if (err == WS_CHAN_RXD) { - byte agentBuf[512]; - int rxd, txd; word32 channel = 0; - wolfSSH_GetLastRxId(args->ssh, &channel); - rxd = wolfSSH_ChannelIdRead(args->ssh, channel, - agentBuf, sizeof(agentBuf)); - if (rxd > 4) { - word32 msgSz = 0; - - ato32(agentBuf, &msgSz); - if (msgSz > (word32)rxd - 4) { - rxd += wolfSSH_ChannelIdRead(args->ssh, channel, - agentBuf + rxd, - sizeof(agentBuf) - rxd); - } - - txd = rxd; - rxd = sizeof(agentBuf); - ret = wolfSSH_AGENT_Relay(args->ssh, - agentBuf, (word32*)&txd, - agentBuf, (word32*)&rxd); - if (ret == WS_SUCCESS) { - ret = wolfSSH_ChannelIdSend(args->ssh, channel, - agentBuf, rxd); - } - } - WMEMSET(agentBuf, 0, sizeof(agentBuf)); - continue; + if (wolfSSH_GetLastRxId(args->ssh, &channel) + == WS_SUCCESS) { + ret = wolfSSH_AGENT_RelayChannel(args->ssh, channel); + agentChannel = channel; + agentOwed = AgentRelayHeld(ret, &agentWantsWrite); + } + else { + ret = WS_FATAL_ERROR; + agentOwed = 0; } + if (ret < 0 && !agentOwed) { + /* Stop the read thread; the channel is no longer + * safe to send on. */ + args->readError = ret; + stop = 1; + bytes = 0; + } + /* Back to select(), which is where the owed reply is + * retried. */ + if (agentOwed) + bytes = 0; + continue; + } #endif /* WOLFSSH_AGENT */ else if (err == WS_CBIO_ERR_CONN_CLOSE || err == WS_SOCKET_ERROR_E || diff --git a/examples/client/client.c b/examples/client/client.c index 0041bd361..628c141f0 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -401,11 +401,16 @@ static THREAD_RET readInput(void* in) return THREAD_RET_SUCCESS; } -#if defined(WOLFSSH_AGENT) -static inline void ato32(const byte* c, word32* u32) +#ifdef WOLFSSH_AGENT +/* Statuses that say the relay still owes a reply rather than failing. */ +static int AgentRelayHeld(int code, int* wantsWrite) { - *u32 = (c[0] << 24) | (c[1] << 16) | (c[2] << 8) | c[3]; + *wantsWrite = (code == WS_WANT_WRITE); + return code == WS_WANT_WRITE || code == WS_WINDOW_FULL + || code == WS_REKEYING; } + + #endif static THREAD_RET readPeer(void* in) @@ -420,7 +425,14 @@ static THREAD_RET readPeer(void* in) HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); #endif fd_set readSet; + fd_set writeSet; fd_set errSet; +#ifdef WOLFSSH_AGENT + struct timeval timeout; + word32 agentChannel = 0; + int agentOwed = 0; + int agentWantsWrite = 0; +#endif #ifdef USE_WINDOWS_API if (args->rawMode == 0) { @@ -456,11 +468,26 @@ static THREAD_RET readPeer(void* in) /* select() clears the sets, re-arm them every pass. */ FD_ZERO(&readSet); + FD_ZERO(&writeSet); FD_ZERO(&errSet); FD_SET(fd, &readSet); FD_SET(fd, &errSet); - bytes = select(fd + 1, &readSet, NULL, &errSet, NULL); +#ifdef WOLFSSH_AGENT + /* Only a reply the transport is holding waits on a writable socket. + * One held by the peer's window or a rekey waits on the timeout. */ + if (agentWantsWrite) + FD_SET(fd, &writeSet); + + /* Time out so an owed reply is retried on a silent peer. Without one + * the wait stays event driven. */ + timeout.tv_sec = 1; + timeout.tv_usec = 0; + bytes = select(fd + 1, &readSet, &writeSet, &errSet, + agentOwed ? &timeout : NULL); +#else + bytes = select(fd + 1, &readSet, &writeSet, &errSet, NULL); +#endif if (bytes < 0) { #ifdef USE_WINDOWS_API if (WSAGetLastError() == WSAEINTR) @@ -475,6 +502,18 @@ static THREAD_RET readPeer(void* in) break; } wc_LockMutex(&args->lock); +#ifdef WOLFSSH_AGENT + /* Retry the owed reply on any wake */ + if (agentOwed) { + int relayRet = wolfSSH_AGENT_RelayChannel(args->ssh, agentChannel); + + agentOwed = AgentRelayHeld(relayRet, &agentWantsWrite); + if (relayRet < 0 && !agentOwed) { + wc_UnLockMutex(&args->lock); + break; + } + } +#endif while (bytes > 0 && (FD_ISSET(fd, &readSet) || FD_ISSET(fd, &errSet))) { /* there is something to read off the wire */ WMEMSET(buf, 0, bufSz); @@ -504,34 +543,28 @@ static THREAD_RET readPeer(void* in) } #ifdef WOLFSSH_AGENT else if (ret == WS_CHAN_RXD) { - byte agentBuf[512]; - int rxd, txd; word32 channel = 0; - wolfSSH_GetLastRxId(args->ssh, &channel); - rxd = wolfSSH_ChannelIdRead(args->ssh, channel, - agentBuf, sizeof(agentBuf)); - if (rxd > 4) { - word32 msgSz = 0; - - ato32(agentBuf, &msgSz); - if (msgSz > (word32)rxd - 4) { - rxd += wolfSSH_ChannelIdRead(args->ssh, channel, - agentBuf + rxd, - sizeof(agentBuf) - rxd); - } - - txd = rxd; - rxd = sizeof(agentBuf); - ret = wolfSSH_AGENT_Relay(args->ssh, - agentBuf, (word32*)&txd, - agentBuf, (word32*)&rxd); - if (ret == WS_SUCCESS) { - ret = wolfSSH_ChannelIdSend(args->ssh, channel, - agentBuf, rxd); - } + if (wolfSSH_GetLastRxId(args->ssh, &channel) + == WS_SUCCESS) { + ret = wolfSSH_AGENT_RelayChannel(args->ssh, + channel); + agentChannel = channel; + agentOwed = AgentRelayHeld(ret, &agentWantsWrite); + } + else { + ret = WS_FATAL_ERROR; + agentOwed = 0; + } + if (ret < 0 && !agentOwed) { + /* Leave the read loop; the channel is no + * longer safe to send on. */ + break; } - WMEMSET(agentBuf, 0, sizeof(agentBuf)); + /* Back to select(), which is where the owed reply + * is retried. */ + if (agentOwed) + bytes = 0; continue; } #endif /* WOLFSSH_AGENT */ diff --git a/src/agent.c b/src/agent.c index 7f8d5fe7d..928c91cd3 100644 --- a/src/agent.c +++ b/src/agent.c @@ -72,6 +72,17 @@ #define WOLFSSH_AGENT_MAX_RSP_SZ 2048 #endif +/* Largest agent message this build will handle. The peer declares the + * length, so it is bounded before it drives an allocation. */ +#ifndef WOLFSSH_AGENT_MAX_MSG_SZ + #define WOLFSSH_AGENT_MAX_MSG_SZ 32768 +#endif + +/* Starting size and growth step for the channel accumulation buffer. */ +#ifndef WOLFSSH_AGENT_RELAY_CHUNK_SZ + #define WOLFSSH_AGENT_RELAY_CHUNK_SZ 512 +#endif + /* payloadSz is an estimate, but it shall be greater-than/equal-to * the actual value. */ static int PrepareMessage(WOLFSSH_AGENT_CTX* agent, word32 payloadSz) @@ -1568,6 +1579,60 @@ static WOLFSSH_AGENT_CTX* AgentInit(WOLFSSH_AGENT_CTX* agent, void* heap) } +/* Makes room for needSz bytes past what the buffer still holds unread, + * allocating the buffer on first use. */ +static int AgentBufferPrep(WOLFSSH_AGENT_CTX* agent, + WOLFSSH_BUFFER** bufPtr, word32 needSz) +{ + WOLFSSH_BUFFER* buf; + int ret = WS_SUCCESS; + + buf = *bufPtr; + + if (buf == NULL) { + buf = (WOLFSSH_BUFFER*)WMALLOC(sizeof(*buf), agent->heap, + DYNTYPE_AGENT_BUFFER); + if (buf == NULL) + ret = WS_MEMORY_E; + else { + ret = BufferInit(buf, 0, agent->heap); + if (ret != WS_SUCCESS) + WFREE(buf, agent->heap, DYNTYPE_AGENT_BUFFER); + else + *bufPtr = buf; + } + } + + if (ret == WS_SUCCESS) + ret = GrowBuffer(buf, needSz); + + return ret; +} + + +/* Drops whatever the buffer holds, keeping the allocation. */ +static void AgentBufferReset(WOLFSSH_BUFFER* buf) +{ + if (buf != NULL) { + if (buf->bufferSz > 0) + WS_FORCEZERO(buf->buffer, buf->bufferSz); + buf->length = 0; + buf->idx = 0; + } +} + + +/* Releases one of the agent's buffers and its contents. */ +static void AgentBufferFree(WOLFSSH_AGENT_CTX* agent, WOLFSSH_BUFFER** bufPtr) +{ + if (*bufPtr != NULL) { + ShrinkBuffer(*bufPtr, 1); + WFREE(*bufPtr, agent->heap, DYNTYPE_AGENT_BUFFER); + *bufPtr = NULL; + } +} + + WOLFSSH_AGENT_CTX* wolfSSH_AGENT_new(void* heap) { WOLFSSH_AGENT_CTX* agent; @@ -1592,6 +1657,8 @@ void wolfSSH_AGENT_free(WOLFSSH_AGENT_CTX* agent) heap = agent->heap; if (agent->msg != NULL) WFREE(agent->msg, agent->heap, DYNTYPE_AGENT_BUFFER); + AgentBufferFree(agent, &agent->rxBuf); + AgentBufferFree(agent, &agent->relayBuf); wc_FreeRng(&agent->rng); wolfSSH_AGENT_ID_list_free(agent->idList, heap); WMEMSET(agent, 0, sizeof(*agent)); @@ -1845,11 +1912,130 @@ int wolfSSH_AGENT_worker(WOLFSSH* ssh) } +/* Writes the whole buffer to the agent, following up a short write. Reports + * the count sent in writtenSz, so a caller can tell a failure that sent + * nothing from one that sent part of the message. */ +static int AgentWriteAll(WOLFSSH* ssh, const byte* buf, word32 bufSz, + word32* writtenSz) +{ + word32 idx = 0; + int ret = WS_SUCCESS; + int sz; + + while (ret == WS_SUCCESS && idx < bufSz) { + sz = ssh->ctx->agentIoCb(WOLFSSH_AGENT_IO_WRITE, + (byte*)(buf + idx), bufSz - idx, ssh->agentCbCtx); + if (sz <= 0 || (word32)sz > bufSz - idx) + ret = WS_AGENT_CXN_FAIL; + else + idx += (word32)sz; + } + + if (writtenSz != NULL) + *writtenSz = idx; + + return ret; +} + + +/* Writes one whole message to the agent, reconnecting once when the socket + * is dead and nothing went out. */ +static int AgentWriteMessage(WOLFSSH* ssh, const byte* buf, word32 bufSz) +{ + word32 wroteSz = 0; + int ret; + + ret = AgentWriteAll(ssh, buf, bufSz, &wroteSz); + if (ret != WS_SUCCESS && wroteSz == 0 && ssh->ctx->agentCb) { + /* Only retry when nothing went out */ + ret = ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, ssh->agentCbCtx); + if (ret != WS_AGENT_SUCCESS) + ret = WS_AGENT_CXN_FAIL; + else + ret = AgentWriteAll(ssh, buf, bufSz, NULL); + } + + return ret; +} + + +/* Reads exactly bufSz bytes from the agent, looping over short reads. */ +static int AgentReadFull(WOLFSSH* ssh, byte* buf, word32 bufSz) +{ + word32 idx = 0; + int ret = WS_SUCCESS; + int sz; + + while (ret == WS_SUCCESS && idx < bufSz) { + sz = ssh->ctx->agentIoCb(WOLFSSH_AGENT_IO_READ, + buf + idx, bufSz - idx, ssh->agentCbCtx); + if (sz <= 0 || (word32)sz > bufSz - idx) + ret = WS_AGENT_CXN_FAIL; + else + idx += (word32)sz; + } + + return ret; +} + + +/* Reads one whole agent message into ssh->agent->rxBuf, length prefix and + * all, so msgSz counts those four bytes. msg and msgSz may be NULL. */ +static int AgentReadMessage(WOLFSSH* ssh, byte** msg, word32* msgSz) +{ + WOLFSSH_AGENT_CTX* agent; + byte hdr[LENGTH_SZ]; + word32 payloadSz = 0; + word32 wholeSz = 0; + int ret; + + agent = ssh->agent; + ret = AgentReadFull(ssh, hdr, (word32)sizeof(hdr)); + + if (ret == WS_SUCCESS) { + word32 begin = 0; + + GetUint32(&payloadSz, hdr, (word32)sizeof(hdr), &begin); + if (payloadSz == 0 || payloadSz > WOLFSSH_AGENT_MAX_MSG_SZ) { + WLOG(WS_LOG_AGENT, "agent message size %u out of range", payloadSz); + ret = WS_BUFFER_E; + } + else { + wholeSz = payloadSz + LENGTH_SZ; + } + } + + /* The last reply is spent, so drop it before asking for room. */ + if (ret == WS_SUCCESS) + AgentBufferReset(agent->rxBuf); + + if (ret == WS_SUCCESS) + ret = AgentBufferPrep(agent, &agent->rxBuf, wholeSz); + + if (ret == WS_SUCCESS) { + WMEMCPY(agent->rxBuf->buffer, hdr, LENGTH_SZ); + ret = AgentReadFull(ssh, agent->rxBuf->buffer + LENGTH_SZ, payloadSz); + } + + if (ret == WS_SUCCESS) { + agent->rxBuf->length = wholeSz; + if (msg != NULL) + *msg = agent->rxBuf->buffer; + if (msgSz != NULL) + *msgSz = wholeSz; + } + + return ret; +} + + int wolfSSH_AGENT_Relay(WOLFSSH* ssh, const byte* msg, word32* msgSz, byte* rsp, word32* rspSz) { WOLFSSH_AGENT_CTX* agent = NULL; - int ret = WS_SUCCESS, sz; + byte* agentRsp = NULL; + word32 agentRspSz = 0; + int ret = WS_SUCCESS; WLOG_ENTER(); @@ -1884,40 +2070,25 @@ int wolfSSH_AGENT_Relay(WOLFSSH* ssh, if (ret == WS_SUCCESS) { /* Write msg to the agent socket. */ - sz = ssh->ctx->agentIoCb(WOLFSSH_AGENT_IO_WRITE, - (byte*)msg, *msgSz, ssh->agentCbCtx); - if (sz > 0) { - *msgSz = (word32)sz; - } - else { - if (sz == WS_CBIO_ERR_GENERAL) { - if (ssh->ctx->agentCb) { - ret = ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, - ssh->agentCbCtx); - if (ret != WS_AGENT_SUCCESS) - ret = WS_AGENT_CXN_FAIL; - } - - if (ret == WS_AGENT_SUCCESS) { - sz = ssh->ctx->agentIoCb(WOLFSSH_AGENT_IO_WRITE, - (byte*)msg, *msgSz, ssh->agentCbCtx); - if (sz > 0) - *msgSz = (word32)sz; - else - ret = WS_AGENT_CXN_FAIL; - } - } - } + ret = AgentWriteMessage(ssh, msg, *msgSz); } if (ret == WS_SUCCESS) { - sz = ssh->ctx->agentIoCb(WOLFSSH_AGENT_IO_READ, - rsp, *rspSz, ssh->agentCbCtx); - if (sz > 0) - *rspSz = (word32)sz; - else - ret = WS_AGENT_CXN_FAIL; + ret = AgentReadMessage(ssh, &agentRsp, &agentRspSz); + } + if (ret == WS_SUCCESS) { + if (agentRspSz > *rspSz) { + WLOG(WS_LOG_AGENT, "agent reply too large for the caller buffer"); + ret = WS_BUFFER_E; + } + else { + WMEMCPY(rsp, agentRsp, agentRspSz); + *rspSz = agentRspSz; + } + /* The caller has the reply now, so it is not one this session still + * owes the channel. */ + AgentBufferReset(ssh->agent->rxBuf); } if (ret == WS_AGENT_SUCCESS) @@ -1935,6 +2106,177 @@ int wolfSSH_AGENT_Relay(WOLFSSH* ssh, } +/* Sends what is left of the agent's reply, tracking it in rxBuf->idx. + * Returns WS_WANT_WRITE while any of the reply is still owed. */ +static int AgentRelaySendReply(WOLFSSH* ssh, word32 channelId) +{ + WOLFSSH_BUFFER* rsp; + word32 flushes; + int ret = WS_SUCCESS; + int txd; + + rsp = ssh->agent->rxBuf; + + if (rsp != NULL && rsp->idx < rsp->length) { + flushes = ssh->txFlushCount; + txd = wolfSSH_ChannelIdSend(ssh, channelId, rsp->buffer + rsp->idx, + rsp->length - rsp->idx); + + /* The send names what holds an unfinished reply, so pass it on. */ + if (txd > 0) + rsp->idx += (word32)txd; + else if (txd == WS_WANT_WRITE || txd == WS_WINDOW_FULL + || txd == WS_REKEYING) + ret = txd; + else if (SendPacketDelivered(ssh, flushes, txd)) + /* The peer has the bytes under a failing return, and how many is + * not recoverable, so the reply cannot resume. */ + ret = WS_AGENT_CXN_FAIL; + else if (txd < 0) + ret = txd; + else + ret = WS_AGENT_CXN_FAIL; + } + + /* Queued bytes still have to go out, whatever holds the reply. */ + if ((ret == WS_SUCCESS || ret == WS_WINDOW_FULL || ret == WS_REKEYING) + && wolfSSH_OutputPending(ssh)) { + txd = wolfSSH_SendPacket(ssh); + if (txd != WS_SUCCESS && txd != WS_WANT_WRITE) + ret = txd; + else if (wolfSSH_OutputPending(ssh)) + /* Bytes still queued wait on a writable socket. */ + ret = WS_WANT_WRITE; + } + + /* Done only once every byte is out of rxBuf. */ + if (ret == WS_SUCCESS && rsp != NULL && rsp->idx < rsp->length) + ret = WS_WANT_WRITE; + + if (ret == WS_SUCCESS) + AgentBufferReset(rsp); + + return ret; +} + + +int wolfSSH_AGENT_RelayChannel(WOLFSSH* ssh, word32 channelId) +{ + WOLFSSH_AGENT_CTX* agent = NULL; + WOLFSSH_BUFFER* in = NULL; + word32 msgSz = 0; + word32 wholeSz; + word32 begin; + int ret = WS_SUCCESS; + int rxd; + int progress; + + WLOG_ENTER(); + + if (ssh == NULL) + ret = WS_SSH_NULL_E; + + if (ret == WS_SUCCESS) { + if (ssh->agent == NULL) + ret = WS_AGENT_NULL_E; + } + + if (ret == WS_SUCCESS) { + agent = ssh->agent; + + /* A different channel means the held bytes belong to a conversation + * that is over, so they are dropped rather than prepended. */ + if (!agent->relayActive || agent->relayChannel != channelId) { + AgentBufferReset(agent->relayBuf); + AgentBufferReset(agent->rxBuf); + agent->relayChannel = channelId; + agent->relayActive = 1; + } + + if (agent->state == AGENT_STATE_INIT && ssh->ctx->agentCb) { + ret = ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP, + ssh->agentCbCtx); + if (ret == WS_AGENT_SUCCESS) + agent->state = AGENT_STATE_CONNECTED; + else + ret = WS_AGENT_CXN_FAIL; + } + } + + /* Finish the last call's reply first */ + if (ret == WS_SUCCESS) + ret = AgentRelaySendReply(ssh, channelId); + + /* One read reports only what is buffered now, so keep going while + * anything moves. */ + while (ret == WS_SUCCESS) { + progress = 0; + + ret = AgentBufferPrep(agent, &agent->relayBuf, + WOLFSSH_AGENT_RELAY_CHUNK_SZ); + if (ret != WS_SUCCESS) + break; + in = agent->relayBuf; + + rxd = wolfSSH_ChannelIdRead(ssh, channelId, in->buffer + in->length, + in->bufferSz - in->length); + if (rxd < 0) + ret = rxd; + else if (rxd > 0) { + in->length += (word32)rxd; + progress = 1; + } + + while (ret == WS_SUCCESS && in->length - in->idx >= LENGTH_SZ) { + begin = in->idx; + GetUint32(&msgSz, in->buffer, in->length, &begin); + if (msgSz == 0 || msgSz > WOLFSSH_AGENT_MAX_MSG_SZ) { + WLOG(WS_LOG_AGENT, "agent message size %u out of range", msgSz); + AgentBufferReset(in); + ret = WS_BUFFER_E; + break; + } + + wholeSz = msgSz + LENGTH_SZ; + if (in->length - in->idx < wholeSz) { + /* Make room for the rest and wait for it. */ + ret = AgentBufferPrep(agent, &agent->relayBuf, wholeSz); + in = agent->relayBuf; + break; + } + + ret = AgentWriteMessage(ssh, in->buffer + in->idx, wholeSz); + /* An add-identity request carries a private key. */ + WS_FORCEZERO(in->buffer + in->idx, wholeSz); + in->idx += wholeSz; + + if (ret == WS_SUCCESS) + ret = AgentReadMessage(ssh, NULL, NULL); + + if (ret == WS_SUCCESS) + ret = AgentRelaySendReply(ssh, channelId); + + progress = 1; + } + + if (!progress) + break; + } + + /* A read late in the loop can credit the window with nothing left to + * frame, so drive and report that write too. */ + if (ret == WS_SUCCESS) + ret = AgentRelaySendReply(ssh, channelId); + + if (ret != WS_SUCCESS && ret != WS_WANT_WRITE && ret != WS_WINDOW_FULL + && ret != WS_REKEYING && agent != NULL) + agent->error = ret; + + WLOG_LEAVE(ret); + return ret; +} + + int wolfSSH_AGENT_SignRequest(WOLFSSH* ssh, const byte* digest, word32 digestSz, byte* sig, word32* sigSz, diff --git a/tests/api.c b/tests/api.c index 51e291d08..1e2dfd501 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2911,18 +2911,29 @@ typedef struct AgentTestCtx { int partialWrite; byte response[AGENT_TEST_BUF_SZ]; word32 responseSz; + word32 readIdx; + word32 readChunk; + int failWriteCall; + int failSetupCall; + int setupCalls; int writeCalls; int readCalls; } AgentTestCtx; static int test_agent_cb(WS_AgentCbAction action, void* ctx) { - (void)ctx; + AgentTestCtx* io = (AgentTestCtx*)ctx; - if (action == WOLFSSH_AGENT_LOCAL_SETUP || - action == WOLFSSH_AGENT_LOCAL_CLEANUP) { + if (action == WOLFSSH_AGENT_LOCAL_SETUP) { + if (io != NULL) { + io->setupCalls++; + if (io->failSetupCall == io->setupCalls) + return WS_AGENT_SETUP_E; + } return WS_AGENT_SUCCESS; } + if (action == WOLFSSH_AGENT_LOCAL_CLEANUP) + return WS_AGENT_SUCCESS; return WS_AGENT_INVALID_ACTION; } @@ -2971,9 +2982,12 @@ static int test_agent_io_cb(WS_AgentIoCbAction action, void* buf, word32 bufSz, void* ctx) { AgentTestCtx* io = (AgentTestCtx*)ctx; + word32 avail; if (action == WOLFSSH_AGENT_IO_WRITE) { io->writeCalls++; + if (io->failWriteCall == io->writeCalls) + return WS_CBIO_ERR_GENERAL; if (io->partialWrite && bufSz > 0) { io->partialWrite = 0; return (int)(bufSz - 1); @@ -2982,10 +2996,20 @@ static int test_agent_io_cb(WS_AgentIoCbAction action, void* buf, word32 bufSz, } io->readCalls++; - if (io->responseSz == 0 || bufSz < io->responseSz) + avail = io->responseSz - io->readIdx; + if (avail == 0) return 0; - memcpy(buf, io->response, io->responseSz); - return (int)io->responseSz; + if (avail > bufSz) + avail = bufSz; + if (io->readChunk > 0 && avail > io->readChunk) + avail = io->readChunk; + memcpy(buf, io->response + io->readIdx, avail); + io->readIdx += avail; + /* Replay the canned reply once it has been drained, so a test that runs + * several exchanges against one context gets an answer each time. */ + if (io->readIdx == io->responseSz) + io->readIdx = 0; + return (int)avail; } static void setup_agent_test(WOLFSSH_CTX** ctx, WOLFSSH** ssh, AgentTestCtx* io) @@ -3592,6 +3616,285 @@ static void test_wolfSSH_agent_signrequest_rsa_too_large(void) } #endif /* RSA_MAX_SIZE fits AGENT_TEST_BUF_SZ */ #endif /* WOLFSSH_NO_RSA_SHA2_256 */ + +/* Appends a whole agent message to the canned response stream. */ +static word32 append_agent_message(AgentTestCtx* ctx, byte id, byte fill, + word32 bodySz) +{ + byte body[256]; + byte msg[AGENT_TEST_BUF_SZ]; + word32 msgSz; + + AssertTrue(bodySz <= sizeof(body)); + memset(body, fill, bodySz); + build_agent_message(msg, &msgSz, id, body, bodySz); + AssertTrue(ctx->responseSz + msgSz <= sizeof(ctx->response)); + memcpy(ctx->response + ctx->responseSz, msg, msgSz); + ctx->responseSz += msgSz; + + return msgSz; +} + +/* A reply arriving a few bytes at a time is one message, not several. */ +static void test_wolfSSH_agent_relay_reply_split_reads(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + word32 msgSz; + + memset(&io, 0, sizeof(io)); + io.readChunk = 7; + msgSz = append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x5a, 195); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_SUCCESS); + AssertIntEQ(rspSz, msgSz); + AssertTrue(memcmp(rsp, io.response, msgSz) == 0); + AssertTrue(io.readCalls > 1); + + cleanup_agent_test(ctx, ssh); +} + +/* Two replies queued back to back: the first call must take exactly its own + * message so the second call is not answered with the first one's tail. */ +static void test_wolfSSH_agent_relay_reply_desync(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + word32 firstSz; + word32 secondSz; + + memset(&io, 0, sizeof(io)); + firstSz = append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x11, 100); + secondSz = append_agent_message(&io, MSGID_AGENT_FAILURE, 0x22, 60); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_SUCCESS); + AssertIntEQ(rspSz, firstSz); + AssertTrue(memcmp(rsp, io.response, firstSz) == 0); + + requestSz = 0; + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + rspSz = sizeof(rsp); + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_SUCCESS); + AssertIntEQ(rspSz, secondSz); + AssertTrue(memcmp(rsp, io.response + firstSz, secondSz) == 0); + + cleanup_agent_test(ctx, ssh); +} + +/* A short write to the agent is finished, not reported as a dead socket. */ +static void test_wolfSSH_agent_relay_short_write(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + word32 msgSz; + + memset(&io, 0, sizeof(io)); + io.partialWrite = 1; + msgSz = append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x33, 32); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_SUCCESS); + AssertIntEQ(rspSz, msgSz); + AssertIntEQ(io.writeCalls, 2); + + cleanup_agent_test(ctx, ssh); +} + +/* A reply header declaring more than WOLFSSH_AGENT_MAX_MSG_SZ is refused on + * the header alone, with no body read and nothing allocated for one. */ +static void test_wolfSSH_agent_relay_oversize_reply(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + + memset(&io, 0, sizeof(io)); + put_uint32(io.response, 0x00100000); + io.responseSz = LENGTH_SZ; + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_BUFFER_E); + /* One read, for the header. Without the ceiling the body read would run + * on and the caller-buffer check would raise the same WS_BUFFER_E. */ + AssertIntEQ(io.readCalls, 1); + + cleanup_agent_test(ctx, ssh); +} + +/* A zero declared length carries no message id and is refused. */ +static void test_wolfSSH_agent_relay_zero_length(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + + memset(&io, 0, sizeof(io)); + put_uint32(io.response, 0); + io.responseSz = LENGTH_SZ; + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_BUFFER_E); + + cleanup_agent_test(ctx, ssh); +} + +/* A write that sends nothing is taken as a dead socket: reconnect once and + * send the message again. */ +static void test_wolfSSH_agent_relay_reconnects_on_dead_socket(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + word32 msgSz; + + memset(&io, 0, sizeof(io)); + io.failWriteCall = 1; + msgSz = append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x66, 32); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_SUCCESS); + AssertIntEQ(rspSz, msgSz); + /* The opening connect plus the reconnect. */ + AssertIntEQ(io.setupCalls, 2); + AssertIntEQ(io.writeCalls, 2); + + cleanup_agent_test(ctx, ssh); +} + +/* A reconnect the callback refuses is reported as a connection failure. */ +static void test_wolfSSH_agent_relay_reconnect_failure(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + + memset(&io, 0, sizeof(io)); + io.failWriteCall = 1; + io.failSetupCall = 2; + append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x77, 32); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_AGENT_CXN_FAIL); + AssertIntEQ(io.setupCalls, 2); + AssertIntEQ(io.readCalls, 0); + + cleanup_agent_test(ctx, ssh); +} + +/* A write that lands part of a message and then fails is not retried: the + * agent would see the leading bytes twice. */ +static void test_wolfSSH_agent_relay_no_retry_after_partial_write(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[AGENT_TEST_BUF_SZ]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + + memset(&io, 0, sizeof(io)); + io.partialWrite = 1; + io.failWriteCall = 2; + append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x55, 32); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_AGENT_CXN_FAIL); + /* The opening connect only; no reconnect was attempted. */ + AssertIntEQ(io.setupCalls, 1); + AssertIntEQ(io.writeCalls, 2); + AssertIntEQ(io.readCalls, 0); + + cleanup_agent_test(ctx, ssh); +} + +/* A reply the caller has no room for is an error, not a truncated message. */ +static void test_wolfSSH_agent_relay_reply_exceeds_caller_buf(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + AgentTestCtx io; + byte request[16] = {0}; + byte rsp[64]; + word32 requestSz; + word32 rspSz = sizeof(rsp); + + memset(&io, 0, sizeof(io)); + append_agent_message(&io, MSGID_AGENT_SUCCESS, 0x44, 200); + build_agent_message(request, &requestSz, MSGID_AGENT_REQUEST_IDENTITIES, + NULL, 0); + setup_agent_test(&ctx, &ssh, &io); + + AssertIntEQ(wolfSSH_AGENT_Relay(ssh, request, &requestSz, rsp, &rspSz), + WS_ERROR); + AssertIntEQ(wolfSSH_get_error(ssh), WS_BUFFER_E); + + cleanup_agent_test(ctx, ssh); +} + #endif /* WOLFSSH_AGENT */ @@ -8272,6 +8575,15 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_agent_signrequest_rsa_too_large(); #endif #endif + test_wolfSSH_agent_relay_reply_split_reads(); + test_wolfSSH_agent_relay_reply_desync(); + test_wolfSSH_agent_relay_short_write(); + test_wolfSSH_agent_relay_oversize_reply(); + test_wolfSSH_agent_relay_zero_length(); + test_wolfSSH_agent_relay_reply_exceeds_caller_buf(); + test_wolfSSH_agent_relay_no_retry_after_partial_write(); + test_wolfSSH_agent_relay_reconnects_on_dead_socket(); + test_wolfSSH_agent_relay_reconnect_failure(); #endif #ifdef WOLFSSH_OSSH_CERTS #ifndef WOLFSSH_NO_ED25519 diff --git a/tests/regress.c b/tests/regress.c index 9b135211b..674e6104b 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -354,6 +354,7 @@ typedef struct { word32 outSz; word32 outCap; byte blockNext; /* make the next send report a would-block */ + byte blockAll; /* make every send report a would-block */ byte isrNext; /* make the next send report an interrupted call */ } MemIo; @@ -375,6 +376,8 @@ static int MemSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) { (void)ssh; MemIo* io = (MemIo*)ctx; + if (io->blockAll) + return WS_CBIO_ERR_WANT_WRITE; if (io->blockNext) { io->blockNext = 0; return WS_CBIO_ERR_WANT_WRITE; @@ -400,6 +403,7 @@ static void MemIoInit(MemIo* io, byte* in, word32 inSz, byte* out, word32 outCap io->outSz = 0; io->outCap = outCap; io->blockNext = 0; + io->blockAll = 0; io->isrNext = 0; } @@ -3027,6 +3031,826 @@ static void TestAgentOpenAfterRequestSucceeds(void) FreeChannelOpenHarness(&harness); } + +/* Records every byte handed to the agent so a test can check the exact + * message that was relayed. */ +#define AGENT_RELAY_TEST_SZ 8192 + +typedef struct AgentRelayIo { + byte written[AGENT_RELAY_TEST_SZ]; + byte response[128]; + word32 writtenSz; + word32 responseSz; + word32 readIdx; + word32 writeCalls; + byte shortWrite; + byte failSetup; + word32 failWriteCall; + word32 failReadCall; + word32 readCalls; + word32 setupCalls; +} AgentRelayIo; + +static int AgentRelayCb(WS_AgentCbAction action, void* ctx) +{ + AgentRelayIo* io = (AgentRelayIo*)ctx; + + if (action == WOLFSSH_AGENT_LOCAL_SETUP) { + if (io != NULL) + io->setupCalls++; + if (io != NULL && io->failSetup) + return WS_AGENT_SETUP_E; + return WS_AGENT_SUCCESS; + } + if (action == WOLFSSH_AGENT_LOCAL_CLEANUP) + return WS_AGENT_SUCCESS; + + return WS_AGENT_INVALID_ACTION; +} + +static int AgentRelayIoCb(WS_AgentIoCbAction action, void* buf, word32 bufSz, + void* ctx) +{ + AgentRelayIo* io = (AgentRelayIo*)ctx; + word32 avail; + + if (action == WOLFSSH_AGENT_IO_WRITE) { + io->writeCalls++; + if (io->failWriteCall == io->writeCalls) + return WS_CBIO_ERR_GENERAL; + if (io->shortWrite && bufSz > 0) { + io->shortWrite = 0; + bufSz--; + } + AssertTrue(io->writtenSz + bufSz <= sizeof(io->written)); + WMEMCPY(io->written + io->writtenSz, buf, bufSz); + io->writtenSz += bufSz; + return (int)bufSz; + } + + io->readCalls++; + if (io->failReadCall == io->readCalls) + return WS_CBIO_ERR_GENERAL; + + avail = io->responseSz - io->readIdx; + if (avail == 0) + return 0; + if (avail > bufSz) + avail = bufSz; + WMEMCPY(buf, io->response + io->readIdx, avail); + io->readIdx += avail; + /* Replay the canned reply once it drains, so every relayed request gets + * an answer. */ + if (io->readIdx == io->responseSz) + io->readIdx = 0; + return (int)avail; +} + +/* Builds on ChannelOpenHarness for the session and transport, and re-points + * its MemIo at a buffer large enough for several relayed replies. */ +typedef struct AgentRelayHarness { + ChannelOpenHarness base; + WOLFSSH_CHANNEL* channel; + AgentRelayIo agentIo; + byte in[128]; + byte out[AGENT_RELAY_TEST_SZ]; +} AgentRelayHarness; + +/* An agent message is an SSH blob: a 4-byte length over bodySz filler bytes. */ +static word32 BuildAgentMessage(byte* out, word32 outSz, word32 bodySz, + byte fill) +{ + byte body[AGENT_RELAY_TEST_SZ]; + + AssertTrue(bodySz <= sizeof(body)); + WMEMSET(body, fill, bodySz); + + return AppendBlob(out, outSz, 0, body, bodySz); +} + +/* Opens an auth-agent channel the way the peer would, then marks it confirmed + * so the relay can send replies back over it. */ +static WOLFSSH_CHANNEL* AgentRelayAddChannel(AgentRelayHarness* harness, + word32 peerChannelId) +{ + WOLFSSH_CHANNEL* channel; + word32 inSz; + + inSz = BuildChannelOpenPacket("auth-agent@openssh.com", peerChannelId, + 0x4000, 0x8000, NULL, 0, harness->in, sizeof(harness->in)); + MemIoInit(&harness->base.io, harness->in, inSz, + harness->out, sizeof(harness->out)); + AssertIntEQ(DoReceive(harness->base.ssh), WS_SUCCESS); + + channel = wolfSSH_ChannelFind(harness->base.ssh, + peerChannelId, WS_CHANNEL_ID_PEER); + AssertNotNull(channel); + channel->openConfirmed = 1; + + return channel; +} + +static void InitAgentRelayHarness(AgentRelayHarness* harness) +{ + WMEMSET(harness, 0, sizeof(*harness)); + + InitChannelOpenHarnessClient(&harness->base, harness->in, 0); + MemIoInit(&harness->base.io, harness->in, 0, + harness->out, sizeof(harness->out)); + + /* DoChannelOpen admits an auth-agent channel only on a client that has + * sent auth-agent-req, so the harness stands in for that request. */ + harness->base.ssh->connectState = CONNECT_CLIENT_CHANNEL_AGENT_REQUEST_SENT; + + AssertIntEQ(wolfSSH_CTX_AGENT_enable(harness->base.ctx, 1), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_set_agent_cb(harness->base.ctx, AgentRelayCb, + AgentRelayIoCb), WS_SUCCESS); + harness->base.ssh->agent = wolfSSH_AGENT_new(harness->base.ctx->heap); + AssertNotNull(harness->base.ssh->agent); + AssertIntEQ(wolfSSH_set_agent_cb_ctx(harness->base.ssh, &harness->agentIo), + WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_enable(harness->base.ssh, 1), WS_SUCCESS); + + /* Any well formed reply will do; these tests assert on the request side. */ + harness->agentIo.responseSz = BuildAgentMessage(harness->agentIo.response, + sizeof(harness->agentIo.response), 8, 0xF0); + + harness->channel = AgentRelayAddChannel(harness, 11); +} + +/* A request split over three channel deliveries reaches the agent once, whole + * and in order. */ +static void TestAgentRelayChannelReassemblesFragmentedRequest(void) +{ + AgentRelayHarness harness; + byte msg[604]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 600, 0x5a); + AssertIntEQ(msgSz, sizeof(msg)); + + AssertIntEQ(ChannelPutData(harness.channel, msg, 200), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + AssertIntEQ(ChannelPutData(harness.channel, msg + 200, 200), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + AssertIntEQ(ChannelPutData(harness.channel, msg + 400, 204), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(harness.agentIo.writtenSz, sizeof(msg)); + AssertIntEQ(WMEMCMP(harness.agentIo.written, msg, sizeof(msg)), 0); + + FreeChannelOpenHarness(&harness.base); +} + +/* Two messages arriving together are relayed as two, with the second not + * folded into the first one's length. */ +static void TestAgentRelayChannelKeepsTrailingBytes(void) +{ + AgentRelayHarness harness; + byte msg[120]; + word32 firstSz; + word32 secondSz; + + InitAgentRelayHarness(&harness); + firstSz = BuildAgentMessage(msg, sizeof(msg), 56, 0x11); + secondSz = BuildAgentMessage(msg + firstSz, sizeof(msg) - firstSz, + 56, 0x22); + AssertIntEQ(firstSz + secondSz, sizeof(msg)); + + AssertIntEQ(ChannelPutData(harness.channel, msg, sizeof(msg)), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + + AssertIntEQ(harness.agentIo.writeCalls, 2); + AssertIntEQ(harness.agentIo.writtenSz, sizeof(msg)); + AssertIntEQ(WMEMCMP(harness.agentIo.written, msg, sizeof(msg)), 0); + + FreeChannelOpenHarness(&harness.base); +} + +/* A backlog larger than one read is drained rather than stranded. */ +static void TestAgentRelayChannelDrainsOversizeBacklog(void) +{ + AgentRelayHarness harness; + byte msg[3000]; + word32 firstSz; + word32 secondSz; + + InitAgentRelayHarness(&harness); + firstSz = BuildAgentMessage(msg, sizeof(msg), 1496, 0x99); + secondSz = BuildAgentMessage(msg + firstSz, sizeof(msg) - firstSz, + 1496, 0xAA); + AssertIntEQ(firstSz + secondSz, sizeof(msg)); + + AssertIntEQ(ChannelPutData(harness.channel, msg, sizeof(msg)), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + + AssertIntEQ(harness.agentIo.writeCalls, 2); + AssertIntEQ(harness.agentIo.writtenSz, sizeof(msg)); + AssertIntEQ(WMEMCMP(harness.agentIo.written, msg, sizeof(msg)), 0); + + FreeChannelOpenHarness(&harness.base); +} + +/* A partial request on one channel is dropped rather than prepended to the + * next channel's data. */ +static void TestAgentRelayChannelResetsOnChannelChange(void) +{ + AgentRelayHarness harness; + WOLFSSH_CHANNEL* other; + byte partial[8]; + byte whole[36]; + word32 wholeSz; + + InitAgentRelayHarness(&harness); + WMEMSET(partial, 0x77, sizeof(partial)); + partial[0] = 0; + partial[1] = 0; + partial[2] = 0; + partial[3] = 200; + + AssertIntEQ(ChannelPutData(harness.channel, partial, sizeof(partial)), + WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + other = AgentRelayAddChannel(&harness, 12); + wholeSz = BuildAgentMessage(whole, sizeof(whole), 32, 0x33); + AssertIntEQ(ChannelPutData(other, whole, wholeSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, other->channel), + WS_SUCCESS); + + AssertIntEQ(harness.agentIo.writtenSz, wholeSz); + AssertIntEQ(WMEMCMP(harness.agentIo.written, whole, wholeSz), 0); + + FreeChannelOpenHarness(&harness.base); +} + +/* A declared length past the build's ceiling is refused and nothing is sent + * to the agent. */ +static void TestAgentRelayChannelRejectsOversizeLength(void) +{ + AgentRelayHarness harness; + byte msg[16]; + + InitAgentRelayHarness(&harness); + WMEMSET(msg, 0, sizeof(msg)); + msg[0] = 0x00; + msg[1] = 0x10; + msg[2] = 0x00; + msg[3] = 0x00; + + AssertIntEQ(ChannelPutData(harness.channel, msg, sizeof(msg)), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_BUFFER_E); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + FreeChannelOpenHarness(&harness.base); +} + +/* A zero declared length carries no message id, so it is refused and nothing + * is sent to the agent. */ +static void TestAgentRelayChannelRejectsZeroLength(void) +{ + AgentRelayHarness harness; + byte msg[16]; + + InitAgentRelayHarness(&harness); + WMEMSET(msg, 0, sizeof(msg)); + + AssertIntEQ(ChannelPutData(harness.channel, msg, sizeof(msg)), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_BUFFER_E); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + FreeChannelOpenHarness(&harness.base); +} + + +/* The agent's reply goes back out on the channel as CHANNEL_DATA carrying + * exactly the reply bytes, in several packets because the test caps the + * peer's max packet size. */ +static void TestAgentRelayChannelReplyReachesChannel(void) +{ + AgentRelayHarness harness; + byte reply[AGENT_RELAY_TEST_SZ]; + byte msg[36]; + word32 msgSz; + word32 outBefore; + word32 idx; + word32 replySz = 0; + word32 dataPkts = 0; + int calls = 1; + int ret; + + InitAgentRelayHarness(&harness); + /* Small enough that the canned reply cannot leave in one packet. */ + harness.channel->peerMaxPacketSz = 5; + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x44); + outBefore = harness.base.io.outSz; + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + + /* One call per packet the cap allows: each holds the rest of the reply + * and answers WS_WANT_WRITE until the last of it is out. */ + ret = wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel); + while (ret == WS_WANT_WRITE && ++calls < 64) { + ret = wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel); + } + AssertIntEQ(ret, WS_SUCCESS); + AssertTrue(calls > 1); + + /* Unencrypted packets: 4 length, 1 padding, 1 msg id, 4 channel id, then + * the SSH string holding this piece of the reply. Stitch them back into + * one buffer and compare that against what the agent handed over. */ + idx = outBefore; + while (idx + LENGTH_SZ < harness.base.io.outSz) { + word32 pktSz = ReadUint32(harness.base.io.out + idx); + + if (harness.base.io.out[idx + 5] == MSGID_CHANNEL_DATA) { + word32 dataSz = ReadUint32(harness.base.io.out + idx + 10); + + AssertTrue(replySz + dataSz <= sizeof(reply)); + WMEMCPY(reply + replySz, harness.base.io.out + idx + 14, dataSz); + replySz += dataSz; + dataPkts++; + } + idx += LENGTH_SZ + pktSz; + } + + AssertTrue(dataPkts > 1); + AssertIntEQ(replySz, harness.agentIo.responseSz); + AssertIntEQ(WMEMCMP(reply, harness.agentIo.response, replySz), 0); + + FreeChannelOpenHarness(&harness.base); +} + + +/* An unfinished reply is owed, not dropped: the relay answers WS_WANT_WRITE + * and leaves a request arriving behind it in the channel, untouched. */ +static void TestAgentRelayChannelHoldsReplyTail(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + word32 writtenAfterFirst; + + InitAgentRelayHarness(&harness); + /* Small enough that the canned reply cannot leave in one packet. */ + harness.channel->peerMaxPacketSz = 5; + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x55); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + writtenAfterFirst = harness.agentIo.writtenSz; + AssertIntEQ(writtenAfterFirst, msgSz); + + /* A second request lands while the tail is still owed. */ + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + AssertIntEQ(harness.agentIo.writtenSz, writtenAfterFirst); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A send the socket would not take leaves the reply queued, and the relay + * still owes it even though the byte count was full. */ +static void TestAgentRelayChannelOwesQueuedReply(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x66); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + + /* The whole reply fits in one send, and the socket takes none of it. */ + harness.base.io.blockAll = 1; + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(harness.base.ssh)); + + /* The retry must not call it done while the transport still holds it. */ + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + + /* Once the socket takes it, the reply is no longer owed. */ + harness.base.io.blockAll = 0; + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(wolfSSH_OutputPending(harness.base.ssh), 0); + + FreeChannelOpenHarness(&harness.base); +} + + +/* The agent socket dropped since the last exchange, so the relay reconnects + * and sends the request again rather than ending the channel. */ +static void TestAgentRelayChannelReconnectsOnDeadSocket(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0xAA); + harness.agentIo.failWriteCall = 1; + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(harness.agentIo.writeCalls, 2); + AssertIntEQ(harness.agentIo.writtenSz, msgSz); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A rekey in flight holds the reply rather than failing the channel. */ +static void TestAgentRelayChannelHoldsOnRekey(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0xBB); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + harness.base.ssh->isKeying = 1; + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_REKEYING); + AssertIntEQ(harness.base.ssh->agent->error, WS_SUCCESS); + + /* The rekey finished, so the held reply goes out. */ + harness.base.ssh->isKeying = 0; + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + + FreeChannelOpenHarness(&harness.base); +} + + +/* The agent socket is gone, so the connect the relay opens with fails and + * nothing is handed to the agent. */ +static void TestAgentRelayChannelSetupFailure(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + harness.agentIo.failSetup = 1; + harness.base.ssh->agent->state = AGENT_STATE_INIT; + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x88); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_AGENT_CXN_FAIL); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A channel that has already sent EOF cannot carry the reply. That is not a + * status the relay holds on, so it fails and records the error. */ +static void TestAgentRelayChannelSendFailureIsFatal(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x99); + harness.channel->eofTxd = 1; + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_EOF); + AssertIntEQ(harness.base.ssh->agent->error, WS_EOF); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A read late in the loop can credit the channel window while no agent + * message completes, and that write is still owed. */ +static void TestAgentRelayChannelOwesAdjustFromRead(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0xEE); + (void)msgSz; + + /* Only the length prefix arrives, so no message can complete. */ + AssertIntEQ(ChannelPutData(harness.channel, msg, LENGTH_SZ), WS_SUCCESS); + + /* An empty window makes the read credit it, and the socket takes none. */ + harness.channel->windowSz = 0; + harness.base.io.blockAll = 1; + + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + AssertTrue(wolfSSH_OutputPending(harness.base.ssh)); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A flush inside the reply's own send advances the flush count, and the peer + * window is empty behind it. That is still a held reply, not a failure. */ +static void TestAgentRelayChannelFullWindowAfterFlush(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x6B); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + + /* The read credits the window and that adjust cannot go out yet, so it is + * still queued when the reply is sent. */ + harness.channel->windowSz = 0; + harness.base.io.blockNext = 1; + + /* No reply byte can be framed behind it. */ + harness.channel->peerWindowSz = 0; + + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WINDOW_FULL); + AssertIntEQ(harness.base.ssh->agent->error, WS_SUCCESS); + + FreeChannelOpenHarness(&harness.base); +} + + +/* The agent drops the connection partway through its reply, so the exchange + * fails and nothing of it reaches the channel. */ +static void TestAgentRelayChannelAgentReadFailure(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + word32 outBefore; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x5A); + outBefore = harness.base.io.outSz; + + /* The length prefix reads, the body does not. */ + harness.agentIo.failReadCall = 2; + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_AGENT_CXN_FAIL); + AssertIntEQ(harness.base.ssh->agent->error, WS_AGENT_CXN_FAIL); + AssertIntEQ(harness.base.io.outSz, outBefore); + + FreeChannelOpenHarness(&harness.base); +} + + +/* The channel is gone by the time the relay reads it, so the read's own error + * ends the exchange. */ +static void TestAgentRelayChannelReadFailure(void) +{ + AgentRelayHarness harness; + + InitAgentRelayHarness(&harness); + + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel + 0x4000), WS_INVALID_CHANID); + AssertIntEQ(harness.base.ssh->agent->error, WS_INVALID_CHANID); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + FreeChannelOpenHarness(&harness.base); +} + + +static int AgentRelayHighwaterCb(byte side, void* ctx) +{ + int* calls = (int*)ctx; + + WOLFSSH_UNUSED(side); + + (*calls)++; + return WS_FATAL_ERROR; +} + + +/* The highwater callback fails after the reply is on the wire, so its error + * arrives as the send's return. Holding the reply for a retry would hand the + * peer those bytes twice, so the channel fails instead. */ +static void TestAgentRelayChannelHighwaterErrorIsFatal(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + int calls = 0; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0xDD); + + wolfSSH_SetHighwaterCb(harness.base.ctx, 1, AgentRelayHighwaterCb); + wolfSSH_SetHighwaterCtx(harness.base.ssh, &calls); + harness.base.ssh->highwaterMark = 1; + harness.base.ssh->txCount = 1; + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_AGENT_CXN_FAIL); + AssertIntEQ(calls, 1); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A partial request leaves no reply to send, but a window adjust the channel + * read deferred is still queued, so the relay reports the write as owed. */ +static void TestAgentRelayChannelOwesQueuedAdjust(void) +{ + AgentRelayHarness harness; + byte msg[36]; + byte probe[4]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0xCC); + (void)msgSz; + + /* Only the length prefix arrives, so nothing can go to the agent yet. */ + AssertIntEQ(ChannelPutData(harness.channel, msg, LENGTH_SZ), WS_SUCCESS); + + /* Something the transport would not take is already queued below. */ + harness.base.io.blockAll = 1; + WMEMSET(probe, 0, sizeof(probe)); + wolfSSH_ChannelIdSend(harness.base.ssh, harness.channel->channel, + probe, sizeof(probe)); + AssertTrue(wolfSSH_OutputPending(harness.base.ssh)); + + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + AssertIntEQ(harness.agentIo.writtenSz, 0); + + FreeChannelOpenHarness(&harness.base); +} + + +/* A peer window that runs out mid-reply leaves the rest owed rather than + * ending the session with part of the reply already sent. */ +static void TestAgentRelayChannelHoldsOnFullWindow(void) +{ + AgentRelayHarness harness; + byte msg[36]; + word32 msgSz; + + InitAgentRelayHarness(&harness); + /* Smaller than the canned reply, so the window empties partway. */ + harness.channel->peerWindowSz = 5; + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0x77); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + + /* The window clamps this send rather than refusing it, so the reply is + * simply unfinished. */ + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + AssertIntEQ(harness.channel->peerWindowSz, 0); + + /* Now the window is empty, and the relay says so rather than calling it + * a failure. */ + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WINDOW_FULL); + + FreeChannelOpenHarness(&harness.base); +} + +/* A rekey stops the send before anything queued below goes out, so the relay + * drives that itself and names the socket while it still holds bytes. */ +static void TestAgentRelayChannelFlushesUnderRekey(void) +{ + AgentRelayHarness harness; + byte msg[36]; + byte probe[4]; + word32 msgSz; + int calls = 0; + int ret; + + InitAgentRelayHarness(&harness); + /* Small enough that the canned reply cannot leave in one packet. */ + harness.channel->peerMaxPacketSz = 5; + msgSz = BuildAgentMessage(msg, sizeof(msg), 32, 0xDD); + + AssertIntEQ(ChannelPutData(harness.channel, msg, msgSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + + /* The rekey starts with part of its own packet left queued below. */ + harness.base.io.blockAll = 1; + WMEMSET(probe, 0, sizeof(probe)); + wolfSSH_ChannelIdSend(harness.base.ssh, harness.channel->channel, + probe, sizeof(probe)); + AssertTrue(wolfSSH_OutputPending(harness.base.ssh)); + harness.base.ssh->isKeying = 1; + + /* The socket holds those bytes, so that is what the caller waits on. */ + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_WANT_WRITE); + + /* It takes them now, so the queue drains and only the rekey is left. */ + harness.base.io.blockAll = 0; + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_REKEYING); + AssertIntEQ(wolfSSH_OutputPending(harness.base.ssh), 0); + + /* The rekey finished, so the rest of the reply goes out. */ + harness.base.ssh->isKeying = 0; + ret = wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel); + while (ret == WS_WANT_WRITE && ++calls < 64) { + ret = wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel); + } + AssertIntEQ(ret, WS_SUCCESS); + + FreeChannelOpenHarness(&harness.base); +} + + +/* Shifting a held request down leaves a copy of it above the buffer's + * length, and a channel change scrubs that copy too. */ +static void TestAgentRelayChannelScrubsShiftedRequest(void) +{ + AgentRelayHarness harness; + WOLFSSH_CHANNEL* other; + WOLFSSH_BUFFER* relay; + byte first[36]; + byte second[36]; + byte third[36]; + word32 firstSz; + word32 secondSz; + word32 thirdSz; + word32 i; + + InitAgentRelayHarness(&harness); + firstSz = BuildAgentMessage(first, sizeof(first), 32, 0x11); + secondSz = BuildAgentMessage(second, sizeof(second), 32, 0x22); + thirdSz = BuildAgentMessage(third, sizeof(third), 32, 0x99); + + /* A whole request and the head of the next, twice over: the first round + * grows the buffer, so the second round shifts inside it. */ + AssertIntEQ(ChannelPutData(harness.channel, first, firstSz), WS_SUCCESS); + AssertIntEQ(ChannelPutData(harness.channel, second, secondSz - 12), + WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + + AssertIntEQ(ChannelPutData(harness.channel, second + secondSz - 12, 12), + WS_SUCCESS); + AssertIntEQ(ChannelPutData(harness.channel, third, thirdSz - 12), + WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, + harness.channel->channel), WS_SUCCESS); + AssertIntEQ(harness.agentIo.writtenSz, firstSz + secondSz); + + /* The conversation moves on, dropping the head held for this one. */ + other = AgentRelayAddChannel(&harness, 12); + AssertIntEQ(ChannelPutData(other, first, firstSz), WS_SUCCESS); + AssertIntEQ(wolfSSH_AGENT_RelayChannel(harness.base.ssh, other->channel), + WS_SUCCESS); + + relay = harness.base.ssh->agent->relayBuf; + AssertNotNull(relay); + for (i = 0; i < relay->bufferSz; i++) + AssertTrue(relay->buffer[i] != 0x99); + + FreeChannelOpenHarness(&harness.base); +} + + #endif /* WOLFSSH_AGENT */ #endif /* !NO_WOLFSSH_CLIENT */ @@ -4447,6 +5271,7 @@ static int RunRequestThroughFreeingCb(ChannelOpenHarness* harness, return DoReceive(harness->ssh); } + /* The generic callback may free the channel it was called on. Nothing below * it reads the channel again after that, and the request ends there: the * type is not handled, no typed callback runs, and a wanted reply fails on @@ -16119,6 +16944,28 @@ int main(int argc, char** argv) TestAgentChannelNullAgentSendsOpenFail(); TestAgentOpenWithAgentDisabledFails(); TestAgentOpenAfterRequestSucceeds(); + TestAgentRelayChannelReassemblesFragmentedRequest(); + TestAgentRelayChannelKeepsTrailingBytes(); + TestAgentRelayChannelDrainsOversizeBacklog(); + TestAgentRelayChannelResetsOnChannelChange(); + TestAgentRelayChannelRejectsOversizeLength(); + TestAgentRelayChannelRejectsZeroLength(); + TestAgentRelayChannelReplyReachesChannel(); + TestAgentRelayChannelHoldsReplyTail(); + TestAgentRelayChannelOwesQueuedReply(); + TestAgentRelayChannelOwesQueuedAdjust(); + TestAgentRelayChannelOwesAdjustFromRead(); + TestAgentRelayChannelHighwaterErrorIsFatal(); + TestAgentRelayChannelFullWindowAfterFlush(); + TestAgentRelayChannelAgentReadFailure(); + TestAgentRelayChannelReadFailure(); + TestAgentRelayChannelHoldsOnFullWindow(); + TestAgentRelayChannelSetupFailure(); + TestAgentRelayChannelSendFailureIsFatal(); + TestAgentRelayChannelReconnectsOnDeadSocket(); + TestAgentRelayChannelHoldsOnRekey(); + TestAgentRelayChannelFlushesUnderRekey(); + TestAgentRelayChannelScrubsShiftedRequest(); #endif #endif #if defined(WOLFSSH_AGENT) && !defined(NO_WOLFSSH_SERVER) diff --git a/wolfssh/agent.h b/wolfssh/agent.h index 0139cacd3..6f07fd310 100644 --- a/wolfssh/agent.h +++ b/wolfssh/agent.h @@ -127,6 +127,10 @@ enum AgentStates { }; +/* Defined in wolfssh/internal.h. Held by pointer so this installed header + * does not have to include an internal one. */ +struct WOLFSSH_BUFFER; + struct WOLFSSH_AGENT_CTX { void* heap; byte* msg; @@ -141,6 +145,12 @@ struct WOLFSSH_AGENT_CTX { int requestSuccess; int requestFailure; byte lastMsgId; + /* One agent reply */ + struct WOLFSSH_BUFFER* rxBuf; + /* Channel bytes waiting to be framed */ + struct WOLFSSH_BUFFER* relayBuf; + word32 relayChannel; + byte relayActive; }; typedef struct WOLFSSH_AGENT_CTX WOLFSSH_AGENT_CTX; @@ -199,6 +209,13 @@ WOLFSSH_API int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh); WOLFSSH_LOCAL int wolfSSH_AGENT_worker(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_AGENT_Relay(WOLFSSH* ssh, const byte* msg, word32* msgSz, byte* rsp, word32* rspSz); +/* Moves whole agent messages between the channel named by channelId and the + * agent, holding a partial request or unfinished reply between calls; the + * same channelId must come back for the rest of either. While a reply is + * owed it names what holds it: WS_WANT_WRITE the transport, WS_WINDOW_FULL + * or WS_REKEYING the peer. Call again until WS_SUCCESS. Any other + * non-success code leaves the channel unusable. */ +WOLFSSH_API int wolfSSH_AGENT_RelayChannel(WOLFSSH* ssh, word32 channelId); WOLFSSH_API int wolfSSH_AGENT_SignRequest(WOLFSSH* ssh, const byte* digest, word32 digestSz, byte* sig, word32* sigSz, From 653212896cb2428f8bcc1e7b570c3148aa885ade Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Fri, 18 Sep 2026 18:25:01 +0900 Subject: [PATCH 2/2] agent: raise the message size ceiling - WOLFSSH_AGENT_MAX_MSG_SZ defaults to 262144. --- src/agent.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent.c b/src/agent.c index 928c91cd3..9e3b22a01 100644 --- a/src/agent.c +++ b/src/agent.c @@ -75,7 +75,7 @@ /* Largest agent message this build will handle. The peer declares the * length, so it is bounded before it drives an allocation. */ #ifndef WOLFSSH_AGENT_MAX_MSG_SZ - #define WOLFSSH_AGENT_MAX_MSG_SZ 32768 + #define WOLFSSH_AGENT_MAX_MSG_SZ 262144 #endif /* Starting size and growth step for the channel accumulation buffer. */