agent: let the application open the agent channel

The one server-side site that opens auth-agent@openssh.com sits inside
wolfSSH_accept(), so an application driving its own channels cannot
reach it: the session records the request and no channel follows.

- add wolfSSH_AGENT_ChannelOpen(), the same open lifted out of accept(),
  which still calls it
- it reports WS_BAD_ARGUMENT until the peer asks and on a client
  session, and is idempotent after, so an application can poll it
- publish the agent on a queued open too, so a retry after WS_WANT_WRITE
  finds it rather than opening a second channel and leaking the first
- flush what is left of a queued open on the next call, rather than
  reporting a success the peer never saw
- record ssh->error from the send alone, so neither a poll ahead of the
  request nor a failed allocation stops accept() continuing
pull/1245/head
John Safranek 2026-09-02 09:40:30 -07:00 committed by philljj
parent 85f7978eb4
commit 41f171d982
4 changed files with 193 additions and 41 deletions

View File

@ -1731,6 +1731,84 @@ int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled)
}
int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh)
{
WOLFSSH_AGENT_CTX* newAgent = NULL;
WOLFSSH_CHANNEL* newChannel = NULL;
int ret = WS_SUCCESS;
/* wolfSSH_accept() clears only want-read/want-write/auth-pending, so a
* WS_BAD_ARGUMENT latched by a poll kills the handshake. */
int recordError = 0;
WLOG_ENTER();
if (ssh == NULL)
ret = WS_SSH_NULL_E;
else if (ssh->ctx->side != WOLFSSH_ENDPOINT_SERVER) {
/* Server side only. wolfSSH_connect() sets ssh->agent too, so the
* checks below would report a channel a client never opened. */
ret = WS_BAD_ARGUMENT;
}
else if (!ssh->useAgent) {
/* Nothing asked for agent forwarding on this session. */
ret = WS_BAD_ARGUMENT;
}
else if (ssh->agent == NULL) {
/* Nothing else sets ssh->agent, so a NULL one means "not opened
* yet". Idempotent, so a poll cannot open a second channel. */
WLOG(WS_LOG_AGENT, "Starting agent channel");
newAgent = wolfSSH_AGENT_new(ssh->ctx->heap);
if (newAgent == NULL)
ret = WS_MEMORY_E;
if (ret == WS_SUCCESS) {
newChannel = ChannelNew(ssh, ID_CHANTYPE_AUTH_AGENT,
ssh->ctx->windowSz, ssh->ctx->maxPacketSz);
if (newChannel == NULL)
ret = WS_MEMORY_E;
}
if (ret == WS_SUCCESS) {
recordError = 1;
ret = SendChannelOpenSession(ssh, newChannel);
if (ret < WS_SUCCESS
&& ret != WS_WANT_WRITE && ret != WS_WANT_READ) {
ChannelDelete(newChannel, ssh->ctx->heap);
}
else {
/* Publish on a queued open too, so a retry takes the
* already-open path rather than opening a second. */
ChannelAppend(ssh, newChannel);
newAgent->channel = newChannel->channel;
ssh->agent = newAgent;
newAgent = NULL;
if (ssh->ctx->agentCb) {
ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP,
ssh->agentCbCtx);
}
}
}
if (newAgent != NULL)
wolfSSH_AGENT_free(newAgent);
}
else if (wolfSSH_OutputPending(ssh)) {
/* Any queued output, not just this open. Flush it rather than
* report a success the peer hasn't seen. */
recordError = 1;
ret = wolfSSH_SendPacket(ssh);
}
if (recordError)
ssh->error = ret;
WLOG_LEAVE(ret);
return ret;
}
int wolfSSH_AGENT_worker(WOLFSSH* ssh)
{
int ret = WS_SUCCESS;

View File

@ -813,52 +813,12 @@ int wolfSSH_accept(WOLFSSH* ssh)
#endif /* WOLFSSH_SFTP and !NO_WOLFSSH_SERVER */
#ifdef WOLFSSH_AGENT
if (ssh->useAgent) {
WOLFSSH_AGENT_CTX* newAgent;
WOLFSSH_CHANNEL* newChannel;
WLOG(WS_LOG_AGENT, "Starting agent channel");
newAgent = wolfSSH_AGENT_new(ssh->ctx->heap);
if (newAgent == NULL) {
ssh->error = WS_MEMORY_E;
WLOG(WS_LOG_DEBUG, acceptError,
"SERVER_USERAUTH_ACCEPT_DONE", ssh->error);
return WS_ERROR;
}
newChannel = ChannelNew(ssh, ID_CHANTYPE_AUTH_AGENT,
ssh->ctx->windowSz, ssh->ctx->maxPacketSz);
if (newChannel == NULL) {
wolfSSH_AGENT_free(newAgent);
ssh->error = WS_MEMORY_E;
WLOG(WS_LOG_DEBUG, acceptError,
"SERVER_USERAUTH_ACCEPT_DONE", ssh->error);
return WS_FATAL_ERROR;
}
ssh->error = SendChannelOpenSession(ssh, newChannel);
ssh->error = wolfSSH_AGENT_ChannelOpen(ssh);
if (ssh->error < WS_SUCCESS) {
if (ssh->error == WS_WANT_WRITE ||
ssh->error == WS_WANT_READ) {
ChannelAppend(ssh, newChannel);
}
else {
ChannelDelete(newChannel, ssh->ctx->heap);
wolfSSH_AGENT_free(newAgent);
}
WLOG(WS_LOG_DEBUG, acceptError,
"SERVER_USERAUTH_ACCEPT_DONE", ssh->error);
return WS_FATAL_ERROR;
}
ChannelAppend(ssh, newChannel);
newAgent->channel = newChannel->channel;
if (ssh->ctx->agentCb) {
ssh->ctx->agentCb(WOLFSSH_AGENT_LOCAL_SETUP,
ssh->agentCbCtx);
}
if (ssh->agent != NULL)
wolfSSH_AGENT_free(ssh->agent);
ssh->agent = newAgent;
}
#endif /* WOLFSSH_AGENT */
ssh->acceptState = ACCEPT_CLIENT_SESSION_ESTABLISHED;

View File

@ -4437,6 +4437,102 @@ static void TestAgentChannelNullAgentSendsOpenFail(void)
FreeChannelOpenHarness(&harness);
}
/* Nothing asked for forwarding, so the open is refused rather than started.
* The refusal is the documented answer to a poll, so it must not land in
* ssh->error: wolfSSH_accept() would then abort with WS_INVALID_STATE_E. */
static void TestAgentChannelOpenWithoutRequest(void)
{
ChannelOpenHarness harness;
InitChannelOpenHarness(&harness, NULL, 0);
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_BAD_ARGUMENT);
AssertNull(harness.ssh->agent);
AssertIntEQ(harness.io.outSz, 0);
AssertIntEQ(harness.ssh->error, WS_SUCCESS);
/* The handshake survives the poll: no input, so accept only wants read. */
AssertIntEQ(wolfSSH_accept(harness.ssh), WS_FATAL_ERROR);
AssertIntEQ(harness.ssh->error, WS_WANT_READ);
FreeChannelOpenHarness(&harness);
}
/* A queued open publishes the agent, so the caller's next poll must finish
* the send rather than report a success the peer never saw, and must not
* open a second channel. */
static void TestAgentChannelOpenFlushesQueuedOpen(void)
{
ChannelOpenHarness harness;
word32 outSz;
InitChannelOpenHarness(&harness, NULL, 0);
harness.ssh->useAgent = 1;
harness.io.blockNext = 1;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_WANT_WRITE);
AssertNotNull(harness.ssh->agent);
AssertIntEQ(harness.ssh->channelListSz, 1);
AssertIntEQ(harness.io.outSz, 0);
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_SUCCESS);
AssertIntEQ(harness.ssh->channelListSz, 1);
AssertTrue(harness.io.outSz > 0);
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
MSGID_CHANNEL_OPEN);
/* The flushed open is the answer wolfSSH_accept() retries on: success,
* no second channel, no new packet, ssh->error untouched. */
outSz = harness.io.outSz;
harness.ssh->error = WS_SUCCESS;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_SUCCESS);
AssertIntEQ(harness.ssh->channelListSz, 1);
AssertIntEQ(harness.io.outSz, outSz);
AssertIntEQ(harness.ssh->error, WS_SUCCESS);
FreeChannelOpenHarness(&harness);
}
/* A send that fails outright, rather than blocking, leaves nothing behind,
* so a later poll starts the open over. */
static void TestAgentChannelOpenSendFailureCleansUp(void)
{
ChannelOpenHarness harness;
InitChannelOpenHarness(&harness, NULL, 0);
harness.ssh->useAgent = 1;
/* No room, so MemSend reports a general error. */
harness.io.outCap = 0;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_SOCKET_ERROR_E);
AssertNull(harness.ssh->agent);
AssertIntEQ(harness.ssh->channelListSz, 0);
AssertIntEQ(harness.io.outSz, 0);
AssertIntEQ(harness.ssh->error, WS_SOCKET_ERROR_E);
FreeChannelOpenHarness(&harness);
}
#ifndef NO_WOLFSSH_CLIENT
/* Server-side call. A client has an ssh->agent of its own, so answering the
* poll from it would report a channel that was never opened. */
static void TestAgentChannelOpenOnClientRefused(void)
{
ChannelOpenHarness harness;
InitChannelOpenHarnessClient(&harness, NULL, 0);
harness.ssh->useAgent = 1;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_BAD_ARGUMENT);
AssertIntEQ(harness.ssh->channelListSz, 0);
AssertIntEQ(harness.io.outSz, 0);
AssertIntEQ(harness.ssh->error, WS_SUCCESS);
FreeChannelOpenHarness(&harness);
}
#endif /* !NO_WOLFSSH_CLIENT */
#endif
@ -13427,6 +13523,12 @@ int main(int argc, char** argv)
#endif
#ifdef WOLFSSH_AGENT
TestAgentChannelNullAgentSendsOpenFail();
TestAgentChannelOpenWithoutRequest();
TestAgentChannelOpenFlushesQueuedOpen();
TestAgentChannelOpenSendFailureCleansUp();
#ifndef NO_WOLFSSH_CLIENT
TestAgentChannelOpenOnClientRefused();
#endif
#endif
#endif /* NO_WOLFSSH_SERVER */
#if defined(WOLFSSH_AGENT) && !defined(WOLFSSH_NO_ED25519) \

View File

@ -181,6 +181,18 @@ WOLFSSH_API int wolfSSH_CTX_set_agent_cb(WOLFSSH_CTX* ctx,
WOLFSSH_API int wolfSSH_set_agent_cb_ctx(WOLFSSH* ssh, void* ctx);
WOLFSSH_API int wolfSSH_CTX_AGENT_enable(WOLFSSH_CTX* ctx, byte isEnabled);
WOLFSSH_API int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled);
/* Server side. Opens the auth-agent@openssh.com channel to the client once
* the peer's auth-agent-req@openssh.com asks for forwarding. wolfSSH_accept()
* does it on the default path; an application driving its own channels polls
* this instead. Opens one channel, then flushes what of the open is queued.
* Returns WS_SUCCESS, WS_BAD_ARGUMENT before the peer asks or on a client
* session, WS_WANT_READ or WS_WANT_WRITE while output is still queued,
* WS_SSH_NULL_E, WS_MEMORY_E, or whatever the send reports. WS_SUCCESS says
* the open went out, not that the peer took it; a refusal reaches the
* channel-open-fail callback.
* Only the send records in ssh->error, so a poll ahead of the peer's request
* leaves the session fit for wolfSSH_accept(). */
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);