Commit Graph

3072 Commits (dc03049a13738d8cf8de55d1a30e5e462e9a3166)

Author SHA1 Message Date
Mark Atwood 9731cfe250 test: assert SBOM wolfSSL version comes from WOLFSSL_DIR
The dependency assertion checked only that wolfssl was present with a
DEPENDS_ON edge, never that the recorded version matched WOLFSSL_DIR, so
the SBOM could attest a version wolfSSH was not built against and still
pass. Compare versionInfo against the tree.

That assertion alone would not have caught it: the runner installs
wolfssl from the same tree it passes as WOLFSSL_DIR, so pkg-config and
wolfssl/version.h always agree and a wrong source is indistinguishable
from a right one. Add a step that forces them apart -- a stub pkg-config
reports a sentinel version, and a distclean'd copy of the tree has no
generated version.h, so the configure.ac fallback is the only route to a
correct answer. Reverting the recipe fix makes this step fail with the
sentinel, and the fallback path now has CI coverage it lacked.
2026-08-27 09:43:33 -07:00
Mark Atwood 48fc50c9bc fix: read wolfSSL version from WOLFSSL_DIR only
The wolfSSL dependency version came solely from
WOLFSSL_DIR/wolfssl/version.h. That header is generated by configure as
well as tracked, so `make distclean` in the wolfSSL tree removes it. With
it gone the --dep-version override was never passed and gen-sbom fell
back to `pkg-config --modversion wolfssl`, which reports the *installed*
wolfSSL. On a host whose installed build differs from WOLFSSL_DIR the
SBOM recorded that unrelated version (9.9.9 in the report) and still
exited 0, attesting a component wolfSSH was not built against.

Fall back to AC_INIT in WOLFSSL_DIR/configure.ac, which survives
distclean, and fail when neither source is readable rather than letting
an installed copy answer for the tree. SBOM_WOLFSSL_VERSION still
overrides both.
2026-08-27 09:43:33 -07:00
Mark Atwood f8f580b878 fix: fail SBOM CI when gen-sbom missing on master
The gate skipped SBOM generation and exited 0 whenever wolfssl's
scripts/gen-sbom was absent, so the job could report success having
never exercised `make sbom`. That was deliberate while the script was
unmerged, but it landed on master in wolfSSL/wolfssl#10343.

On master, a missing gen-sbom or one too old for --dep-wolfssl is a
regression, so fail instead of skipping: a green run now always means
the target was exercised and the wolfssl-dependency assertions ran.
Only a deliberately pinned older wolfssl_ref still degrades to a skip.

The ref is passed through env rather than interpolated into the shell
script, to keep the workflow expression out of the command line.
2026-08-27 09:43:33 -07:00
Mark Atwood d52677082f fix: unpin SBOM CI from merged wolfssl PR ref
wolfSSL/wolfssl#10343 merged on 2026-07-27, but the SBOM workflow still
checked out wolfssl at refs/pull/10343/head. GitHub keeps PR refs alive
after merge, so CI stayed green while validating a pre-merge snapshot
rather than master. Point wolfssl_ref and its fallback at master.

Distinguish the two gen-sbom lookup failures. A set-but-wrong
WOLFSSL_DIR previously printed the "re-run with WOLFSSL_DIR=..." advice
to someone who had already set it; it now names the path that was
probed and states that WOLFSSL_DIR is a source tree, not an install
prefix.

Refresh the README and in-recipe notes that still told users to track
the #10343 branch until it merged.
2026-08-27 09:43:33 -07:00
Sameeh Jubran 4e322f0e71 sbom: re-vendor unified canonical scripts/sbom.am
Sync scripts/sbom.am with the unified canonical copy in wolfSSL
(scripts/sbom.am), which adds the SBOM_OPTIONS_H override so one
fragment supports both options.h- and config.h/AM_CFLAGS-based macro
capture. wolfssh uses the default config.h path and does not set
SBOM_OPTIONS_H, so behavior is unchanged. Keeps the vendored fragment
byte-identical to the canonical.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran b89b940add sbom: sync scripts/sbom.am with canonical copy and widen CI filter
Re-sync scripts/sbom.am with the canonical wolfSSL copy ($(docdir)
sbomdir, [[:space:]] version parse, GNU-make and uninstall-sbom docs)
and widen the SBOM workflow pull_request filter to '**'.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 2f3abf2ec4 scripts/sbom.am: capture AM_CFLAGS/CFLAGS and configurable config.h
Sync the vendored fragment with the canonical wolfSSL copy: add
$(AM_CFLAGS) $(CFLAGS) to the -dM -E build-options capture and make the
config header path overridable via SBOM_CONFIG_H (default config.h) so
the SBOM records the configured feature macros.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 1f57f67db7 sbom: default license to GPL-3.0-or-later and pin CI to #10343 ref
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran f0e4b404d5 sbom: harden gen-sbom path resolution and align docs
Guard the SBOM_GEN wolfSSL fallback with $(if $(WOLFSSL_DIR),...) so an
unset WOLFSSL_DIR leaves it empty (yielding the intended "set WOLFSSL_DIR"
error) instead of resolving to an absolute /scripts/gen-sbom that could run
an unrelated host script. Reword the fragment header to describe the actual
gen-sbom resolution (vendored copy if present, else WOLFSSL_DIR) rather than
claiming offline tarball builds that aren't wired up yet. Condition the
README's wolfSSL-dependency statement on a sufficiently new gen-sbom to
match the existing degradation note.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 5323b26b0a sbom: extract shared scripts/sbom.am recipe and add CI
Move the make sbom / install-sbom / uninstall-sbom recipe out of
Makefile.am into a reusable scripts/sbom.am fragment; Makefile.am now
just declares the product facts (name, LICENSING, wolfssl dependency,
GPL-3.0-only default) and includes it.

