wolfssh/scp: complete rekey that starts mid-transfer

pull/1011/head
Yosuke Shimizu 2026-06-10 19:32:28 +09:00 committed by John Safranek
parent 7393aaea9b
commit 1efd64743c
2 changed files with 600 additions and 49 deletions

View File

@ -63,6 +63,17 @@ const char scpError[] = "scp error: %s, %d";
const char scpState[] = "scp state: %s";
/* Logs an SCP state error, except a non-blocking WS_WANT_READ/WS_WANT_WRITE,
* which is normal back-pressure the caller retries (matching the quiet
* SCP_SEND_FILE handling) rather than a transfer failure. */
static void LogScpStateError(const char* state, int ret)
{
(void)state;
if (ret != WS_WANT_READ && ret != WS_WANT_WRITE)
WLOG(WS_LOG_ERROR, scpError, state, ret);
}
static int _DumpExtendedData(WOLFSSH* ssh)
{
byte msg[WOLFSSH_DEFAULT_EXTDATA_SZ];
@ -79,6 +90,122 @@ static int _DumpExtendedData(WOLFSSH* ssh)
}
/* Sends sz bytes from data, completing any rekey or full window that blocks
* the send.
*
* Attempts the send; on a WS_WINDOW_FULL (peer window reached zero) or
* WS_REKEYING result, drives wolfSSH_worker once to pick up the peer's window
* adjust or to advance the rekey, then retries the send. The retry is what
* clears the status: wolfSSH_worker does not reset ssh->error, so a loop that
* keyed on ssh->error staying WS_WINDOW_FULL would keep driving the worker
* after the window already reopened and stall the transfer. This mirrors the
* original SCP_SEND_FILE handling, extended to the control-message senders so a
* rekey or full window during a timestamp/header/confirmation send is no longer
* reported as fatal. Termination needs no retry count: in blocking mode
* wolfSSH_worker waits on the socket for the peer's packet; in non-blocking
* mode it returns WS_WANT_READ/WS_WANT_WRITE, which is returned to the caller
* so a stalled rekey cannot spin forever. Returns the byte count from
* wolfSSH_stream_send (>= 0) or a negative error code, matching the wrapped
* call so callers keep their existing return handling.
*/
static int ScpStreamSend(WOLFSSH* ssh, byte* data, word32 sz)
{
int ret = WS_SUCCESS;
int err;
int done = 0;
if (ssh == NULL || data == NULL)
return WS_BAD_ARGUMENT;
/* Flush queued output before sending. Otherwise a KEXINIT enqueued by a
* rekey triggered on the prior send sits unsent while wolfSSH_worker runs
* DoReceive before its own flush, blocking on the socket with the peer
* waiting for our KEXINIT. */
if (wolfSSH_OutputPending(ssh)) {
ret = wolfSSH_SendPacket(ssh);
if (ret < 0)
return ret;
}
while (!done) {
ret = wolfSSH_stream_send(ssh, data, sz);
if (ret >= 0) {
/* sent (full or partial byte count); exits via while (!done) */
done = 1;
}
else {
err = wolfSSH_get_error(ssh);
if (err == WS_WINDOW_FULL || err == WS_REKEYING) {
ret = wolfSSH_worker(ssh, NULL);
err = wolfSSH_get_error(ssh);
/* A non-blocking want surfaces as a generic worker error with
* the want recorded in ssh->error (see GetInputData). Return it
* so the caller retries instead of tearing down the send. */
if (err == WS_WANT_READ || err == WS_WANT_WRITE)
return err;
/* Only a rekey/window/channel-data status means "keep driving".
* Any other negative status is fatal and returned. */
if (ret < 0 && ret != WS_REKEYING && ret != WS_WINDOW_FULL
&& ret != WS_CHAN_RXD)
return ret;
/* otherwise loop and retry the send, which clears the status */
}
else {
/* Fatal or other final status; return it unchanged. */
done = 1;
}
}
}
return ret;
}
/* Reads up to sz bytes into data, completing any rekey that fires mid-read.
*
* Flushes queued output before reading so a KEXINIT enqueued by a receive-side
* highwater rekey is actually sent, otherwise the peer can wait for our KEXINIT
* while we block on the read. On a read that fails with WS_REKEYING the worker
* is driven to finish the rekey and the read is retried. The helper is
* error-code transparent: every other status (WS_EOF, WS_EXTDATA,
* WS_CHANNEL_CLOSED, WS_SOCKET_ERROR_E, WS_WANT_READ/WS_WANT_WRITE, byte count)
* is returned unchanged so each caller keeps its existing branch handling.
*/
static int ScpStreamRead(WOLFSSH* ssh, byte* data, word32 sz)
{
int ret = WS_SUCCESS;
int done = 0;
if (ssh == NULL || data == NULL)
return WS_BAD_ARGUMENT;
do {
if (wolfSSH_OutputPending(ssh)) {
ret = wolfSSH_SendPacket(ssh);
if (ret < 0)
return ret;
}
ret = wolfSSH_stream_read(ssh, data, sz);
if (ret < 0 && wolfSSH_get_error(ssh) == WS_REKEYING) {
/* Drive the rekey to completion, then retry the read. A worker
* status that is not rekey or channel data means the rekey stalled
* or a non-blocking want occurred, so return it rather than
* spin. */
ret = wolfSSH_worker(ssh, NULL);
if (ret < 0 && ret != WS_CHAN_RXD
&& wolfSSH_get_error(ssh) != WS_REKEYING)
return ret;
}
else {
done = 1;
}
} while (!done);
return ret;
}
int DoScpSink(WOLFSSH* ssh)
{
int ret = WS_SUCCESS;
@ -112,7 +239,7 @@ int DoScpSink(WOLFSSH* ssh)
break;
}
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_MESSAGE", ret);
LogScpStateError("RECEIVE_MESSAGE", ret);
break;
}
@ -153,7 +280,7 @@ int DoScpSink(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_CONFIRMATION");
if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret);
LogScpStateError("SEND_CONFIRMATION", ret);
break;
}
@ -164,7 +291,7 @@ int DoScpSink(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_CONFIRMATION");
if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_CONFIRMATION", ret);
LogScpStateError("RECEIVE_CONFIRMATION", ret);
break;
}
@ -175,7 +302,7 @@ int DoScpSink(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_FILE");
if ( (ret = ReceiveScpFile(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_FILE", ret);
LogScpStateError("RECEIVE_FILE", ret);
break;
}
@ -283,13 +410,17 @@ static int SendScpTimestamp(WOLFSSH* ssh)
#endif
bufSz = (int)WSTRLEN(buf);
ret = wolfSSH_stream_send(ssh, (byte*)buf, bufSz);
if (ret != bufSz) {
ret = WS_FATAL_ERROR;
} else {
ret = ScpStreamSend(ssh, (byte*)buf, bufSz);
if (ret == bufSz) {
WLOG(WS_LOG_DEBUG, "scp: sent timestamp: %s", buf);
ret = WS_SUCCESS;
}
/* A non-blocking want is left as WS_WANT_READ/WS_WANT_WRITE for the caller
* to retry (nothing is queued yet, so the resend is clean), consistent with
* the SCP_SEND_FILE data path; only a real short send is fatal. */
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
ret = WS_FATAL_ERROR;
}
return ret;
}
@ -324,13 +455,16 @@ static int SendScpFileHeader(WOLFSSH* ssh)
return WS_BAD_ARGUMENT;
#endif
bufSz = (int)WSTRLEN(filehdr);
ret = wolfSSH_stream_send(ssh, (byte*)filehdr, bufSz);
if (ret != bufSz) {
ret = WS_FATAL_ERROR;
} else {
ret = ScpStreamSend(ssh, (byte*)filehdr, bufSz);
if (ret == bufSz) {
WLOG(WS_LOG_DEBUG, "scp: sent file header: %s", filehdr);
ret = WS_SUCCESS;
}
/* leave a non-blocking want for the caller to retry; only a real short
* send is fatal (see SendScpTimestamp) */
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
ret = WS_FATAL_ERROR;
}
return ret;
}
@ -357,13 +491,16 @@ static int SendScpEnterDirectory(WOLFSSH* ssh)
bufSz = (int)WSTRLEN(buf);
ret = wolfSSH_stream_send(ssh, (byte*)buf, bufSz);
if (ret != bufSz) {
ret = WS_FATAL_ERROR;
} else {
ret = ScpStreamSend(ssh, (byte*)buf, bufSz);
if (ret == bufSz) {
WLOG(WS_LOG_DEBUG, "scp: sent directory msg: %s", buf);
ret = WS_SUCCESS;
}
/* leave a non-blocking want for the caller to retry; only a real short
* send is fatal (see SendScpTimestamp) */
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
ret = WS_FATAL_ERROR;
}
return ret;
}
@ -383,13 +520,16 @@ static int SendScpExitDirectory(WOLFSSH* ssh)
buf[0] = 'E';
buf[1] = '\n';
ret = wolfSSH_stream_send(ssh, (byte*)buf, sizeof(buf));
if (ret != sizeof(buf)) {
ret = WS_FATAL_ERROR;
} else {
ret = ScpStreamSend(ssh, (byte*)buf, sizeof(buf));
if (ret == sizeof(buf)) {
WLOG(WS_LOG_DEBUG, "scp: sent end directory msg: E");
ret = WS_SUCCESS;
}
/* leave a non-blocking want for the caller to retry; only a real short
* send is fatal (see SendScpTimestamp) */
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
ret = WS_FATAL_ERROR;
}
return ret;
}
@ -436,7 +576,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_CONFIRMATION");
if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret);
LogScpStateError("SEND_CONFIRMATION", ret);
break;
}
@ -447,7 +587,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_CONFIRMATION_WITH_RECEIPT");
if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret);
LogScpStateError("SEND_CONFIRMATION", ret);
break;
}
@ -459,8 +599,7 @@ int DoScpSource(WOLFSSH* ssh)
"SCP_RECEIVE_CONFIRMATION_WITH_RECEIPT");
if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError,
"RECEIVE_CONFIRMATION_WITH_RECEIPT", ret);
LogScpStateError("RECEIVE_CONFIRMATION_WITH_RECEIPT", ret);
break;
}
@ -471,7 +610,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_CONFIRMATION");
if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "RECEIVE_CONFIRMATION", ret);
LogScpStateError("RECEIVE_CONFIRMATION", ret);
break;
}
@ -540,7 +679,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_TIMESTAMP");
if ( (ret = SendScpTimestamp(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_TIMESTAMP", ret);
LogScpStateError("SEND_TIMESTAMP", ret);
break;
}
@ -552,7 +691,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_ENTER_DIRECTORY");
if ( (ret = SendScpEnterDirectory(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_ENTER_DIRECTORY", ret);
LogScpStateError("SEND_ENTER_DIRECTORY", ret);
break;
}
@ -564,7 +703,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_EXIT_DIRECTORY");
if ( (ret = SendScpExitDirectory(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_EXIT_DIRECTORY", ret);
LogScpStateError("SEND_EXIT_DIRECTORY", ret);
break;
}
@ -576,7 +715,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_EXIT_DIRECTORY_FINAL");
if ( (ret = SendScpExitDirectory(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_EXIT_DIRECTORY", ret);
LogScpStateError("SEND_EXIT_DIRECTORY", ret);
break;
}
@ -587,7 +726,7 @@ int DoScpSource(WOLFSSH* ssh)
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE_HEADER");
if ( (ret = SendScpFileHeader(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SEND_FILE_HEADER", ret);
LogScpStateError("SEND_FILE_HEADER", ret);
break;
}
@ -598,12 +737,15 @@ int DoScpSource(WOLFSSH* ssh)
case SCP_SEND_FILE:
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE");
ret = wolfSSH_stream_send(ssh, ssh->scpFileBuffer,
ssh->scpBufferedSz);
if (ret == WS_WINDOW_FULL || ret == WS_REKEYING) {
ret = wolfSSH_worker(ssh, NULL);
if (ret == WS_SUCCESS || ssh->error == WS_WANT_READ)
continue;
ret = ScpStreamSend(ssh, ssh->scpFileBuffer,
ssh->scpBufferedSz);
if (ret == WS_WANT_READ || ret == WS_WANT_WRITE) {
/* ScpStreamSend already drove the worker through any rekey
* or full window; a non-blocking want means the socket is
* not ready. Surface it for the caller to retry without
* closing the file mid-transfer. scpBufferedSz and
* scpFileOffset are preserved for the next call. */
break;
}
if (ret == WS_EXTDATA) {
_DumpExtendedData(ssh);
@ -718,14 +860,14 @@ int DoScpRequest(WOLFSSH* ssh)
case SCP_SINK:
WLOG(WS_LOG_DEBUG, scpState, "SCP_SINK");
if ( (ret = DoScpSink(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SCP_SINK", ret);
LogScpStateError("SCP_SINK", ret);
}
break;
case SCP_SOURCE:
WLOG(WS_LOG_DEBUG, scpState, "SCP_SOURCE");
if ( (ret = DoScpSource(ssh)) < WS_SUCCESS) {
WLOG(WS_LOG_ERROR, scpError, "SCP_SOURCE", ret);
LogScpStateError("SCP_SOURCE", ret);
}
break;
}
@ -736,7 +878,7 @@ int DoScpRequest(WOLFSSH* ssh)
/* Peer MUST send back a SSH_MSG_CHANNEL_CLOSE unless already
sent*/
ret = wolfSSH_stream_read(ssh, buf, 1);
ret = ScpStreamRead(ssh, buf, 1);
if (ret == WS_SOCKET_ERROR_E || ret == WS_CHANNEL_CLOSED) {
WLOG(WS_LOG_DEBUG, scpState, "Peer hung up, but SCP is done");
ret = WS_SUCCESS;
@ -1395,6 +1537,33 @@ int ReceiveScpMessage(WOLFSSH* ssh)
return WS_BUFFER_E;
}
/* Flush queued output before polling. A KEXINIT enqueued by a rekey
* would otherwise sit unsent while wolfSSH_worker runs DoReceive before
* its own flush, deadlocking against a peer that waits for our
* KEXINIT. */
if (wolfSSH_OutputPending(ssh)) {
ret = wolfSSH_SendPacket(ssh);
if (ret < 0)
return ret;
}
/* If channel data is already buffered, read it directly rather than
* polling the socket. A control message delivered into the channel
* buffer while a rekey was completing leaves wolfSSH_worker returning
* the rekey status (not WS_CHAN_RXD), so without this the buffered
* message is never read and the next worker blocks on the socket. */
if (wolfSSH_stream_peek(ssh, NULL, 1) > 0) {
sz = wolfSSH_stream_read(ssh, buf + ssh->scpRecvMsgSz,
DEFAULT_SCP_MSG_SZ - ssh->scpRecvMsgSz);
/* match the WS_CHAN_RXD branch below: return on a non-positive
* read so a hypothetical zero cannot re-loop this peek path */
if (sz <= 0)
return sz;
ssh->scpRecvMsgSz += sz;
sz = ssh->scpRecvMsgSz;
continue;
}
err = wolfSSH_worker(ssh, &lastChannel);
if (err < 0) {
int rc;
@ -1517,7 +1686,7 @@ int ReceiveScpFile(WOLFSSH* ssh)
}
if (ret == WS_SUCCESS) {
ret = wolfSSH_stream_read(ssh, ssh->scpFileBuffer, partSz);
ret = ScpStreamRead(ssh, ssh->scpFileBuffer, partSz);
if (ret > 0) {
ssh->scpFileBufferSz = ret;
}
@ -1559,11 +1728,8 @@ int SendScpConfirmation(WOLFSSH* ssh)
/* skip first byte for accurate strlen, may be 0 */
msgSz = (int)XSTRLEN(msg + 1) + 1;
ret = wolfSSH_stream_send(ssh, (byte*)msg, msgSz);
if (ret != msgSz || ssh->scpConfirm == WS_SCP_ABORT) {
ret = WS_FATAL_ERROR;
} else {
ret = ScpStreamSend(ssh, (byte*)msg, msgSz);
if (ret == msgSz && ssh->scpConfirm != WS_SCP_ABORT) {
ret = WS_SUCCESS;
WLOG(WS_LOG_DEBUG, "scp: sent confirmation (code: %d)", msg[0]);
@ -1573,6 +1739,11 @@ int SendScpConfirmation(WOLFSSH* ssh)
ssh->scpConfirmMsgSz = 0;
}
}
/* leave a non-blocking want for the caller to retry; a real short send or a
* peer abort is fatal (see SendScpTimestamp) */
else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) {
ret = WS_FATAL_ERROR;
}
return ret;
}
@ -1587,7 +1758,7 @@ int ReceiveScpConfirmation(WOLFSSH* ssh)
return WS_BAD_ARGUMENT;
WMEMSET(msg, 0, sizeof(msg));
msgSz = wolfSSH_stream_read(ssh, msg, DEFAULT_SCP_MSG_SZ);
msgSz = ScpStreamRead(ssh, msg, DEFAULT_SCP_MSG_SZ);
if (msgSz < 0) {
if (wolfSSH_get_error(ssh) == WS_EXTDATA)

View File

@ -49,7 +49,7 @@
#include <wolfssh/agent.h>
#endif
#ifdef WOLFSSH_SFTP
#if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP)
#define WOLFSSH_TEST_LOCKING
#ifndef SINGLE_THREADED
#define WOLFSSH_TEST_THREADING
@ -62,6 +62,9 @@
#endif
#include <wolfssh/test.h>
#include "tests/api.h"
#ifdef WOLFSSH_TEST_ECHOSERVER
#include "examples/echoserver/echoserver.h"
#endif
/* for echoserver test cases */
int myoptind = 0;
@ -1249,8 +1252,6 @@ static void test_wolfSSH_agent_signrequest_success(void)
#if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) && \
!defined(SINGLE_THREADED)
#include "examples/echoserver/echoserver.h"
byte userPassword[256];
static int sftpUserAuth(byte authType, WS_UserAuthData* authData, void* ctx)
@ -1288,6 +1289,14 @@ static int AcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz,
return 0;
}
/* Counts keying completions (initial handshake and each rekey) so the test can
* assert a mid-transfer rekey actually fired. ctx points to an int counter. */
static void sftpKeyingCompleteCb(void* ctx)
{
if (ctx != NULL)
(*(int*)ctx)++;
}
/* performs connection to port, sets WOLFSSH_CTX and WOLFSSH on success
* caller needs to free ctx and ssh when done
*/
@ -1870,6 +1879,7 @@ static void sftp_rekey_test(int nonBlock)
int argsCount;
int err;
int tries;
int kexCount = 0;
WS_SOCKET_T clientFd;
WS_SFTPNAME* ls;
int i;
@ -1900,6 +1910,12 @@ static void sftp_rekey_test(int nonBlock)
AssertNotNull(ctx);
AssertNotNull(ssh);
/* Count keying completions from here on. The initial handshake already ran
* inside sftp_client_connect, so kexCount stays 0 until the highwater rekey
* fires mid-SFTP; the AssertIntGT below then proves it did. */
wolfSSH_SetKeyingCompletionCb(ctx, sftpKeyingCompleteCb);
wolfSSH_SetKeyingCompletionCbCtx(ssh, &kexCount);
/* Handshake completed in blocking mode; switch to non-blocking so the
* LS/rekey phase exercises the WS_WANT_READ/WS_WANT_WRITE early-return
* path in buffer_send/buffer_read. */
@ -1946,6 +1962,10 @@ static void sftp_rekey_test(int nonBlock)
ls = NULL;
}
/* A mid-SFTP rekey must have fired; otherwise the test silently stops
* exercising the buffer_send/buffer_read rekey paths it was written for. */
AssertIntGT(kexCount, 0);
tries = 0;
do {
argsCount = wolfSSH_shutdown(ssh);
@ -2454,6 +2474,362 @@ static void test_wolfSSH_SFTP_SetDefaultPath(void) { ; }
#endif /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */
#if defined(WOLFSSH_SCP) && !defined(NO_WOLFSSH_CLIENT) && \
!defined(SINGLE_THREADED) && !defined(NO_FILESYSTEM) && \
!defined(WOLFSSH_SCP_USER_CALLBACKS) && !defined(WOLFSSH_ZEPHYR)
/* Upper bound on non-blocking retry iterations. A legitimate transfer across a
* forced rekey completes in well under this; the bound keeps a regression from
* hanging CI by tripping the AssertIntLE below instead. */
#define SCP_REKEY_MAX_TRIES 100
/* Payload larger than the forced highwater so the transfer straddles it. */
#define SCP_REKEY_FILE_SZ 2048
static byte scpUserPassword[256];
static int scpUserAuth(byte authType, WS_UserAuthData* authData, void* ctx)
{
int ret = WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
if (authType == WOLFSSH_USERAUTH_PASSWORD) {
const char* password = (const char*)ctx;
word32 passwordSz;
if (password != NULL) {
passwordSz = (word32)WSTRLEN(password);
if (passwordSz > (word32)sizeof(scpUserPassword))
passwordSz = (word32)sizeof(scpUserPassword);
WMEMCPY(scpUserPassword, password, passwordSz);
authData->sf.password.password = scpUserPassword;
authData->sf.password.passwordSz = passwordSz;
ret = WOLFSSH_USERAUTH_SUCCESS;
}
}
return ret;
}
static int scpAcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz,
void* ctx)
{
(void)pubKey;
(void)pubKeySz;
(void)ctx;
return 0;
}
/* Counts keying completions (initial handshake and each rekey) so the test can
* assert a mid-transfer rekey actually fired. ctx points to an int counter. */
static void scpKeyingCompleteCb(void* ctx)
{
if (ctx != NULL)
(*(int*)ctx)++;
}
/* Writes sz bytes of buf to name. Returns 0 on success. */
static int scpWriteTestFile(const char* name, const byte* buf, word32 sz)
{
WFILE* fp = NULL;
int ret = 0;
if (WFOPEN(NULL, &fp, name, "wb") != 0 || fp == NULL)
return -1;
if (WFWRITE(NULL, buf, 1, sz, fp) != sz)
ret = -1;
WFCLOSE(NULL, fp);
return ret;
}
/* Returns 0 if the first sz bytes of name match expect. */
static int scpFilesMatch(const char* name, const byte* expect, word32 sz)
{
WFILE* fp = NULL;
byte got[SCP_REKEY_FILE_SZ];
int ret = 0;
if (sz > sizeof(got))
return -1;
if (WFOPEN(NULL, &fp, name, "rb") != 0 || fp == NULL)
return -1;
if (WFREAD(NULL, got, 1, sz, fp) != sz)
ret = -1;
if (ret == 0 && XMEMCMP(got, expect, sz) != 0)
ret = -1;
WFCLOSE(NULL, fp);
return ret;
}
/* Connects an SCP client to port, completes the SSH handshake and opens the
* exec channel carrying cmd, leaving ssh ready for wolfSSH_SCP_to/from. Doing
* the handshake here (rather than inside the transfer call) lets the caller set
* a low highwater before the data phase so a rekey fires mid-transfer.
*/
static void scp_client_connect(WOLFSSH_CTX** ctx, WOLFSSH** ssh, int port,
const char* cmd)
{
WS_SOCKET_T sockFd = WOLFSSH_SOCKET_INVALID;
SOCKADDR_IN_T clientAddr;
socklen_t clientAddrSz = sizeof(clientAddr);
int ret;
char* host = (char*)wolfSshIp;
const char* username = "jill";
const char* password = "upthehill";
if (ctx == NULL || ssh == NULL)
return;
*ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
if (*ctx == NULL)
return;
wolfSSH_CTX_SetPublicKeyCheck(*ctx, scpAcceptAnyServerHostKey);
wolfSSH_SetUserAuth(*ctx, scpUserAuth);
*ssh = wolfSSH_new(*ctx);
if (*ssh == NULL) {
wolfSSH_CTX_free(*ctx);
*ctx = NULL;
return;
}
build_addr(&clientAddr, host, port);
tcp_socket(&sockFd, ((struct sockaddr_in *)&clientAddr)->sin_family);
if (sockFd < 0) {
wolfSSH_free(*ssh);
wolfSSH_CTX_free(*ctx);
*ctx = NULL;
*ssh = NULL;
return;
}
ret = connect(sockFd, (const struct sockaddr *)&clientAddr, clientAddrSz);
if (ret != 0) {
WCLOSESOCKET(sockFd);
wolfSSH_free(*ssh);
wolfSSH_CTX_free(*ctx);
*ctx = NULL;
*ssh = NULL;
return;
}
wolfSSH_SetUserAuthCtx(*ssh, (void*)password);
ret = wolfSSH_SetUsername(*ssh, username);
if (ret == WS_SUCCESS)
ret = wolfSSH_SetChannelType(*ssh, WOLFSSH_SESSION_EXEC, (byte*)cmd,
(word32)WSTRLEN(cmd));
if (ret == WS_SUCCESS)
ret = wolfSSH_set_fd(*ssh, (int)sockFd);
if (ret == WS_SUCCESS)
ret = wolfSSH_connect(*ssh);
if (ret != WS_SUCCESS) {
WCLOSESOCKET(sockFd);
wolfSSH_free(*ssh);
wolfSSH_CTX_free(*ctx);
*ctx = NULL;
*ssh = NULL;
return;
}
}
/* Drives an SCP transfer with a forced mid-transfer rekey.
*
* toServer == 0: client SINK (wolfSSH_SCP_from), exercises ScpStreamRead, the
* confirmed hang path. toServer == 1: client SOURCE
* (wolfSSH_SCP_to), exercises the ScpStreamSend rekey/window
* drain loop. nonBlock drives the non-blocking retry path.
*/
static void scp_rekey_test(int nonBlock, int toServer)
{
func_args ser;
tcp_ready ready;
int argsCount;
int ret;
int err;
int tries;
int kexCount = 0;
word32 i;
WS_SOCKET_T clientFd;
#ifdef USE_WINDOWS_API
DWORD rcvTimeout = 20000;
#else
struct timeval rcvTimeout;
#endif
byte fileData[SCP_REKEY_FILE_SZ];
char cmd[64];
const char* args[10];
WOLFSSH_CTX* ctx = NULL;
WOLFSSH* ssh = NULL;
/* Fixed names used for filesystem create/verify/cleanup. The *Buf copies
* are what get passed to the SCP API, which rewrites the path in place
* (rename/clean), so they cannot be reused to name the file afterward. The
* leading "./" keeps a directory component so the base-dir open succeeds,
* as the real scpclient passes $PWD-prefixed paths. */
const char* srcName = "./scp_rekey_src.txt";
const char* fromName = "./scp_rekey_from.txt";
const char* toName = "./scp_rekey_to.txt";
char srcBuf[32];
char fromBuf[32];
char toBuf[32];
const char* verifyName;
THREAD_TYPE serThread;
/* mutable copies for the SCP API (rewritten in place during the transfer) */
WSTRNCPY(srcBuf, srcName, sizeof(srcBuf));
WSTRNCPY(fromBuf, fromName, sizeof(fromBuf));
WSTRNCPY(toBuf, toName, sizeof(toBuf));
/* deterministic source content */
for (i = 0; i < SCP_REKEY_FILE_SZ; i++)
fileData[i] = (byte)(i & 0xff);
AssertIntEQ(scpWriteTestFile(srcName, fileData, SCP_REKEY_FILE_SZ), 0);
WMEMSET(&ser, 0, sizeof(func_args));
argsCount = 0;
args[argsCount++] = ".";
args[argsCount++] = "-1";
#ifndef USE_WINDOWS_API
args[argsCount++] = "-p";
args[argsCount++] = "0";
#endif
ser.argv = (char**)args;
ser.argc = argsCount;
ser.signal = &ready;
InitTcpReady(ser.signal);
ThreadStart(echoserver_test, (void*)&ser, &serThread);
WaitTcpReady(&ready);
/* -f: server is source (client SINK); -t: server is sink (client SOURCE) */
if (toServer) {
WSNPRINTF(cmd, sizeof(cmd), "scp -t %s", toName);
verifyName = toName;
}
else {
WSNPRINTF(cmd, sizeof(cmd), "scp -f %s", srcName);
verifyName = fromName;
}
scp_client_connect(&ctx, &ssh, ready.port, cmd);
AssertNotNull(ctx);
AssertNotNull(ssh);
/* Count keying completions from here on. The initial handshake already ran
* inside scp_client_connect, so kexCount stays 0 until the highwater-driven
* rekey fires mid-transfer; the AssertIntGT below then proves it did. */
wolfSSH_SetKeyingCompletionCb(ctx, scpKeyingCompleteCb);
wolfSSH_SetKeyingCompletionCbCtx(ssh, &kexCount);
/* handshake done in blocking mode; switch to non-blocking for the data
* phase so the WS_WANT_READ/WS_WANT_WRITE retry path is exercised */
clientFd = wolfSSH_get_fd(ssh);
if (nonBlock)
tcp_set_nonblocking(&clientFd);
/* Bound the blocking-mode recv so a KEXINIT/rekey deadlock regression fails
* the AssertIntEQ below instead of hanging CI forever. The
* SCP_REKEY_MAX_TRIES bound only covers the non-blocking retry loop; a
* non-blocking socket never blocks in recv, so this is a no-op there. */
#ifdef USE_WINDOWS_API
(void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO,
(const char*)&rcvTimeout, sizeof(rcvTimeout));
#else
rcvTimeout.tv_sec = 20;
rcvTimeout.tv_usec = 0;
(void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO,
&rcvTimeout, sizeof(rcvTimeout));
#endif
/* 256 is well below the 2 KB payload, so the highwater check fires partway
* through and the ScpStreamRead/ScpStreamSend rekey handling must carry the
* transfer to completion. */
AssertIntEQ(wolfSSH_SetHighwater(ssh, 256), WS_SUCCESS);
/* The retry loop only applies to non-blocking. In blocking mode the
* ScpStreamRead/ScpStreamSend fixes must carry the rekey transparently, so
* a single call completes the transfer; gating on nonBlock keeps the
* blocking path from masking a regression that leaves WS_REKEYING set. */
tries = 0;
do {
if (toServer)
ret = wolfSSH_SCP_to(ssh, srcBuf, toBuf);
else
ret = wolfSSH_SCP_from(ssh, srcBuf, fromBuf);
err = wolfSSH_get_error(ssh);
/* tcp_select() waits for receive-readiness; on WS_WANT_WRITE it has no
* write event to wait on, so its 1s timeout is the intended (rare)
* fallback that yields the CPU instead of busy-spinning. */
if (nonBlock && ret != WS_SUCCESS && (err == WS_WANT_READ
|| err == WS_WANT_WRITE || err == WS_REKEYING
|| err == WS_CHAN_RXD))
tcp_select(clientFd, 1);
tries++;
} while (nonBlock && ret != WS_SUCCESS && (err == WS_WANT_READ
|| err == WS_WANT_WRITE || err == WS_REKEYING
|| err == WS_CHAN_RXD)
&& tries <= SCP_REKEY_MAX_TRIES);
/* Fails fast (instead of hanging CI) if a regression keeps the transfer
* stuck in a want/rekey state past the retry bound. */
AssertIntLE(tries, SCP_REKEY_MAX_TRIES);
AssertIntEQ(ret, WS_SUCCESS);
/* A mid-transfer rekey must have fired; otherwise the test silently stops
* exercising the ScpStreamSend/ScpStreamRead rekey paths it was written
* for. */
AssertIntGT(kexCount, 0);
/* best-effort shutdown; the completed transfer above is the real assertion */
ret = wolfSSH_shutdown(ssh);
(void)ret;
clientFd = wolfSSH_get_fd(ssh);
WCLOSESOCKET(clientFd);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
ThreadJoin(serThread);
/* verify the transferred file matches the source once the server is done */
AssertIntEQ(scpFilesMatch(verifyName, fileData, SCP_REKEY_FILE_SZ), 0);
WREMOVE(NULL, srcName);
WREMOVE(NULL, verifyName);
}
static void test_wolfSSH_SCP_ReKey(void)
{
scp_rekey_test(0, 0);
}
static void test_wolfSSH_SCP_ReKey_NonBlock(void)
{
scp_rekey_test(1, 0);
}
static void test_wolfSSH_SCP_ReKey_ToServer(void)
{
scp_rekey_test(0, 1);
}
static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void)
{
scp_rekey_test(1, 1);
}
#else /* WOLFSSH_SCP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED &&
* !NO_FILESYSTEM && !WOLFSSH_SCP_USER_CALLBACKS && !WOLFSSH_ZEPHYR */
static void test_wolfSSH_SCP_ReKey(void) { ; }
static void test_wolfSSH_SCP_ReKey_NonBlock(void) { ; }
static void test_wolfSSH_SCP_ReKey_ToServer(void) { ; }
static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void) { ; }
#endif
#ifdef USE_WINDOWS_API
static byte color_test[] = {
0x1B, 0x5B, 0x34, 0x6D, 0x75, 0x6E, 0x64, 0x65,
@ -3162,6 +3538,10 @@ int wolfSSH_ApiTest(int argc, char** argv)
/* SCP tests */
test_wolfSSH_SCP_CB();
test_wolfSSH_SCP_ReKey();
test_wolfSSH_SCP_ReKey_NonBlock();
test_wolfSSH_SCP_ReKey_ToServer();
test_wolfSSH_SCP_ReKey_ToServer_NonBlock();
/* SFTP tests */
test_wolfSSH_SFTP_SendReadPacket();