GetOpenSshKey() leaves no keyId for a key-type name it does not know
and returns WS_UNIMPLEMENTED_E itself, so IdentifyOpenSshKey() never
sees ID_UNKNOWN after a failure and passes that result straight on.
- tests/unit.c adds test_IdentifyOpenSshKeyUnknownName() for the
WS_UNIMPLEMENTED_E result on an unknown key-type name.
Issue: F-13330
wolfSSH_SFTP_LSTAT() moves on to the request ID check only once the
reply buffer is allocated, and clears its state when the allocation
fails, so the next STAT or LSTAT starts a new request.
- test LSTAT against a matched, misnumbered and unallocatable reply
Issue: F-17047
SFTP_CheckReqId() matches each reply header against the outstanding
request ID, advances the ID once on a match, and sets
WS_SFTP_BAD_REQ_ID on a mismatch. Every client response handler calls
it before acting on the reply, including the STATUS-only ones.
- DoName and GetHandle set WS_SFTP_BAD_REQ_TYPE on an unexpected
reply type and free their state
- SetSTAT, Close, Remove and RMDIR now check the reply ID
- correct the WS_SFTP_BAD_REQ_TYPE comment in error.h
- test each request against a matched, misnumbered and mistyped reply
Issue: F-10547, F-13909, F-10566
The example client prints the keyboard-interactive name, instruction
and prompts through ClientPrintSafe(), which prints a '.' in place of
each control character other than tab and newline, DEL, and each byte
0x80 and up, per RFC 4256 section 3.3 and RFC 4251 section 9.2.
- tests: auth case capturing the printed strings
Issue: F-15534
ClientUserAuth() hands the library its static password buffer, and the
library sends from it after the callback returns. The clients now wipe
it, and the example's keyboard-interactive responses, as soon as
connect returns instead of holding them for the whole session.
- ClientClearPassword() in both common.c copies, called after connect
in the wolfssh app, the example client and the sftp client
- the password arm wipes before each prompt and on failure, so a retry
keeps no tail of a longer password
- the keyboard arm wipes its scratch copy after each response
- tests: regress and auth cases for the retry tail, the wipe and a
failed read
Issue: F-8846
Userauth callbacks compare passwords and digests outside the library,
where the internal ConstantCompare() is hidden. Export a wrapper, and
add WS_CONSTANTCOMPARE(), which, like WS_FORCEZERO(), maps to
wolfCrypt's wc_ConstantCompare() when the wolfSSL build provides it
(v5.9.4 and later) and to the local loop otherwise.
- misc.c: build the local loop only as the fallback, with a volatile
accumulator
- internal.c: compare MACs and the CBC check block with the macro
- ssh.h: point the userauth callback documentation at the export
- api.c: test match, mismatch, length and argument handling
Issue: F-7513, F-7514
SSH_FXP_REMOVE does not take directories and SSH_FXP_RENAME fails on an
existing newpath (draft-ietf-secsh-filexfer-02 6.5). The non-Windows
handlers check with an lstat first, oldpath before newpath, and REMOVE
unlinks. A regular file renames by linkat() and unlink(), which refuses
an existing name atomically; anything else checks then calls rename().
- configure: probe for linkat()
- port.h: WUNLINK on POSIX, WLINK as linkat() when configure finds it
- FatFs, Nucleus: only "/" and "/." stand in for the root
- regress: test both against files, directories, symlinks and a
read-only directory
Issue: F-15535, F-15536
SFTP_GetHeader now takes a limit from its caller and refuses a declared
body over it, or one too short to hold the header, with WS_BUFFER_E.
Every client reply handler allocated a buffer of the peer-declared size
first, so one 9-byte header could make the client allocate 2 GiB.
- STATUS, ATTRS and HANDLE replies are held to WOLFSSH_MAX_SFTP_PACKET
- NAME keeps WOLFSSH_MAX_SFTP_NAME; DoName's own check moves into
the call
- a READ reply may also be as large as the caller's buffer plus its
length prefix
- SendWritePacket keeps an error already set instead of
WS_SFTP_BAD_HEADER
- regress test drives all eleven client calls at and past the bound
Issue: F-10598, F-13430, F-14433
The known_hosts search skips an entry whose host matches but whose key
type differs, so a host pinned with one type that presents another was
reported as an unknown host. The check now remembers that the host is
known with another type and says so before offering to add the new one,
as OpenSSH does.
- note the first other-type entry's key type and line in the loop
- print a warning with that entry, the host and the new fingerprint
before the add prompt; a later same-type entry still matches or
rejects as before
- reject a server key type that is not printable ASCII before it is
printed or stored
- regress.c: add TestKnownHostsOtherType
Issue: ZD-22634
STAT, LSTAT, OPEN, REMOVE, RENAME and OPENDIR take the status for a
failed call from errno, as OpenSSH's sftp-server does: ENOENT and
ENOTDIR answer SSH_FX_NO_SUCH_FILE, EACCES and EPERM
SSH_FX_PERMISSION_DENIED, anything else SSH_FX_FAILURE. Only the
default POSIX file system sets errno; other ports keep their old status.
- OPENDIR on POSIX no longer reports every failure as NO_SUCH_FILE
Issue: F-16081
RecvOpenDir() allocates the directory node, its name and the HANDLE
reply before linking the node into dirList, so an allocation failure
leaves no handle the peer was never told about. Both the POSIX and the
Windows branch.
Issue: F-10551
RecvReadDir() puts at most WOLFSSH_MAX_SFTP_READDIR entries (default
100) in one NAME reply and keeps the handle's place, so a large
directory takes several requests. The readdir helpers return WS_EOF at
the end of the directory; any other failure, an allocation included,
answers SSH_FX_FAILURE instead of EOF or a short listing.
- free the entry list on every path
- the POSIX helper tells a readdir() error from the end by errno
- an entry whose full path does not fit is listed without attributes,
and on Windows one whose name does not convert is skipped
- a SFTP_Name_readdir hook failure other than WS_MEMORY_E ends the
listing; WS_NEXT_ERROR still marks the last entry
- WOLFSSH_MAX_SFTP_READDIR below 1 is a build error
Issue: F-10552, F-13429
wolfSSH_SFTP_read() answers a request type it has no handler for,
SSH_FXP_EXTENDED among them, with SSH_FX_OP_UNSUPPORTED rather than
the generic SSH_FX_FAILURE (draft-ietf-secsh-filexfer-02 Section 7).
Issue: F-8840
RFC 4253 Section 4.2 bars the minus sign from softwareversion.
ValidateProtoId now scans the body up to the first space and rejects a
'-' there; the comments after the space may still carry one.
- document the rule on wolfSSH_CTX_SetSshProtoIdStr() in ssh.h
- unit vectors for a dash in softwareversion and in the comments
Issue: F-16082
The -p option of wolfssh, client, wolfscp, wolfsftp and echoserver now
goes through a strtol check, so a value that is not wholly a number in
range exits with an error instead of wrapping into a different port.
- apps/wolfssh: ClientParsePort(), shared with ClientParseDestination(),
which also rejects an empty "ssh://host:" port instead of defaulting
- examples: a ClientParsePort() in examples/client/common.c; port 0 is
rejected in every build, not only with a main driver
- echoserver: accepts 0 to 65535, keeping 0 for the test harness
- add TestClientParsePort and an empty URI port case to regress.test
Issue: F-14763, F-14767, F-14768, F-12552
It was the one channel sender that read ssh->defaultPeerChannelId
without a NULL check. It now returns WS_BAD_ARGUMENT for a NULL ssh and
logs entry and exit like its siblings.
- add TestTerminalResizeNullSsh to regress.test
Issue: F-14769
DoReceive() wipes a packet that fails to decrypt, authenticate or pass
the length checks before it returns. Under CBC the wipe waits until
DiscardPacket() has run its MAC, then covers a fixed MAX_PACKET_SZ, so
it adds nothing that depends on the decrypted length.
- BundlePacket() wipes the aborted packet it rewinds over, for every
sender
- tests: reject and discard paths check the packet is gone; the
BundlePacket() failure test checks the output buffer tail
Issue: F-14437, F-14438
The channel read paths wipe the bytes they hand to the application, and
GrowBuffer() wipes the stale copy its in-place compaction leaves above
the new length. _UpdateChannelWindow() compacts through GrowBuffer()
instead of its own WMEMMOVE, so it gets the same wipe.
- _BufferTake() serves wolfSSH_stream_read(), _ChannelRead() and
_ChannelReadExt()
- tests: short reads, a compacting read, a drained stderr chunk in the
inline buffer, and GrowBuffer() compaction
Issue: F-11677, F-8848, F-13438, F-10593, F-16109
A client refuses shell, exec, subsystem and pty-req channel requests
and direct-tcpip opens before any callback runs (RFC 4254 6.2, 6.5,
7.2). CHANNEL_OPEN_CONFIRMATION and CHANNEL_OPEN_FAILURE are a
protocol error unless the channel's own open is still outstanding.
- SshInit() installs the default WS_TermResize for servers only.
- Drop the Windows clients' raw-HANDLE resize context; nothing uses it.
- Move the channel request builders out of the server-only test block.
Issue: F-10582, F-13923, F-15533, F-15572, F-14434
CheckAlgoList() skips unknown names, but the setters keep the caller's
string and SendKexInit() sends it verbatim. Every name in the list now
has to be at most 64 bytes of printable US-ASCII with no space and at
most one '@', not first or last, per RFC 4251 Section 6 and RFC 4250
Section 4.6, or the setter returns WS_INVALID_ALGO_ID.
- internal.h: add WOLFSSH_MAX_ALGO_NAME_SZ for the 64-byte cap
- api.c: cover the 64/65-byte bound, space, tab, DEL, high bytes, the
at-sign count and an at-sign at either end
- ssh.h: state the name format in the setter contract
Issue: F-15529
Classic DH e and f are mpints (RFC 4251 section 5), so a value with
the top bit set and no leading zero is negative. KeyAgreeDh_client()
and KeyAgreeDh_server() reject one before the range check, and the key
exchange ends with the usual KEY_EXCHANGE_FAILED disconnect.
- add test_KeyAgreeDh_rejectsNegativePeer() for both sides
- pad e in test_DhGexGroup16KeyAgree() the way SendKexDhInit() does
Issue: F-15532
The host key parsers compare the format identifier at the head of the
key blob (RFC 4253 section 6.6) against the expected name and return
WS_INVALID_ALGO_ID on a mismatch, as ParseECCPubKey() already does.
RSA blobs must say "ssh-rsa", Ed25519 "ssh-ed25519", and ML-DSA the
negotiated algorithm name.
- add GetPubKeyFormat() and use it in place of GetSkip()
- add the wolfSSH_TestParseMlDsaPubKey() hook
- test a mismatched name for each key type
Issue: F-15666
ParseRSAPubKey() and ParseRSAPubKeyCert() run the server's host key
through CheckRsaKeyBits(), so a client rejects an RSA host key under
WOLFSSH_RSA_MIN_KEY_BITS the same way a server rejects a short user
auth key. The failure ends the key exchange in DoKexDhReply().
- move CheckRsaKeyBits() above the host key parsers
- ParseRSAPubKey() maps only the decode failure to WS_RSA_E, so a
parse error or a short key keeps its own code
- test 1024 and 2047 bit host keys in test_ParseRSAPubKey()
Issue: F-14687
C++ rejects three C idioms the tree uses: a conditional mixing an enum
constant with a byte, one mixing INADDR_ANY with a pointer, and a {0}
initializer for a struct. Cast the integer arms of the conditionals,
and clear the structs with WMEMSET instead.
- internal.c, regress.c: cast the ID_UNKNOWN arm to byte
- test.h: cast INADDR_ANY through size_t to const char*
- keygen.c, certman.c, echoserver.c, auth.c: replace {0} with WMEMSET
With WOLFSSH_NO_DH_GEX_SHA256 a guessed KEXDH_GEX_REQUEST was answered
with UNIMPLEMENTED and left the skip armed, so the server then dropped
the real KEXDH_INIT and stalled. DoPacket() now dispatches the message
in every build, and a stub DoKexDhGexRequest() skips only a guess.
- tests: a guessed GEX request through DoReceive is skipped silently
- tests: the GEX request skip case runs without GEX support
RFC 4253 section 7 counts a first_kex_packet_follows guess as right
only when the client's and server's first KEX and host key algorithms
are the same. DoKexInit() now compares the client's guesses with the
server's own first choices, not only with the negotiated ids.
- tests: first choices differ but the guess is negotiated: skip
- tests: same first choices in longer lists: do not skip
Issue: F-15530
RFC 4254 sec 6.5 lets only one shell, exec or subsystem request succeed
per channel. DoChannelRequestSession() now refuses any request after
the first grant without asking a callback, so the granted session and
its command stand.
- a refusal before any grant leaves the channel with no command
- ssh.h documents the refusal; wolfsshd's own check is defence in depth
- regress.c sends a second exec, shell and subsystem after a granted
exec, and a second exec over a generic-callback grant
- unit.c expects a shell after an exec to be refused and checks that a
refused exec's command is scrubbed
On a loaded host the server's replies can already be waiting when the
non-blocking client first calls RealPath, so it finishes without a
WS_WANT_READ. The negotiation test now reports that run instead of
failing it, and still requires the name within the retry bound.
test_wolfSSH_SCP_SendSymlinkReject() calls wsScpSendCallback(),
wIsSymlink(), and other WOLFSSH_LOCAL helpers. A shared build hides
those symbols, so linking api.c against libwolfssh without
WOLFSSH_TEST_INTERNAL fails. Gate the test like the recursive-entry
test; the autotools api.test still runs it.
The OsshCert_* tests and test_wolfSSH_KeyboardInteractive_emptyPrompt()
call WOLFSSH_LOCAL functions, which the winvs DLL import library does
not export. Gate them on WOLFSSH_TEST_INTERNAL, like the SCP
recursive-entry test, so a DLL build with either feature still links.
- stub the OsshCert_* tests when WOLFSSH_TEST_INTERNAL is off
- gate the RSA and ECDSA certificate vectors, which only those tests use
- stub test_wolfSSH_KeyboardInteractive_emptyPrompt() the same way
test_wolfSSH_SCP_SendRecursiveEntry() drives wsScpSendCallback(), a
WOLFSSH_LOCAL symbol. The autotools api.test defines
WOLFSSH_TEST_INTERNAL and links the test library, so it still runs
there. The MSVC api-test project links the DLL import library, where
the symbol is not exported, so the test now compiles to its stub.
- fixes LNK2001 on wsScpSendCallback in the DLL Release build
The agent relay tests call AppendBlob() from the client-only block, but
it was defined inside the block that needs both client and server, so
--disable-server with the agent enabled failed to compile regress.c. It
now sits with the other shared Append helpers.
wolfSSH_AGENT_SignRequest() writes the sign request and reads the reply
through the whole-message helpers the relay uses, so a short write is
followed up and a reply is read to its declared length before it is
parsed. The setup callback's WS_AgentCbError result becomes
WS_AGENT_CXN_FAIL, and an ECDSA key's reply room fits the SSH blob.
- A short write, a hang-up, or a failed setup is WS_AGENT_CXN_FAIL;
cleanup runs only after a setup that succeeded, and the client
callbacks close their socket when connect() fails.
- PrepareUserAuthRequestEcc() reserves r and s as mpints a pad byte over
the curve size, and BuildUserAuthRequestEcc() hands the agent that
room, checks the reply's algorithm name, and bounds it by outputSz.
- A NULL sig, or a NULL digest or key blob with a size, is
WS_BAD_ARGUMENT; agent.h documents the contract and return codes.
- api: the mock agent's short write is followed up, a reply read a byte
at a time is reassembled, and a failed setup and NULL buffers are
refused.
- regress: the mock agents answer as a stream; ECDSA user auth for each
curve at, under, and over the largest blob, and a wrong curve name
refused; a failed setup refused with nothing sent.
Issue: F-13325, F-14629, F-14631
The client refuses the server-only INFO_RESPONSE at the filter and
answers an INFO_REQUEST only while a keyboard-interactive request of
its own is outstanding, from the request going out until the server's
SUCCESS or FAILURE. The server's second-round setup callback gets a
cleared keyboard struct rather than the freed response arrays.
- IsMessageAllowedClient() refuses MSGID_USERAUTH_INFO_RESPONSE in
every state; DoUserAuthInfoResponse() refuses a client session too
- kbRequestSent: set by SendUserAuthRequest() once the request is
delivered, cleared by the server's FAILURE or SUCCESS;
DoUserAuthInfoRequest() disconnects without it or on a server
- DoUserAuthInfoResponse() zeroes the keyboard struct after freeing the
response arrays; ssh.h says so for the setup callback
- regress: INFO_RESPONSE refused by filter and handler, an unsolicited
INFO_REQUEST, exchanges closed by FAILURE and by SUCCESS, a blocked or
highwater-failed send, and the server's second-round setup
Issue: F-7619, F-7629, F-13399
The default SCP receive callback keeps its destination per session, the
base path plus each directory entered, in place of the process working
directory that every session of a threaded server shares. With openat()
it holds that directory open, reaches it and everything created below
it with O_NOFOLLOW, and confirms a reopened parent is the one entered.
- port.h gains WOPENAT, WMKDIRAT, WOLFSSH_O_SEARCH with a search check
as chdir() made, and WOLFSSH_HAVE_DIRFD, which sizes WOLFSSH so it
keys only on configure's openat() probe; WOLFSSH_NO_DIRFD opts out
- ports without them open by the full path after a wIsDirNoFollow()
screen of the base and of each directory entered; a refused base
frees the receive path
- the receive path is capped at DEFAULT_SCP_FILE_NAME_SZ like the
send side's; futimes() stands in for futimens() on hosts with
openat() but not futimens(), such as macOS before 10.13
- unit: ScpRecvCallback_SessionPath walks down and back with the
working directory untouched, then has the tree moved out from under
it; a linked base and a file-named directory are refused
- scp.test: a planted symlink at the destination name keeps its target;
scp-test.yml also builds and tests the fallback
Issue: F-14758, F-13998
test_DoReceive_AeadTagFailure() marks decryptCipher initialized and
sets its cipher type as soon as wc_AesInit() succeeds, so a later
wc_AesGcmSetKey() failure still leaves wolfSSH_free() a context that
CipherClear() will free. Matches what CipherSetKey() does in the
library.
- give the key-set failure its own result code, so the split reports
which of the two calls failed
test_wolfSSH_CTX_UsePrivateKey_buffer_pkcs8_pem() loads PKCS#8 PEM host
keys for ECDSA P-256, RSA and ML-DSA-65, plus an undecodable body that
must fail in the PEM decoder rather than be refused outright. The keys
are embedded, so the assertions do not depend on load_file(), the
working directory, or a filesystem.
- widen both PEM guards, gated on WOLFSSH_CERTS only because load_file()
is, so neither ran in the build 722678df fixed
- spell the client-only macro NO_WOLFSSH_SERVER, not WOLFSSH_NO_SERVER,
which is defined nowhere
- guard serverKeyEccCurveId, which the now-live guard leaves unused in a
client-only build
- wolfSSH_SFTP_connect() drives wolfSSH_worker() while isKeying at
SFTP_BEGIN, returning any status other than WS_REKEYING,
WS_CHAN_RXD, WS_EXTDATA or WS_EOF.
- TestSftpConnectPreFlushStartsRekey() retries with
wolfSSH_SFTP_connect() alone on the client side, sends stderr from
the server mid-exchange, and checks the server reads the INIT once.
- SendChannelData() and SendChannelExtendedData() flush earlier
output whenever wolfSSH_OutputPending(), return WS_REKEYING when
that flush starts a key exchange, and map WS_WANT_WRITE to dataSz
only for their own framed packet.
- WOLFSSH_BUFFER loses plainSz and its clears.
- ssh.h puts wolfSSH_extended_data_send() beside
wolfSSH_stream_send() under one note on their return, and the
wolfSSH_stream_send_eof() note names no other call.
- regress adds three pre-flush tests on the keyed duplex pair, with
DuplexSendThenBlock(), renames TestFailedSendClearsPendingPlaintext()
to TestFailedSendDiscardsQueuedPacket(), and lets
IsPumpRetryable() accept WS_EXTDATA.
GetNameListRaw() stops at the first empty element and drops the rest, as
OpenSSH's match_list() does. Record that at the function, along with the
two things that keep it safe for the library's own lists: the canned
defaults carry only the trailing comma AlgoListSz() strips, and a
caller's list is screened by CheckAlgoList() in the ssh.c setters.
- leave a pointer in regress.c rather than a second copy of the contract
GetSize() fails with WS_BUFFER_E unless the size it reads fits in the
remaining buffer, so begin never exceeds len on its success path. Drop
the follow-up length test, note that at the call site, and record the
postcondition at GetSize() so the bound is documented where it is
enforced, not only where it is relied on.
- cover the invariant the note now carries: a dataSz that runs past the
buffer, and one that wraps begin + dataSz back inside it, must both
come back WS_BUFFER_E with the parse index left where it started
- drop GetSkip()'s matching recheck of the same bound
- note at GetUint32() the two properties the GetSize() bound rests on
Issue: F-247
Every supported KEX method opens with a client message, so only the
client can send a guessed packet (RFC 4253 section 7.1). DoKexInit()
sets ignoreNextKexMsg only on the server and logs a server's flag on
the client; DoKexDhReply() and DoKexDhGexGroup() no longer skip.
- tests: client KEXINIT with a wrong-guess follows flag does not skip
- tests: client KEXDH_REPLY and GEX_GROUP parse even with the flag set
- tests: the skip case helper drives only the server
Issue: F-10585
GetBoolean() stores any nonzero wire byte as 1, per RFC 4251 section 5,
so callers and callbacks such as hasSignature see only 0 or 1.
- tests: add test_GetBoolean covering 0, 1 and other nonzero bytes
Issue: F-12573