Add a GitHub Actions workflow covering both SBOM paths:
- autotools: build wolfSSL + wolfSSH, run make sbom, assert SPDX
  validity, CycloneDX identity, reproducibility, and the wolfssl
  dependency edge.
- embedded: run gen-sbom against user_settings.h (via pcpp) + the
  wolfSSH source set, asserting the same identity/validity/reproducibility
  plus that user_settings.h options and the wolfssl dependency are
  captured.
Both gracefully skip on a gen-sbom that predates the wolfSSL SBOM
change.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 7ff47dc54c sbom: fix license detection and clarify wolfssl dependency note
Default SBOM_LICENSE_OVERRIDE to GPL-3.0-only so the SBOM records the
correct license instead of NOASSERTION, and point the --dep-wolfssl
note at wolfSSL/wolfssl#10343.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran c34fdf155b fix: make SBOM reflect real config and cover all build types
- Include AM_CPPFLAGS and config.h in the options snapshot so the
  configured feature set (WOLFSSH_SCP/SFTP/NO_WOLFSSH_SERVER/...)
  appears in the SBOM instead of being empty.
- Discover static (.a), Mach-O (.dylib) and Windows (.dll/.lib)
  artifacts, not just a versioned .so.
- Set SOURCE_DATE_EPOCH from the last git commit for reproducible output.
- Clean staging tree and temp defines file via trap even on failure.
- Record wolfssl as an SBOM dependency (--dep-wolfssl), version
  auto-detected from WOLFSSL_DIR/wolfssl/version.h.
- configure.ac: detect git; drop unused version-info split + comment.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Mark Atwood b79c3e8e89 docs: add SBOM/EU CRA Compliance section to README and build docs 2026-08-27 09:43:33 -07:00
Mark Atwood 527df4ffd6 feat: add make sbom target
Adds sbom, install-sbom, and uninstall-sbom targets.
Runs gen-sbom to produce CDX and SPDX outputs.
Requires WOLFSSL_DIR pointing to a wolfssl tree with
the feat/sbom-embedded branch (includes gen-sbom).
2026-08-27 09:43:33 -07:00
Yosuke Shimizu 78c992a5a1 ssh.c: report the byte count from a channel read that defers its credit
- wolfSSH_stream_read() advances inputBuffer->idx before crediting the
  window, and it and _ChannelRead() return the bytes copied with a
  non-success adjust left in ssh->error.
- _ChannelRead() takes the WOLFSSH from channel->ssh, rejects an idx
  past inputBuffer->length, restores the entry ssh->error on a clean
  credit, and retires a stale WS_WANT_WRITE only when its own credit
  went out.
- wolfSSH_SFTP_Close() checks NoticeError() only on a failed send.
- The src/ssh.c block comments, and new wolfssh/ssh.h notes above
  wolfSSH_stream_read(), wolfSSH_ChannelRead() and
  wolfSSH_ChannelIdRead(), state the window-adjust and ssh->error
  contract.
- tests/unit.c adds test_stream_read_deferredWindowAdjust() and
  test_ChannelIdRead_deferredWindowAdjust(): an adjust that defers,
  then fails, then succeeds, plus a read with nothing buffered against
  a seeded WS_WANT_WRITE.
2026-08-27 09:01:27 -07:00
Yosuke Shimizu 83762b7ba6 wolfsftp: buffer the SFTP DATA length across partial reads
- STATE_SEND_READ_FTP_DATA trims state->buffer to UINT32_SZ with
  wolfSSH_SFTP_buffer_set_size(), reads the data string length into
  it with wolfSSH_SFTP_buffer_read(), and decodes it with
  wolfSSH_SFTP_buffer_rewind() and wolfSSH_SFTP_buffer_ato32().
- The outSz bound is applied to the decoded length before
  wolfSSH_SFTP_buffer_create() allocates for it, and that call's
  return is checked.
- ssh->error is set to WS_BUFFER_E when the size or decode helper
  fails, to WS_RECV_OVERFLOW_E when the decoded length exceeds
  outSz, and to WS_MEMORY_E when the allocation fails.
- The szFlat stack array is removed from
  wolfSSH_SFTP_SendReadPacket().
- tests/unit.c gains test_SftpSendReadPacketSplit() and
  test_SftpSendReadPacketOverflow(), both registered in
  wolfSSH_UnitTest(), with the SftpBuildData() and
  SftpClientDriveReadSplit() helpers. The first drives a DATA reply
  split at each of the four points across the length prefix; the
  second drives one whose string length exceeds the caller's buffer
  and checks for WS_RECV_OVERFLOW_E with no retained send read
  state.

Issue: F-8828
2026-08-26 20:44:18 -07:00
Yosuke Shimizu 40f9400f94 internal: send a disconnect when key exchange fails
- DoKexDhInit() sends SSH_MSG_DISCONNECT with KEY_EXCHANGE_FAILED on
  WS_CRYPTO_FAILED and WS_PUBKEY_REJECTED_E, DoKexDhGexGroup() on
  WS_CRYPTO_FAILED and WS_DH_SIZE_E.
- DoKexDhReply() sends KEY_EXCHANGE_FAILED on WS_CRYPTO_FAILED and
  HOST_KEY_NOT_VERIFIABLE on WS_PUBKEY_REJECTED_E.
- DuplexEndpoint records the reason code of a plaintext outbound
  disconnect, and InitKexReplyHarnessKex() takes an explicit KEX
  algorithm.
- New mutator modes shorten f and e, write a zero-length e, cut the
  GEX prime below the requested floor and set the GEX generator to 1;
  LocateSinglePacketPayload() finds the payload for all three
  single-packet rewriters.
- The harness KEX algorithm falls back to curve25519-sha256, then
  ecdh-sha2-nistp256, when no plain diffie-hellman-group is built.
- Tests assert the reason code on the wire for each new mode and for
  host key rejection, and assert no disconnect on a successful
  handshake.

Issue: F-8838
2026-08-26 20:43:23 -07:00
John Safranek 78633a1a23 Clear disconnectTxd once the buffer drains
The flag was set in SendDisconnect() and never cleared, so it meant "a
disconnect was sent" rather than "a flush is owed". Once ours had gone
out, the next teardown call still pushed whatever the internal senders
had queued behind it: measured, a CHANNEL_EOF from DoChannelEof() went
on the wire after the disconnect. wolfSSH_SendPacket() now clears it.

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek d05978f00d Report the disconnect with no channel to drop
wolfSSH_shutdown() set ssh->error to WS_DISCONNECT only inside the
channel branch, so a flush that emptied the buffer with the channel
already retired left behind the WS_WANT_WRITE that queued it. echoserver
and sftpclient read that error and burn ten wolfSSH_worker() calls on a
write that is already done. TestShutdownFlushesWithNoChannel asserts it.

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek a7fda8f6a3 Say what the disconnect contract covers
The contract in ssh.h claimed more than the code does. A CHANNEL_EOF
already received outranks the drain, so both stream reads report WS_EOF
with data still buffered, and wolfSSH_accept()/wolfSSH_connect() never
look at the flag at all. Both fixes belong with the channel EOF work in
#1195; until then the header says what is really gated.

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek 173a85b1eb Disconnect outranks a stuck rekey
wolfSSH_stream_peek() and wolfSSH_stream_read() test isKeying before
disconnected, so a peer that rekeys then disconnects wedges both: only
NEWKEYS clears isKeying and none is coming. Callers spin on WS_REKEYING
and never get the buffered data. Both gates and read's copy step now
defer to disconnected. Covered by TestDisconnectOutranksRekey.

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek 18b9249ee7 Make the disconnect tests prove what they claim
The two disconnect tests ran on a session that had never finished user
auth, so IsMessageAllowed() blocked the sends on its own and the "nothing
on the wire" assertions held even with the gates removed. Both now sit
past user auth. With only the shutdown gate reverted the test measures 72
bytes out and both teardown flags set, where before it measured nothing.

- wolfSSH_stream_peek() reports WS_DISCONNECT when the channel is gone,
  the way wolfSSH_stream_read() already did; a missing channel used to
  read as a bad argument on a session that had simply ended
- the drain test covers the no-channel case for both calls

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek 5805256b4c Close the last three post-disconnect sends
The disconnect gate left three ways for traffic to reach a peer that had
already ended the session, and it made the default highwater callback
report a failure for a packet that had gone out fine.

- wolfSSH_shutdown() drops the channel when ssh->disconnected is set, so
  the EOF, exit status and close are skipped along with the wait for a
  close the peer will never send
- wsHighwater() skips the rekey request on a disconnected session, so a
  firing high water mark no longer turns SendDisconnect() and
  SendChannelEof() into failures
- wolfSSH_ChangeTerminalSize() gained the SendAfterDisconnect() gate,
  making the ssh.h contract true for every send declared below it
- regress covers all three, including that shutdown leaves eofTxd and
  closeTxd clear and puts nothing on the wire

A disconnect of our own left queued by a short send still reaches the peer.
SendDisconnect() records disconnectTxd once the packet is bundled, and one
FlushQueuedDisconnect() helper gates the retry in wolfSSH_SendDisconnect()
and wolfSSH_shutdown() on that. Keying it on disconnected alone would push
whatever was queued, since the peer's disconnect sets that flag too and
leaves only unrelated traffic behind.

wolfSSH_shutdown() flushes ahead of the channel-list test, so the peer's
close retiring the last channel does not strand the disconnect, and an
unfinished flush outranks WS_CHANNEL_CLOSED. Its WS_WANT_WRITE stays in
ssh->error as well, since callers gate their shutdown retry on that.

The highwater guard sits in HighwaterCheck(), not in the default callback:
the return that fails the send comes from whatever callback the application
installed, and it propagates out through wolfSSH_SendPacket().

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek ef43586026 Gate the last three senders on a disconnect
The ssh.h comment promised that every send call below it reports
WS_DISCONNECT, but three did not: wolfSSH_TriggerKeyExchange(),
wolfSSH_SendIgnore() and wolfSSH_SendDisconnect().

- All three now take the SendAfterDisconnect() gate, so the sentence in
  ssh.h describes the code rather than the intent.
- TriggerKeyExchange() is the highwater callback's rekey trigger, so this
  also stops a rekey starting on a session the peer has ended.
- SendIgnore() and SendDisconnect() gained the NULL check the gate needs;
  both already reported WS_BAD_ARGUMENT for that from the callee.
- A second disconnect is refused: one ends the session.
- regress.c: the three calls join the send sweep.

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek bac3c8ba37 Report a disconnect from stream_peek
wolfSSH_stream_peek() is how the shell loops decide whether a channel is
drained. It had no disconnect check, so a dead session looked exactly like
a drained one: zero bytes available, nothing to tell them apart.

- Report WS_DISCONNECT once the buffered data runs dry, the same shape
  wolfSSH_stream_read() uses. What is still buffered comes back first.
- ssh.h and internal.h name peek alongside the read call, and no longer
  claim the read side is ungated outright.
- regress.c: peek sees the buffered byte, then sees the disconnect.

Raised from the channel-eof branch, where peek becomes the drain gate for
the wolfsshd and echoserver shell loops.

Issue: F-8837
2026-08-26 20:15:01 -05:00
John Safranek 285e0409cf Block every send after a disconnect
The disconnect flag gated wolfSSH_stream_read() and wolfSSH_stream_send(),
which is the client-side API. wolfsshd and echoserver drive their channels
through the channel-id calls, so the daemon was never gated at all.

- New SendAfterDisconnect() helper, used by the six send entry points:
  stream_send, stream_exit, ChannelIdSend, ChannelIdSendExt,
  extended_data_send and global_request.
- Reads stay open, since data that arrived before the disconnect is still
  the caller's. wolfSSH_stream_read() drains its buffer and reports
  WS_DISCONNECT only once it runs dry.
- wolfSSH_worker() stays ungated; the shutdown paths still pump it.
- ssh.h and internal.h describe the split.
- regress.c: buffered data survives the disconnect, and every send call
  refuses without a byte leaving the session.

Issue: F-8837

Every public send call means every one: the channel-pointer sends
(wolfSSH_ChannelSend, wolfSSH_ChannelSendExt, wolfSSH_ChannelExit), the
forwarding requests and both wolfSSH_ChannelFwdNew* opens carry the gate
too, and none of them had a message-filter backstop.

ChannelCreditWindow() parks its credit rather than sending. The reads that
drain what arrived before the disconnect credit the window for the bytes
taken, and that credit went straight to the transport: each drain put a
CHANNEL_WINDOW_ADJUST on the wire after the session was over, and a failing
send replaced the byte count already copied for the caller.
2026-08-26 20:15:01 -05:00
John Safranek e6b324d14f Make a disconnect end the session
SSH_MSG_DISCONNECT left nothing behind but ssh->error, which
wolfSSH_stream_read() clears on entry. An application looping on the
stream calls lost the code and went back to a connection already over.

- Add WOLFSSH.disconnected, set by DoDisconnect() and SendDisconnect().
- DoDisconnect() sets it before decoding the payload, so a malformed
  message still ends the session. RFC 4253 section 11.1.
- wolfSSH_stream_read() and wolfSSH_stream_send() report WS_DISCONNECT
  from the flag instead of reaching for the transport again.
- Both guards run ahead of the channelList NULL test, so a torn-down
  session reports the disconnect rather than WS_BAD_ARGUMENT.
- ssh.h states that undrained channel data goes with the session;
  internal.h states which calls the flag gates and which it does not.
- regress.c: the receive side, the send side, and both of those again on
  a session with an open channel.

Issue: F-8837

The test channel credits the peer's window too. Left at 0, SendChannelData()
bails with WS_WINDOW_FULL before the wire, and the "nothing went out" checks
would hold with the gate removed.
2026-08-26 20:15:01 -05:00
John Safranek 5303681b91 Fix the shutdown channel teardown
wolfSSH_shutdown() searched for the session channel by the peer's channel
ID while telling ChannelFind() to match the local ID field. Each side
numbers its channels independently, so the search usually found nothing.

- The session channel is the head of the list; take it directly instead
  of searching for what is already in hand.
- Restores the EOF, exit-status and close sends, and the drain that waits
  on the peer's close, all skipped on the NULL result.
- Only bit when the two IDs differ, so the single-channel tests, where
  both sides pick 0, never saw it.
- unit.c: shut down a channel whose peer ID is not its local ID, then
  check that EOF and close went out.

Issue: F-8817
2026-08-26 20:15:01 -05:00
John Safranek 5336ee5aca Handle a select() error in the client's peer reader
bytes held select()'s return in a word32, so a -1 became 0xFFFFFFFF and
ran the read path on descriptor sets select() had left alone. The
SIGWINCH handler interrupts this select, so a terminal resize reaches it.

- Keep the result in an int
- Retry on EINTR, report anything else
- Same fix readPeer() in examples/client/client.c already carries
2026-08-26 16:53:23 -05:00
John Safranek 7d43f9e189 Bound the client's queued send flush and let a rekey through
FlushQueuedSend() retried wolfSSH_worker() for as long as it reported
WS_WANT_WRITE. A peer that stops reading never lets the socket drain, so
the sending thread spun there, and at the shutdown drain that thread was
main, leaving the client unable to exit.

- Give the retry a ten second deadline
- Return the still pending WS_WANT_WRITE to the caller
- Take that for done at the shutdown drain, the socket closes next
- Mask WS_REKEYING, the worker only reports it once the send is out,
  and readInput() was taking it for a send failure
2026-08-26 16:53:23 -05:00
John Safranek 62c0e2efa5 Don't close the client's log file while a thread can still write it
On the MSVC path the input thread is never waited on, it blocks in a
console read with nothing to cancel it, so it can still be logging when
main closes the file named by -E.

- Flush the log there and let process exit close the stream
- The POSIX path joins its threads first, it still closes the file
2026-08-26 16:53:23 -05:00
John Safranek 50ce6135f7 Quote the client test's paths and use printf for escapes
The paths are built from pwd, so an unquoted use split on a build
directory with a space in it, and the cleanup's rm -rf then deleted
whatever the first word named.

- Quote work_dir and every path derived from it
- Pass the directory to rm after --
- Replace the two echo -e calls, dash prints a literal -e
2026-08-26 16:53:23 -05:00
John Safranek abf8c206a6 Skip the client app test in a forced non-blocking build
The echoserver needs -N under WOLFSSH_TEST_BLOCK, and even with it leaves
a failed write queued while it waits on the peer, so a session stalls.
scp.test and get-put.test skip the build too.
2026-08-26 16:53:23 -05:00
John Safranek f02a107cb4 Flush a queued client write instead of waiting on the peer
A send the socket wasn't ready for stays queued but still reports the data
as taken, so the client waited on a reply to a message it never sent.
Flush after a queued send, a terminal size change, and at shutdown.

The shutdown drain reports its want read as WS_FATAL_ERROR, so read the
status with wolfSSH_get_error(); an ordinary shutdown was exiting 1. Time
out readPeer()'s select() so a flush can't strand the reader.
2026-08-26 16:53:23 -05:00
John Safranek b067dace1a Don't build the client app against a single threaded wolfSSL
The client runs every session's I/O on threads, so it needs a threaded
wolfSSL. configure probes for SINGLE_THREADED when the client app is
enabled. Asking for the app with --enable-sshclient is an error, getting
it from --enable-all drops the app instead, so --enable-all still
configures against a single threaded wolfSSL.

The compile time check stays for the builds that never run configure.
That leaves the SINGLE_THREADED terms in the app's own guards
unreachable, so drop them.
2026-08-26 16:53:23 -05:00
John Safranek e6e320e94e Keep a failed shutdown send out of the client's exit status
wolfSSH_shutdown() returns WS_WANT_WRITE when the channel EOF, exit and
close messages are still queued on the non-blocking socket. Masking that
to WS_SUCCESS reported a clean exit for a session whose close messages
never reached the peer.

Mask a want write from the drain worker only, where the close messages
are already sent. The want read masking stays on both, wolfSSH_shutdown()
runs a worker of its own and passes that want read back.
2026-08-26 16:53:23 -05:00
John Safranek 23899eda85 Build and test the wolfssh client app in CI
- --enable-sshclient defaults to no, so the app was built only by the
  configs that use --enable-all, and never under the multi-compiler
  warning flags. Add it to the multi-compiler matrix.
- Add scripts/sshclient.test, run by make check. It covers the client's
  sessions and the -E log file against the echoserver.
- The script is not gated on BUILD_SSHCLIENT. It exits 77 when the
  client app or the echoserver isn't there, so every build runs it and
  the ones without the app report it as a skip.
- Check the client and the echoserver by asking each for its usage
  message, not by looking for the file. Both are libtool wrapper
  scripts in the build tree, and a wrapper outlives a reconfigure that
  drops the program it wraps, then runs only far enough to say so.
- The echoserver runs in echo mode and the client's stdin comes from a
  fifo written a piece at a time, so the session carries data and ends
  on its own. Each client run has a watchdog.
- Rename sshd-test.yml's job to cover both apps. That workflow builds
  the client app along with wolfsshd.
- Check that the command reaches the server, now that the client sends
  it rather than discarding it.
- Make the SINGLE_THREADED guard a preprocessor #error. The runtime
  err_sys() only caught the misconfiguration in an autotools build that
  got as far as running; the #error catches it at compile time for the
  IDE and plain Makefile builds too.
- Treat WS_WANT_READ and WS_WANT_WRITE out of wolfSSH_worker() as a
  clean shutdown. The socket is non-blocking, so the peer having
  nothing ready is not a session failure.
2026-08-26 16:53:23 -05:00
John Safranek 55f73576ea Wire up the wolfssh client's -E log file option
- -E was parsed into config.logFile and printed by -G, never read.
- Install a logging callback that writes to the named file, following
  what wolfsshd does for its own -E.
- Turn logging on with wolfSSH_Debugging_ON(). Installing the callback
  is not enough on its own, the file came out empty in any build that
  wasn't --enable-debug, including the --enable-all builds where the
  library has all of its logging compiled in. wolfsshd turns logging on
  the same way.
- Match DefaultLoggingCb()'s format, timestamp and level tag, so a log
  written to the file and one written to stderr are comparable. That
  function's GetLogStr() is private to the library, so the level names
  are repeated in the app.
- Parse the command line and open the file in main(), before
  wolfSSH_Init(), so the start up messages land in the file.
- Close the file after wolfSSH_Cleanup(). The callback cannot be
  uninstalled, so it ran with a closed stream and segfaulted on exit.
  It falls back to stderr.
- Name the stream logFileStream, apart from struct config's logFile,
  which is the path it was opened from.
- Drop the always true condition around the session threads.
2026-08-26 16:53:23 -05:00
John Safranek ffe7b1cfee Check the channel packet overhead literal against its expression
CHANNEL_PACKET_OVERHEAD_MAX is a hand-computed copy of
CHANNEL_PACKET_OVERHEAD_SZ, needed because the expression bottoms out in
wolfCrypt enum constants that #if reads as zero. Nothing tied the two
together, so a term added to the expression would leave the #error
guarding DEFAULT_MAX_PACKET_SZ silently ineffective.

Assert the bound in internal.c, where both are ordinary constant
expressions, with a negative-array-size typedef.
2026-08-26 12:11:15 -05:00
John Safranek 245baf01f0 Document the wolfSSH_SetChannelType contract
The refusals added for names the peer cannot use changed the return
contract of a public API whose block comment still promised only
WS_SUCCESS. There is no dox_comments entry, so that comment is all an
embedder has.

- Spell out each WS_BAD_ARGUMENT case, the keep-the-stored-name rule,
  and that a refused call leaves the selected type alone.
- api.c asserts connectChannelId across the refusals. It is the field
  SendChannelRequest() switches on, so moving the checks back below the
  assignment would otherwise pass.
2026-08-26 12:11:15 -05:00
John Safranek e6bce999c6 Reject a window-change on a channel with no pty
A window-change arriving before any pty-req had nothing to resize, but
the size was stored and the resize callback run anyway. Dropbear refuses
the same request for the same reason.

- Reject it with the existing rej path, so no reply is sent for a
  request RFC 4254 sec 6.7 says takes none, and the session continues.
- unit.c covers the rejection, and now drives a real pty-req, which
  had no coverage on the receive side at all.
2026-08-26 12:11:15 -05:00
John Safranek f956512c5a Clamp terminal dimensions to what a winsize can hold
The four pty-req and window-change dimensions were decoded straight into
the WOLFSSH fields and handed to the resize callback unchecked. The
consumers copy them into the unsigned short fields of a struct winsize
for TIOCSWINSZ, so anything above 65535 wraps, and 0x10000 arrives as a
0x0 terminal.

- Add SetTerminalSize() and route both the pty-req and window-change
  branches through it, so pty-req stops decoding straight into the
  WOLFSSH fields.
- Clamp all four to TERMINAL_DIMENSION_MAX. Others truncate at the
  ioctl and accept it, but wolfSSH hands the word32 values to
  termResizeCb first, so an unclamped dimension escapes the library
  rather than being cut down on the way to the ioctl.
- Take a zero dimension as sent. Others do too, and a zero is how a
  peer reports a dimension it has no information about.
- unit.c drives all four dimensions from one table, covering the zero,
  single-zero and wrapping cases, in an error code range no other case
  in the function claims.

F-8833 recommended ignoring a zero dimension. That is declined above:
Others take zeros as sent, and a zero is how a peer reports a dimension
it has no information about. The finding's symptom, a 0x0 terminal, is
also reached by a route it did not identify, a dimension above 65535
wrapping, and the clamp closes that one.

Issue: F-8833
2026-08-26 12:11:15 -05:00
John Safranek fd91af61c3 Frame DoPacket from the validated packet length
DoPacket stepped to the next packet using payloadIdx, which handlers
set to however much they read. The default case reads none of an
unimplemented message's payload, leaving the cursor short by that much.

- Advance inputBuffer.idx by UINT32_SZ + curSz from the packet start,
  the length DoReceive already bounds-checked, so a handler that
  ignores trailing bytes cannot move the next packet's start.
- Snapshot curSz on entry beside the packet start, so the frame is
  computed entirely from entry-time state. Reading it back after the
  handler switch would describe the next packet if a handler ever
  re-entered the receive path.
- Covers DoIgnore, DoDebug, DoUnimplemented, DoChannelSuccess and
  DoChannelFailure, which are all short on a padded payload.
- Clamp to the buffer length on the WS_BUFFER_E path.
- unit.c pins the cursor across an unimplemented message, driving
  DoPacket through a new wolfSSH_TestDoPacket() hook. The ShrinkBuffer()
  noted below zeroes the cursor, so DoReceive() cannot be in the path.

Note the short cursor is not currently observable: DoReceive calls
ShrinkBuffer() with forcedFree, which drops the rest of the buffer
after every packet. This is hardening, not a live desync.

Issue: F-8825
2026-08-26 12:11:15 -05:00
John Safranek 4d9f6a9721 Report a channel name the peer cannot use
wolfSSH_SetChannelType() discarded an exec or subsystem name it could
not use and still returned WS_SUCCESS. SendChannelRequest() then omits
the name field entirely, which the peer rejects as malformed, dropping
the connection. Both an oversized name and an empty one reach it; the
empty case is reachable from the command line as "wolfssh -c ''".

- Return WS_BAD_ARGUMENT for a name at or above WOLFSSH_MAX_CHN_NAMESZ,
  matching how the function already reports a bad type or side.
- Return WS_BAD_ARGUMENT when no name is given and none was stored by
  an earlier call, and when a size arrives with no name behind it.
- Keep returning WS_SUCCESS when an earlier call stored a name, which
  is what the SFTP and SCP retry loops depend on.
- Return before setting connectChannelId so a rejected call leaves
  no state behind, as the server-side exec rejection does.
- Keep the stored name intact when a later call is refused.
- api.c asserts each refusal, and the largest name still admitted.
2026-08-26 12:11:15 -05:00
John Safranek 050dee0fbf Bound channel maxPacketSz below the wire limit
MAX_PACKET_SZ caps the whole SSH binary packet, but the channel
maxPacketSz it was compared against counts only channel payload. A
peer honoring the advertised 35000 overruns the receiver's own check.

- Derive MAX_CHANNEL_PACKET_SZ in internal.h: MAX_PACKET_SZ less the
  transport framing, the CHANNEL_EXTENDED_DATA header, the worst-case
  padding BundlePacket() picks, and MAX_HMAC_SZ. 34899 by default.
- Name that overhead twice, once for the compiler and once as a
  literal for the preprocessor, which reads the wolfCrypt enum
  constants in the first form as zero. The #error guarding
  DEFAULT_MAX_PACKET_SZ uses the second rather than its own copy.
- MAX_CHANNEL_PACKET_SZ is derived rather than a tunable, so it is
  not overridable; an override defeated the bound it enforces.
- wolfSSH_CTX_SetWindowPacketSize() bounds maxPacketSz against that
  instead of MAX_PACKET_SZ; DEFAULT_MAX_PACKET_SZ is unaffected.
- api.c tests the new edge and that MAX_PACKET_SZ is now rejected.

Issue: F-8835
2026-08-26 12:11:15 -05:00
Yosuke Shimizu 8e8b62d358 examples/portfwd: drop the password from the options banner
- The startup banner's password line and its argument are dropped
  from the printf; ssh host, username and the two forward endpoints
  remain.
- userPassword has internal linkage, and portfwd_worker() zeroes it
  with wc_ForceZero() as soon as wolfSSH_connect() returns, on both
  the success and the failure path.
- portfwd.c includes wolfssl/wolfcrypt/memory.h.

Issue: F-11673
2026-08-26 09:12:27 -07:00
Yosuke Shimizu 709352ba8b internal: sign Ed25519 user-auth requests through the agent
- BuildUserAuthRequestEd25519() signs with wolfSSH_AGENT_SignRequest()
  when the agent is enabled, writing the returned signature blob
  length-prefixed into the reserved payload and advancing idx past it.
  The capacity handed to the agent is the room the prepare phase set
  aside: two lengths plus the signature and public key type sizes.
- The buffer the local signing path fills is allocated in that path
  rather than at the top of the function; sig starts NULL and the
  small-stack free at the tail already null-checks it.
- PrepareUserAuthRequestEd25519() notes that the agent holds the
  private key and loads none locally.
- tests/regress.c gains TestAgentEd25519UserAuthEmitsSignature(),
  TestAgentEd25519UserAuthPropagatesAgentError() and
  TestAgentEd25519UserAuthRejectsOversizeSignature(), which drive
  SendUserAuthRequest() over a mock agent and parse the emitted
  USERAUTH_REQUEST down to its signature field.
- InitAgentEd25519Ctx() takes the signature size the mock agent
  answers with, so a caller can hand back a blob past the capacity.
- ParsePayloadLen() and BuildExtInfoSigAlgs() move to the shared test
  helper section so the new tests and the existing callers share them.

Issue: F-11660
2026-08-26 08:50:47 -07:00
Yosuke Shimizu f60b00cbf8 ossh: check the key-type parse before the ID lookup
- GetOpenSshPublicKey() calls NameToId() and enters the key-type
  switch only when GetStringRef() returns WS_SUCCESS, and returns
  that result otherwise.
- publicKeyType starts NULL and keyId starts ID_UNKNOWN.
- tests/api.c adds test_GetOpenSshPublicKey_type(), gated on
  WOLFSSH_TPM and WOLFSSH_TEST_INTERNAL, covering a truncated type
  string, a truncated length prefix, an empty type, an unsupported
  type and a well-formed ssh-rsa blob.
- Each case asserts idx alongside the return code: UINT32_SZ for a
  truncated type string, 0 for a truncated length prefix, and the
  full blob size for the empty type, the unsupported type and the
  ssh-rsa key.

Issue: F-11650
2026-08-26 08:50:23 -07:00
Yosuke Shimizu faca3bc265 wolfsftp: keep the remote file when resuming a put
- wolfSSH_SFTP_Put() adds WOLFSSH_FXF_TRUNC to the destination open
  only when the write offset is zero.
- STATE_PUT_LOOKUP_OFFSET clears a saved offset when the local file
  is no larger than it.
- A new STATE_PUT_STAT_REMOTE stats the destination when the saved
  offset is nonzero, and clears the offset unless the reported size
  matches it exactly, or the stat returns WS_SFTP_STATUS_NOT_OK or
  WS_PERMISSIONS. Other stat failures re-save the offset and move to
  STATE_PUT_CLEANUP; a want-read or want-write keeps the state.
  WS_SFTP_PUT_STATE carries the attributes both states read.
- The Windows server open maps WOLFSSH_FXF_CREAT to OPEN_ALWAYS and
  reserves CREATE_ALWAYS for an open that also asked for
  WOLFSSH_FXF_TRUNC; the disabled TRUNCATE_EXISTING mapping is
  dropped.
- tests/api.c adds test_wolfSSH_SFTP_PutResume(), five cases over the
  resume paths, built where the hosted file wrappers are available.

Issue: F-11659
2026-08-26 08:49:48 -07:00
Yosuke Shimizu 7595a95b37 internal: reject inbound packets that are not block aligned
- DoReceive() validates the peeked packet_length in
  PROCESS_PACKET_LENGTH: UINT32_SZ plus curSz for non-AEAD, curSz
  alone for AEAD, against peerBlockSz floored at MIN_BLOCK_SZ. A
  non-zero remainder sets ssh->error to WS_BUFFER_E and returns
  WS_FATAL_ERROR.
- BuildMacTestPacketPrefix() in unit.c takes padLen from the caller
  and pads to a block-aligned total; test_DoReceive_VerifyMacFailure,
  test_DoReceive_AeadTagFailure, and
  test_DoReceive_RejectsShortPadding follow.
- BuildPacket() in regress.c pads to 16.
- test_DoReceive_RejectsMisalignedPacket,
  test_DoReceive_RejectsMisalignedCtr, and
  test_DoReceive_RejectsMisalignedAead cover the cleartext, AES-CTR,
  and AES-GCM paths.

Issue: F-8834
2026-08-26 08:49:19 -07:00