Commit Graph

528 Commits (master)

Author SHA1 Message Date
John Safranek 96548ffef3 internal: hash the pad byte of K without a branch
The MSB of the shared secret K decides whether its mpint encoding
takes a 0 pad byte. CreateMpint now derives the pad with masks, and
HashMpintHeader hashes the length and the optional pad as one update of
LENGTH_SZ + kPad bytes: no branch on the pad in wolfSSH, and no
zero-length update, which the SE050 and TI hash ports reject.

- apply to DoKexDhReply, SendKexDhReply, and the in-tree GenerateKey
- add wolfSSH_TestCreateMpint and a unit test of the pad, leading-zero,
  empty-input and NULL-argument cases
- add a GenerateKey known-answer vector with the MSB of K set

Issue: F-8808
2026-10-13 12:25:44 -07:00
John Safranek 433903f20a Discard a rejected CBC packet before failing
Under AES-CBC, DoReceive() no longer fails as soon as the decrypted
length or the MAC is rejected. It reads the packet out to MAX_PACKET_SZ
bytes and runs the MAC over the rest first, as OpenSSH does, so neither
the failure point nor the MAC work reveals the length.

- the length checks and the MAC check all go through RejectPacket()
- a PROCESS_DISCARD state resumes the discard after a want-read; a
  transport failure mid-discard reports the rejection
- CTR, AEAD and unencrypted packets still fail immediately
- the discard compiles out under WOLFSSH_NO_AES_CBC
- tests/unit.c covers each rejection with short reads, a peer close,
  and a nonzero start offset

Issue: F-12606, F-8807, F-11668
2026-10-13 12:23:05 -07:00
John Safranek de3ec51a77 regress: cover the hybrid KEX failure disconnect
DoKexDhReply() maps any KeyAgree_client() failure to WS_CRYPTO_FAILED
and sends KEY_EXCHANGE_FAILED. Pin that for the hybrid ML-KEM KEXes,
whose client agreement returns raw wolfCrypt codes.

- truncate f in a hybrid KEXDH_REPLY and check the client's error and
  disconnect reason
2026-10-13 12:12:49 -07:00
John Safranek e0e8e45f84 internal: fix hybrid ML-KEM client secret sizing
KeyAgreeEcdhMlKem_client() writes the classical shared secret after the
ML-KEM secret in ssh->k, so it now offers the classical call only the
capacity left after that prefix, as the server side does. The ML-KEM
private key decode result is now checked before decapsulation.

- reject a ssh->k capacity that cannot hold the ML-KEM secret
- add wolfSSH_TestKeyAgreeEcdhMlKem_client() and a unit test that runs
  each hybrid KEX at, below and above the capacity bound, and with a
  corrupted private key, and checks the derived secret

Issue: F-14225, F-10559
2026-10-13 12:12:49 -07:00
John Safranek 6b7e667e96 tests: report how the RealPath loop ended
The negotiation test's asserts report only the sawPartial flag, so a
failure did not say what the first RealPath call returned. Print the
try count, whether a name came back, and the error before asserting,
when the loop ends without a name or without a partial pass.
2026-10-13 11:50:30 -07:00
John Safranek 9496bbfdb2 Keep negotiated key sizes when derivation fails
GenerateKeys() now wipes only the iv, encKey and macKey bytes when a
derivation fails, so the negotiated sizes stay put and a second call
cannot report success with no keys.

- tests/regress.c checks a failed server KEXDH_REPLY send wipes K
- AssertHandshakeSucceeds() takes a KEX algorithm for the per-KEX check
- KEX wipe failures name the algorithm and the endpoint

Issue: F-8845, F-11680, F-11681, F-8843, F-11682, F-14001
2026-10-13 12:24:57 -05:00
John Safranek 5e096fd074 Wipe the KEX shared secret once keys are derived
DoKexDhReply() and SendKexDhReply() zero all of ssh->k and reset kSz
on every exit, so K does not outlive the key exchange. GenerateKeys()
zeroes both handshake key sets when a derivation fails.

- the full-buffer wipe clears CreateMpint and ML-KEM hybrid leftovers
- tests/regress.c checks K after each KEX, the hybrids included
- tests/regress.c checks a failed derivation leaves no keys

Issue: F-8845, F-11680, F-11681, F-8843, F-11682, F-14001
2026-10-13 12:24:57 -05:00
Yosuke Shimizu 63c8aef551 certman: require keyCertSign on self-issued peer intermediates
- CertManIntermediateIsCA() demotes a CA intermediate whose KeyUsage
  extension lacks keyCertSign whether or not it is self-issued.
- certmanForgeCert() takes the subject name from issuerCert when cn is
  NULL, and with WOLFSSL_CERT_EXT sets the subject and authority key
  ids.
- certmanIsSelfIssued() reports whether a cert's issuer and subject
  name hashes match; tests/unit.c includes asn.h for it.
- certmanCheckIntermediate() gains selfIssued, which gives the
  intermediate the root's subject name and fails with -935 when the
  forged cert is not self-issued, and appendRoot, which also sends the
  trusted root at the end of the chain. A promoted case passes only on
  WS_SUCCESS or WS_CERT_PROFILE_E.
- test_CertMan_PromoteValidCaIntermediate() covers a self-issued
  intermediate with and without keyCertSign, and a chain that ends in
  the trusted root.
2026-10-13 09:43:54 -07:00
John Safranek 52d85f9211 tests: cover the shutdown drain helpers
Run the helpers over a socket pair with the test playing the peer.
configure checks for socketpair(), and unit.c builds these tests only
when it is found, since custom-IO builds such as the STM32 Cube pack
have no sockets API.

- peer hangs up after the disconnect, or resets before it goes out
- first send would block, the retry flushes it
- peer disconnected first, nothing is sent
- the drain alone sees the hang up, and sends a disconnect of ours a
  short send left queued
- an exit status after the peer's EOF and an IGNORE is still read, and
  the wait returns at once on a session an error already ended
2026-10-13 10:45:39 -05:00
ouki 3a029461c0 Address review comments 2026-10-12 08:58:48 -07:00
ouki 403a48fc43 Add test for retryable SFTP negotiation on non-blocking socket 2026-10-12 08:58:48 -07:00
ouki c8e2eddb36 Fix SendPacketType to not advance state when negotiation fails
SendPacketType() set the state to SFTP_SEND_PACKET even when
wolfSSH_SFTP_negotiate() returned a retry or an error. No request
buffer was built in that case, so an empty buffer was sent and the
caller could wait for a response to a request that was never sent.

Move to SFTP_SEND_PACKET only when the request has been
built, and return the negotiation result otherwise. Keep the state
for retryable errors.

Add the wolfSSH_TestSftpSendPacketType() test hook and an API test
that fails without this change.

F-13904
2026-10-12 08:58:48 -07:00
John Safranek 91cb9aded4 internal: refuse userauth before service request
The server refuses user auth messages until the ssh-userauth service
request is in, per RFC 4252 section 1, and disconnects on them as on
any known id at the wrong time. A successful userauth request can no
longer satisfy the accept loop and draw SERVICE_ACCEPT for a service
the client never requested.

- IsMessageAllowedServer() gates ids 50 to 79 on acceptState reaching
  ACCEPT_CLIENT_USERAUTH_REQUEST_DONE
- regress: the filter refuses USERAUTH_REQUEST and INFO_RESPONSE when
  keyed and admits both after the service request
- regress: a userauth request with no service request disconnects
- regress: a userauth request pipelined behind the service request
  still gets SERVICE_ACCEPT, then its answer

Issue: #1287
2026-10-12 10:43:48 -05:00
John Safranek 21a200150c internal: sign without server-sig-algs
EXT_INFO is optional, RFC 8308 section 2.2. When the server has sent no
server-sig-algs, a publickey USERAUTH_REQUEST now picks the key's
signature algorithm from the client's canned list, and an OpenSSH RSA
certificate follows the same order. A list that names nothing usable
still fails with WS_MATCH_KEY_ALGO_E.

- record in peerSigAlgsSeen that server-sig-algs arrived
- ClientOsshRsaCertSigId() picks the certificate's name and hash
- regress: publickey requests with and without server-sig-algs
- regress: the signature is named for the offered algorithm
- regress: a list superseding a usable one still refuses

Issue: #1286
2026-10-09 17:09:56 -05:00
John Safranek 96c2c14ae1 internal: refuse auth replies before a request
The client filter now refuses every userauth message, the banner
included, until CONNECT_CLIENT_USERAUTH_SENT, so a SUCCESS sent in place
of the SERVICE_ACCEPT no longer completes wolfSSH_connect(). The first
request goes out right after the service accept, RFC 4252 section 5.4.

- regress: test each userauth message in the pre-request states
- regress: drive an early SUCCESS, FAILURE and PK_OK through DoReceive
- regress: a banner bundled behind the service accept still gets
  through wolfSSH_connect()

Issue: #1285
2026-10-09 10:26:28 -05:00
John Safranek 39983c143f tests: send the highwater IGNORE unkeyed
Strict KEX is on by default, and wolfSSH_SendIgnore() refuses an
unkeyed send under it. The HighwaterQuietAfterBadPacket test session
is never keyed, so it turns strict KEX off on its CTX before the
session exists and the final send reaches HighwaterCheck() again.
2026-10-08 15:33:02 -05:00
Yosuke Shimizu 30182b9076 internal, ossh: sign RSA user auth through ssh-agent
- PrepareUserAuthRequestRsa() checks the agent-backed key blob's type
  against the request, decodes it with GetOpenSshPublicKey(), and
  reserves the signature name by sigNameSz.
- BuildUserAuthRequestRsa() sizes the agent's reply room from the
  requested signature algorithm, requests the rsa-sha2-* hash the
  request names, and refuses a signature made with another algorithm.
- GetOpenSshPublicKey() is built under WOLFSSH_AGENT too, decodes an
  OpenSSH RSA certificate, and is documented in internal.h.
- api: run test_GetOpenSshPublicKey_type() in agent builds, with
  truncated OpenSSH RSA certificate vectors.
- regress: share the agent user auth helpers across key types; the
  mock agent checks the key blob it is asked to sign with. Add tests
  for an RSA key against OpenSSH's and an rsa-sha2-256-only server,
  a certificate against a wolfSSH server, a refusing agent, a
  mismatched blob, and ignored flags and an oversize signature for
  both the key and the certificate.
- openssh-interop.test: part 3 authenticates to sshd with an RSA key
  held by ssh-agent, once per rsa-sha2 algorithm the client was built
  with.
2026-10-08 11:28:25 -07:00
Yosuke Shimizu 48ca2135fb internal, tests: start no highwater rekey after an inbound packet fails
- DoReceive() sets highwaterFlag and msgHighwaterFlag when an
  inbound packet fails decryption, its MAC, or its length or
  alignment check.
- DoPacket() sets both flags when padding_length is below
  MIN_PAD_LENGTH or leaves no room for the message id.
- The highwaterFlag and msgHighwaterFlag comments name that writer.
- The wolfSSH_SendPacket() comment limits its ssh->error claim to the
  flush and says HighwaterCheck() writes nothing there but its
  callback may.
- unit.c adds test_HighwaterQuietAfterBadPacket(): an idle worker
  pass fires the highwater callback, and a bad-MAC worker pass and
  the send after it do not.
- unit.c adds test_DoReceive_RejectsPaddingUnderflow(), and the
  VerifyMacFailure, AeadTagFailure, RejectsShortPadding,
  RejectsMisalignedPacket and WorkerHardRecvErrorOutranksFlush tests
  check both flags are set. VerifyMacFailure clears them per case.
2026-10-08 11:27:50 -07:00
Emma Stensland ec4b478714 internal: derive WOLFSSH_NO_MLDSA when every level is off
With 44, 65 and 87 all off, keygen and key loading still took ML-DSA.
2026-10-08 09:01:42 -07:00
John Safranek d6c5a84a98 tests: sign ECC bad-sig auth with a fresh key
wolfSSL built with WOLFSSL_VALIDATE_ECC_IMPORT, as FIPS is, refuses
to decode Hansel's key with a flipped scalar. Sign with a generated
key on the same curve instead, as the RSA wrong-key test does.

- the test now needs HAVE_ECC_KEY_EXPORT
2026-10-06 10:21:21 -05:00
John Safranek 715f7b02b6 internal: unlock FIPS key for DH keygen
FIPS v5 wc_DhGenerateKeyPair() returns FIPS_PRIVATE_KEY_LOCKED_E
unless private key access is unlocked, so DH and DH-GEX key exchange
failed on both sides. Wrap each keygen call the way wc_DhAgree()
already is.

- KeyAgreeDh_server() and SendKexDhInit()
- wolfSSH_TestSetDhKexKey()
- test_DhGexGroup16KeyAgree() client-side keygen and agree
2026-10-06 10:21:21 -05:00
John Safranek 19bac6d6a8 internal: propagate only WANT_WRITE in DoKexInit
DoKexInit() reports a blocked KEXINIT reply to its caller. Take it from
SendKexInit()'s return, not ssh->error, which holds whatever an earlier
call left there: a WS_WINDOW_FULL from SendChannelData() failed the
peer's rekey, and a WS_WANT_WRITE whose send had finished came back.

- tests/regress.c runs DoKexInit() with WS_SUCCESS, WS_WINDOW_FULL,
  WS_CHAN_RXD and WS_WANT_WRITE left in ssh->error, its reply sent,
  blocked, failed, or not owed, and checks the KEXINIT is consumed.
- The test needs only the server: REGRESS_SERVER_KEY_PATH moves out of
  the KEXDH_REPLY_REGRESS_KEX_ALGO block.

Issue: F-14394
2026-10-01 15:38:17 -05:00
John Safranek 1ddd486132 internal: expect the opening KEX message
After the client's KEXINIT the server left expectMsgId unset until the
client's first KEX message arrived, so any transport ID got through in
that window: a message from another KEX method, or an early NEWKEYS.
Strict KEX allows only the messages the negotiated method expects
(draft-ietf-sshm-strict-kex section 3.2).

- DoKexInit() sets expectMsgId to KEXDH_INIT, or KEXDH_GEX_REQUEST for
  DH-GEX; after a wrong first_kex_packet_follows guess it is set once
  the guessed packet is skipped
- DoPacket() resets peerSeq and ends the initial KEX only on a NEWKEYS
  that installed keys; one that fails is counted like any other packet
- regress: out-of-turn KEX messages ahead of the client's KEXDH_INIT
  end the connection under strict KEX, and a failed NEWKEYS is counted
- regress: the first_kex_packet_follows cross-boundary case expects the
  negotiated KEX's message after the skip
2026-09-30 22:49:25 -05:00
John Safranek 1b9ae35e82 Add strict key exchange, the Terrapin mitigation
wolfSSH negotiates strict key exchange, the mitigation from
draft-miller-sshm-strict-kex for CVE-2023-48795, and turns it on when
the peer asks for it too. Nothing in the initial KEX is authenticated,
so a packet spliced into it shifts the receiver's sequence number, and
that shift is the attack.

- SendKexInit() carries the marker on the initial KEXINIT only, sending
  the draft's unprefixed name and then the -v00@openssh.com name
  deployed OpenSSH uses; Paramiko lets the last kex-strict-* name decide
  and knows only the v00 one, so it goes last
- DoKexInit() matches either spelling and enables the mitigation when
  both sides asked for it; a marker in a rekey KEXINIT is ignored
- DoKexInit() ends the connection when strict KEX is on and the KEXINIT
  was not the peer's first packet
- DoPacket() and SendNewKeys() zero peerSeq and seq at every NEWKEYS,
  the outbound reset landing before SendPendingChannelWindowAdjust()
  can bundle a packet at the old number
- IsMessageAllowed() takes an allow list until the peer's NEWKEYS
  arrives: DISCONNECT, KEXINIT, NEWKEYS and the key exchange range
- a refused message ends the connection with SSH_MSG_DISCONNECT and
  KEY_EXCHANGE_FAILED rather than being dropped
- wolfSSH_CTX_SetStrictKex() opts out per context; wolfSSH_new() copies
  the setting and nothing changes it after, so advertising the marker
  and enforcing it read the same value
- wolfSSH_GetStrictKexNegotiated() reports whether a session has it on
- wolfSSH_SendIgnore() refuses to send while strict KEX is offered and
  the initial KEX is still running, since a strict peer would disconnect
- ID_EXTINFO_{S,C} become ID_EXT_INFO_{S,C}, beside four new pseudo-KEX
  ids for the strict KEX names
- reword WS_MSGID_NOT_ALLOWED_E, which no longer only covers userauth
- cover the negotiation, the gate, the injections and the resets in
  regress.c, and the new calls in api.c
- describe strict KEX and its opt-out in README.md
2026-09-30 22:49:25 -05:00
Yosuke Shimizu 527e019455 internal: set the keying flag once the KEX init reaches the transport
- SendKexInit() takes ssh->txFlushCount before SendPacketFlush() and
  sets WOLFSSH_SELF_IS_KEYING when SendPacketDelivered() reports the
  packet away, in place of setting it before the packet is built.
- PurgePacket() runs on the same decision rather than on the send's
  return.
- SendKexInit() runs HighwaterCheck() itself, after the flag is set,
  in place of taking it from wolfSSH_SendPacket().
- internal.h describes the flag as set once the KEX init is sent or
  queued.
- tests/unit.c covers a KEX init the transport rejects, one it
  refuses with a would-block, and one it takes whole behind a
  highwater callback that fails or reads the keying flag; the first
  two also check whether the packet is left owed.
- HwTestCb() also records wolfSSH_RekeyPending() for the session its
  HwTestCtx names.
- FailHighwater(), s_sendRefusals and RefuseThenResetIoSend() move
  beside the other shared test callbacks and gain WS_MAYBE_UNUSED, so
  the client-side test compiles without the server.
2026-09-24 22:56:41 -07:00
Yosuke Shimizu 84b360ab78 ssh: add a rekey-state accessor
- wolfSSH_RekeyPending() reports whether a key exchange is in flight,
  the first one included, returning 0 for a NULL session.
- the wolfSSH_worker() block in ssh.h names it as the way to ask,
  alongside wolfSSH_OutputPending(), and the prototype notes that the
  flag stays set after a failed exchange, so a loop on it must also
  end on wolfSSH_worker().
- tests/regress.c covers each keying bit alone, both together, and a
  NULL session for both predicates.
- tests/testsuite.c drops its wolfSSH_OutputPending() call, the only
  such export check among the public functions.
2026-09-24 22:56:41 -07:00
Yosuke Shimizu a384999f61 agent, apps, examples: relay whole agent messages
- src/agent.c gains AgentWriteAll(), AgentReadFull(),
  AgentReadMessage() and AgentWriteMessage(), which loop the
  agentIoCb, bound a declared length by the new
  WOLFSSH_AGENT_MAX_MSG_SZ, and reconnect once when a write finds
  the socket dead. wolfSSH_AGENT_Relay() is rebuilt on them.
- New WOLFSSH_API wolfSSH_AGENT_RelayChannel() frames the channel
  side, holding a partial request and an unfinished reply on
  WOLFSSH_AGENT_CTX in two WOLFSSH_BUFFERs whose whole allocation
  is zeroed as each message is consumed. It answers WS_WANT_WRITE,
  WS_WINDOW_FULL or WS_REKEYING while a reply is owed, flushes
  what is left queued below under those last two, and answers
  WS_WANT_WRITE while any of it is still queued.
- readPeer() in apps/wolfssh/wolfssh.c and examples/client/client.c
  calls it, drops its local ato32() and 512-byte buffer, and arms a
  select() write set for the transport's own want-write. While a
  reply is owed it ends the read loop so the retry above select()
  runs, and examples/client/client.c times its select() out at one
  second.
- tests/api.c adds nine wolfSSH_AGENT_Relay() cases and
  tests/regress.c twenty-two for wolfSSH_AGENT_RelayChannel().

Issue: F-10545
2026-09-24 17:24:14 -05:00
John Safranek 93556a3416 tests: build regress.c in both one-sided configs
regress.c compiles in a client-only build with keyboard-interactive
and in a server-only build with SFTP.

- the TestKb*() calls are gated on NO_WOLFSSH_SERVER like their
  definitions
- the #endif lines after TestOct2DecRejectsInvalidNonLeadingDigit()
  close both of its guards, and the attribute test has its own
  WOLFSSH_SFTP block
2026-09-23 20:52:18 -05:00
John Safranek 712f18132e tests: nest the client guard inside the SFTP one
A client-only block in regress.c closed inside #ifdef WOLFSSH_SFTP, so
TestSftpAttributesRoundTrip() built without SFTP and --enable-scp
failed on an unused function. The client block now closes before the
SFTP guard and reopens inside it.
2026-09-23 11:47:04 -05:00
John Safranek 6e49506b60 tests: ungate the EXT_INFO parser cases
EXT_INFO is a transport-generic message, so a server session parses it
too, RFC 8308 section 2.3. These cases call DoExtInfo() directly and
never reach the message filter, so they run in a server-only build as
well as a client-only one, keeping the name-list bounds covered on both
sides.
2026-09-22 17:05:22 -05:00
John Safranek edb7a3f1d7 tests: gate the client cases in regress.c
regress.test now builds and runs in a --disable-client tree. The cases
that drive a client session are compiled only when the client half is,
matching the NO_WOLFSSH_SERVER gates already around the server cases.
Without the client, IsMessageAllowed() denies every message, so those
cases asserted against a session that refuses everything.

- Gate the message-policy run, the disconnect and shutdown families, the
  EXT_INFO and algorithm-negotiation cases, and their driver calls.
- Mark the seven helpers whose only callers are now gated out
  WS_MAYBE_UNUSED rather than move them.
2026-09-22 17:05:22 -05:00
John Safranek 99c0794b41 Fix the client-only build and its test suite
A --disable-server build now links, and make check builds and runs what
does not need a server. wolfSSH_RealPath() is compiled whenever SFTP or
SCP is enabled, because the SFTP client reaches it through
wolfSSH_SFTP_SetDefaultPath(), and the test programs that link
examples/echoserver are built only when the server half is.

- Drop the server term from the wolfSSH_RealPath() guard in ssh.c; the
  block holds that one function.
- Add BUILD_SERVER and BUILD_CLIENT conditionals off the existing
  ENABLED_SERVER and ENABLED_CLIENT.
- Gate api.test on the server, testsuite.test, kex.test, auth.test and
  every script on both halves; unit.test and regress.test stay.
- Gate unit.c's wolfSSH_SFTP_read() tests on the server and share their
  want-read IORecv mock with the client tests below them.
2026-09-22 17:05:22 -05:00
John Safranek c34ac172f7 ssh: add wolfSSH_ChannelIdPeek()
wolfSSH_ChannelIdPeek() reports what is buffered on the channel named
by channelId without consuming it or crediting the window. It carries
wolfSSH_stream_peek()'s contract, except that wolfSSH_stream_peek()
only ever peeks the first channel in the list, and this peeks during a
rekey the way wolfSSH_ChannelIdRead() reads during one.

- a NULL buf asks only for the count, still capped at bufSz
- a drained channel reports WS_EOF, or WS_DISCONNECT once the session
  is gone
- test_ChannelIdPeek() covers the count, the bytes left unconsumed, the
  rekey case, and both drained reports
2026-09-22 14:35:24 -05:00
John Safranek 3b8bed568e tests: split the SFTP server and client argv
wolfSSH_SftpTest() gives the echoserver thread and the SFTP client each
their own argument array. The server thread keeps ser.argv pointing at
its array for as long as it runs, so the two cannot share one.
2026-09-22 14:35:24 -05:00
John Safranek 925ac74be3 tests: check the default algorithm lists
Existing coverage asserts only that the canned KEX, host key, cipher
and MAC lists are non-null and that a session inherits them. Read the
lists, so an inverted guard cannot put SHA-1 or AES-CBC back into the
default proposal unnoticed.

- weak entries are expected only under their opt-in macros
- modern entries are expected present, so an emptied list still fails

Issue: F-13961
2026-09-22 14:09:18 -05:00
John Safranek ea0aa34e41 sftp: write a zero extension count in attributes
SFTP_SetAttributes() sizes and writes only the count for a
WOLFSSH_FILEATRB_EXT block, never the records themselves, so the count
goes out as zero. A caller's atr->extCount would promise records that
do not follow, and the peer's parser reads past the block looking for
them.

- advance idx past the count, so a later field cannot land on it
- wolfSSH_TestSftpSetAttributes() and wolfSSH_TestSftpParseAttributes()
  reach the encoder and parser for testing
- tests/regress.c round-trips the flag combinations

Issue: F-2479
2026-09-22 14:09:18 -05:00
John Safranek 236cc46124 tests: drop the dead descriptor restore results
The done: block in test_SendChannelTerminalRequestNoTty() is only
entered with a descriptor still open on a path that already set an error
code, so the result checks guarding -1494 and -1495 could never be true.
Restore the descriptors and keep the original code.
2026-09-21 17:04:17 -05:00
Emma Stensland 3bec025365 Fix ML-DSA composite logic from code review 2026-09-21 11:11:16 -07:00
John Safranek 07fcea84d7 tests: bound the first forward parse guard too
The helper states that every bound is a subtraction because adding a
packet-derived length to the index wraps, and its own first guard was
still an addition. It could not wrap, since ParseGlobalRequestName()
already bounded the name, but the mixed forms invite the weaker one into
the next guard added here.

Issue: #1246
2026-09-18 00:01:58 +02:00
John Safranek f67b20ffca tests: pin the fixed-port forward reply terms
The reply builder is chosen by !isCancel and a zero requested port, and
the suite exercised neither term on its own. A port-0 cancel now covers
the first, and an explicit port answered by a port-reporting callback
covers the documented case where that return is ignored.

- drop the !isCancel term and the port-0 cancel test fails, finding a
  five byte payload where the bare success is one
- bound the bind-address length with a subtraction in
  ParseGlobalRequestFwdBindPort, where advancing by it wrapped word32
- add AlwaysAllocPortFwdCb, reporting a port for any remote setup

Issue: #1246
2026-09-18 00:01:58 +02:00
John Safranek 24c2139a66 internal: bare success for a fixed-port forward
RFC 4254 7.1 gives a tcpip-forward success a trailing bound-port field
only for a port-0 (dynamic) request. DoGlobalRequestFwd() now sends that
field only when the peer asked the server to allocate a port, and
answers an explicit port with a bare SSH_MSG_REQUEST_SUCCESS, as OpenSSH
and libssh do.

- Key the reply builder off requestedPort, which the port-0 compliance
  check already tracks.
- Check the reply payload length against the requested port in the
  regress global-request helper, which had baked in the trailing field.
- Add ParseGlobalRequestFwdBindPort() to recover that port from the
  request the harness fed in.
- Cover the explicit-port reply.

Issue: #1246
2026-09-18 00:01:58 +02:00
John Safranek 2ec8ad418f tests: run the Ed25519 pubkey test without ECDSA
test_pubkey_auth_ed25519_privonly_hostkey() authenticates with an
Ed25519 user key rather than hansel's ECDSA key, so it covers Ed25519
user auth alongside the host key derive fallback and needs no ECDSA.

- load_key() falls back to the Ed25519 host key when neither RSA nor
  ECDSA is available
- guard run_pubkey_test() on RSA or ECDSA, its only callers
2026-09-17 23:27:22 +02:00
John Safranek 36a2ad30b6 tests: build the Ed25519 cases without ECDSA
The Ed25519 key DER helpers arrive through asn.h, which unit.c includes
only for RSA, and the host key that test_wolfSSH_SetAlgoList() installs
came only in RSA and ECDSA flavors.

- include wolfssl/wolfcrypt/asn_public.h unconditionally in unit.c
- add ./keys/server-key-ed25519.der to api.c as the last host key
  fallback
2026-09-17 23:27:22 +02:00
John Safranek 3a3ad23de3 examples, tests: drop the WOLFSSH_NO_ECC guard
Nothing defines WOLFSSH_NO_ECC, so every "#ifndef WOLFSSH_NO_ECC" block
was always compiled and the sample ECC keys behind them reached their
"#error" with no curve available. Use WOLFSSH_NO_ECDSA, which
wolfssh/internal.h derives.

- echoserver builds load_key() and its two default host keys only when
  RSA or ECDSA is there; the Ed25519 host key still loads
- ECC_PATH moves inside that guard, its only reader
- peerEcc is now read only by load_key(), so mark it used
2026-09-17 23:27:22 +02:00
Yosuke Shimizu d92d1e3367 ssh: report the owed write when the worker leaves output queued
- wolfSSH_shutdown() sets ret to WS_WANT_WRITE whenever the
  wolfSSH_worker() call it makes ends with output pending. The
  sendErr local and the arm that returned wolfSSH_get_error() are
  gone.
- tests/unit.c gains EofDisconnectCb(), which sends a disconnect
  from the channel EOF dispatch, and two tests driving it:
  test_ShutdownOwedWriteAfterEofDisconnect() asserts
  wolfSSH_shutdown() returns WS_WANT_WRITE with output pending and
  WS_SUCCESS with the buffer drained on the retry;
  test_ShutdownHardEofDisconnectSurfacesOnRetry() refuses the
  disconnect's send outright and asserts the retry returns
  WS_SOCKET_ERROR_E.
2026-09-17 09:48:37 -07:00
John Safranek dc03049a13 ssh: add a channel session-granted accessor
wolfSSH_ChannelGetSessionGranted() reports whether a shell, exec or
subsystem request on a channel has been answered CHANNEL_SUCCESS, so an
application can tell a second request from the first without reaching
into WOLFSSH_CHANNEL.

- the flag is still clear for the request a session callback is
  answering, so a set flag is an earlier request's grant
- wolfsshd's session callback reads the grant through it
2026-09-15 21:55:50 -07:00
John Safranek fd4d462510 scp: match the scp command as a whole token
wolfSSH_ChannelCommandIsScp() reports whether a channel's session
command starts an SCP transfer, taking "scp" only as its own token. The
divert in wolfSSH_accept() and an application's exec callback both ask
it, so the two cannot disagree about what the SCP server is handed.

- an exec of "scpbackup foo" runs as an ordinary exec
- a command carrying a NUL within the recorded command size is not an
  SCP command; ParseScpCommand() walks a C string, so a NUL would drop
  whatever follows it
- wolfsshd's session callback asks through the same helper
2026-09-15 21:55:50 -07:00
John Safranek 8460f3f90c internal: log a subsystem request as a subsystem
DoChannelRequestSession() serves the shell, exec and subsystem arms
together, and its debug line labels the string it read. A subsystem
request labels that string "subsystem" rather than "command".

- unit.c: renumber the shell-after-exec failure to follow the codes
  above it
2026-09-15 21:55:50 -07:00
John Safranek 7bb39b63e7 internal: re-find the channel after the callback
The generic channel request callback may free the channel it was handed,
so DoChannelRequest() looks it up again before the type handling reads
it. Gone, the request ends there, and a reply the peer wanted fails on
the missing channel the way one after a typed session callback does.

- ssh.h says the callback may free its channel and what the request does
  from there
- regress.c frees the channel from the callback on each of the three
  answers, and on a pty-req, the type that wrote to the channel outside
  a session request
2026-09-15 15:27:16 -07:00
John Safranek 4477f4744a ssh: add generic request callbacks
wolfSSH_CTX_SetChannelReqAnyCb() and wolfSSH_CTX_SetGlobalReqAnyCb()
register a callback consulted first for a channel or global request,
with the name, the type-specific part, and whether a reply is wanted. A
tri-state answer grants, refuses, or leaves it to the handling already
there, so a policy reaches the types with no hook of their own.

- the name is the one that arrived, since a copy into a buffer truncates
  a long name and ends it at an embedded NUL, and a policy has to answer
  on what the peer sent
- a grant still parses and records what the library needs, so a granted
  session request commits the session, the typed callbacks are not
  consulted, and a granted unknown type is answered CHANNEL_SUCCESS
- a port-0 tcpip-forward skips the callback, since only the forward
  callback can report the port bound and a policy that had bound a
  listener would then have to be refused, per RFC 4254 7.1
- a client refuses tcpip-forward and cancel-tcpip-forward ahead of any
  policy, matched on the name that arrived so it holds in a build with
  no forwarding, where neither name is in the name table
- window-change, exit-status and exit-signal are cleared of a reply
  before the handling runs, so a refusal leaves them unanswered too, per
  RFC 4254 6.7 and 6.10
- regress.c covers the answers, the data delivered, the names that do
  not fit a copy, and which callbacks each answer leaves out
2026-09-15 15:27:16 -07:00
John Safranek 6b954da7f7 tests: cover the application-driven SCP start
The echoserver's -A mode runs an accepted scp command through
wolfSSH_SCP_accept(). Reaching that call's want retry path takes a
non-blocking server, which -N supplies.

- copy to and from an app-driven server in scp.test
- check the entry point's null-session argument
2026-09-15 09:31:16 -07:00
John Safranek 971f2f01e8 echoserver: answer session requests in callbacks
With -A the echoserver drives its own channels: accept() returns at
userauth and the callbacks below start the shell, SFTP or SCP session.
Off by default. The two modes are exclusive, since the callbacks answer
the session requests the accept state machine otherwise answers itself.

- wsShellStartCb() forks the pty, so it is registered in either mode,
  and claims the channel only once there is a shell behind it; a second
  request is refused rather than forking over the running shell
- wsExecStartCb() takes an "scp " command as a transfer and any other
  command as a session, and is registered in either mode, since the
  legacy path has always started a shell for an exec request too
- wsSubsysStartCb() is registered only with -A, as accept() serves sftp
  itself, and guards a NULL command, which a truncated request leaves
  behind
- ssh_worker() drives the session through shellCtx.appFd, claims the
  channel itself when no callback did, and leaves an SFTP or SCP
  handoff through its cleanup so the pty master still closes
- open the agent channel from the select loop, since the peer's
  auth-agent-req lands after accept() has returned, and read the
  listener from the context each pass because it appears mid-loop
- resume a subsystem accept that returns a want, waiting on the socket
  between attempts rather than spinning
- close the accepted socket again, clear fwdFd on EOF or reset, and
  stay in the loop on WS_REKEYING, which the read arm already handles
- key ChildRunning's sig_atomic_t on WOLFSSH_SHELL, the only build
  with the SIGCHLD handler that writes it, so a target whose libc has
  no signal.h still compiles
- ask for echo mode in the keyboard-interactive test, which has no
  account on the host for the shell callback to fork a shell for
2026-09-15 09:31:16 -07:00
John Safranek 65802d438c internal: commit a session only once accepted
A shell, exec or subsystem request changes the channel only once the
callback accepts it. The session type and command are set for the
callback to read and put back if it refuses, and CLIENT_DONE follows
acceptance alone, so wolfSSH_accept() stays where it is rather than
reporting a session it answered CHANNEL_FAILURE as established.

- DoChannelRequestSession() carries the three arms, which differed only
  in the type and the callback consulted
- a refusal puts the type and command back, so a grant an earlier
  request won still stands
- FreeChannelCommand() wipes and releases a command line for both
  ChannelDelete() and the refusal path
- unit.c drives a refused shell, exec and subsystem request through
  DoChannelRequest() and checks nothing was committed, and that a
  refusal after a grant puts the earlier command back whole
- regress.c checks accept() stays at ACCEPT_SERVER_CHANNEL_ACCEPT_SENT
  on a refused shell, and that the sftp gate and both diverts ask for
  the grant alone

Issue: F-8852
2026-09-14 14:48:15 -07:00
Yosuke Shimizu a14e6d0f3c ssh, apps, examples: take the worker's status from its return
- wolfSSH_OutputPending() moves from wolfssh/internal.h to
  wolfssh/ssh.h as a WOLFSSH_API taking a const WOLFSSH*, defined
  in src/ssh.c beside the other public calls.
- wolfSSH_worker() puts the send's code in the return in place of
  an event when the flush fails outright, and gates its flush on
  wolfSSH_OutputPending(). wolfssh/ssh.h states both.
- Both wolfsshd shell loops, both echoservers and
  ReceiveScpMessage() dispatch on wolfSSH_worker()'s return, and
  call wolfSSH_get_error() only to tell a transient failure from a
  terminal one. The POSIX wolfsshd loop sets wantWrite from
  wolfSSH_OutputPending().
- Five worker tests expect the send's code where they expected the
  event, and tests/testsuite.c calls wolfSSH_OutputPending().
2026-09-14 10:40:48 -07:00
Yosuke Shimizu 586b697b18 ssh, internal: always flush the worker's output
- wolfSSH_worker() calls wolfSSH_SendPacket() whenever
  ssh->outputBuffer holds bytes and the session is not
  disconnected. ssh->error keeps the receive's code when the
  receive failed, and the close's when a WS_CHANNEL_CLOSED pass
  hard-failed its flush; WS_REKEYING is withheld on a failed
  flush. Drops the second DoReceive(), the WOLFSSH_TEST_BLOCK
  fork and the separate WS_CHANNEL_CLOSED flush.
- BundlePacket() resets ssh->outputBuffer.length to
  ssh->packetStartIdx when the framing fails. wolfSSH_shutdown()
  reports WS_WANT_WRITE when its close-read leaves output queued,
  and the send's own error in place of it when that send failed.
  SendPacketFlush() records its code in ssh->error on every
  transport failure path, and wolfSSH_TriggerKeyExchange() writes
  it only when SendKexInit() fails.
- portfwd, client and scpclient accept WS_WANT_WRITE from
  wolfSSH_shutdown(); in scpclient the close-message drain runs
  on it.
- wolfssh/ssh.h drops WS_WINDOW_FULL from wolfSSH_worker() and says
  to read the return and wolfSSH_get_error() as independent channels
  on every pass.
- Twenty unit tests and the extended TestWorkerReportsDisconnect
  cover what ret and ssh->error hold after a receive, send, buffer,
  callback or framing failure.
2026-09-14 10:40:48 -07:00
John Safranek 28bf47d2a0 tests: pin the get_fd invalid-socket sentinel
test_wolfSSH_set_fd() compares the NULL return of wolfSSH_get_fd()
against the platform invalid-socket sentinel, the value wolfSSH_new()
initializes rfd/wfd to. The old check only asserted the result was not
WS_SUCCESS, which the previous WS_BAD_ARGUMENT return also satisfied.
2026-09-10 18:03:59 -07:00
John Safranek ae81d4d857 ssh: divert only into a granted session
wolfSSH_accept() hands a session to the built-in SCP or SFTP server
only when the request naming it was answered CHANNEL_SUCCESS. A
callback that refuses one sends CHANNEL_FAILURE, yet sessionType and
command are recorded ahead of that answer and stay set, so the diverts
read a refused session as a served one.

- gate both diverts on channel->sessionGranted, as
  wolfSSH_SFTP_accept() already gates the app-channels path
- the SCP divert tests the channel list itself, having no command
  lookup ahead of it to do that
- cover each divert with a refused request and a granted control
2026-09-10 17:14:23 -07:00
John Safranek 0a9b44a962 internal: scrub the channel command on free
The peer's exec or subsystem command line is wiped ahead of both frees
that release it, ChannelDelete() and the GetStringAlloc() that replaces
it on a repeat request, the way ChannelDelete() already wipes the
decrypted inputBuffer just above. A command line can carry a password
or a token among its arguments.

- ScrubChannelCommand() leaves the free to GetStringAlloc(), so a parse
  that fails behind it holds no dangling pointer
- cover both wipes with the retain-on-free allocator, the replacement
  through wolfSSH_TestDoChannelRequest()
- release the test's hand-built channel on a setup failure

Issue: F-8850
2026-09-10 17:14:23 -07:00
John Safranek ff1f82f547 internal: act on a session request only if parsed
DoChannelRequest() records the session type and asks the exec and
subsystem callbacks whether to grant a session only when the command
string parsed. A failed parse is refused on ret alone, and
channel->command still holds an earlier request's value rather than the
one being answered.

- cover a command length header running past the end of the packet, on
  exec and on subsystem

Issue: F-11674
2026-09-10 17:14:23 -07:00
John Safranek 9c8caea8b5 ssh: expose the session command length
An application vetting an exec or subsystem request in its channel
request callback is handed the command as a C string, which stops at an
embedded NUL. wolfSSH_ChannelGetSessionCommandSz() and
wolfSSH_GetSessionCommandSz() report the parsed wire length, so a
callback can match a name whole the way DoChannelRequest() does.

- both accessors report 0 for a NULL channel or session
- wolfSSH_GetSessionCommand() defers to the channel accessor
- the sftp divert in wolfSSH_accept() asks the accessor for the length
- correct the trace name in wolfSSH_ChannelGetSessionCommand()
- cover a callback seeing "sftp\0evil" through exec and subsystem
2026-09-10 17:14:23 -07:00
John Safranek 44a348f01a tests: check the rejecting callback actually ran
CheckSftpAcceptRefusesUngranted() drives the same refusal two ways, a
registered subsystem callback saying no and app channels standing in
for a missing one, and asserted nothing that told them apart. Assert
the call count each case expects.
2026-09-10 17:14:23 -07:00
John Safranek 3516fb00a0 ssh, sftp: match the sftp subsystem name exactly
The built-in SFTP server takes a session only when the subsystem name
is sftp, matched whole. DoChannelRequest() keeps the parsed length in
channel->commandSz, so neither wolfSSH_SFTP_accept()'s grant gate nor
wolfSSH_accept()'s divert serves "sftpx" or "sftp\0evil".

- cover a granted name longer than sftp, one of its length, and one
  running past an embedded NUL
- cover the divert with those three names and a control that diverts
- exec keeps its command length too

Issue: F-11665
2026-09-10 17:14:23 -07:00
John Safranek 7adf524f6f sftp: require a granted subsystem to serve
wolfSSH_SFTP_accept() serves an application-driven session only on a
channel whose subsystem request was answered CHANNEL_SUCCESS.
DoChannelRequest() records the session type and command before it
decides, and leaves both set on a refusal, so they cannot say by
themselves whether anything was granted.

- add channel->sessionGranted, set from the answer a shell, exec or
  subsystem request gets rather than from the request arriving
- look the channel up again before recording it: a callback may close
  its own channel, and wolfSSH_ChannelFree() frees it
- log a request's strings where they are known good: once the parse
  has succeeded, and ahead of a callback that may free the channel
- gate the app-channels path on that flag alongside the session type
  and the command
- cover a refusal from both sides, no callback registered and a
  callback that rejects, and a callback that frees its channel
2026-09-10 17:14:23 -07:00
John Safranek 742f327ed4 sftp: serve app-channels only on a granted sftp
In application-driven mode wolfSSH_accept() parks at userauth, so the
sftp test its divert applies never runs. wolfSSH_SFTP_accept() applies
it itself: the session channel must be a subsystem the application's
callback granted sftp on, or the call returns WS_INVALID_STATE_E and
leaves the wire alone without recording an error.

- gate the app-channels branch on wolfSSH_GetSessionType() and
  wolfSSH_GetSessionCommand(), the same test accept() makes
- ask for that grant in every accept state: below the user-auth stop
  accept() returns with no channel open, and past the stop there is no
  accept() left that could have checked anything
- say in ssh.h that the mode serves SFTP through that grant and never
  reaches the SCP entry point
- regress.c refuses the call with no channel, ahead of accept(), on a
  granted shell and on an established one, and serves an INIT on a
  granted sftp subsystem
2026-09-10 17:14:23 -07:00
John Safranek 4ac4484afd ssh: correct what a late app-channels enable does
DoChannelRequest() reads ssh->appChannels when the request arrives, so
turning the mode on after accept() established the session still refuses
an uncallbacked shell, exec or subsystem request from then on. Only
accept()'s stopping point is pinned, by the guard around stopState.

- say the flag reaches the requests that follow, and that what it cannot
  do is move where accept() returns
- drive a shell request over the wire in both modes from the late-enable
  test, pinning the behaviour the header now describes
2026-09-10 17:14:23 -07:00
John Safranek 2c832010d2 tests: cover application-driven channels
wolfSSH_SetAppChannels() changes where wolfSSH_accept() stops and what
becomes of a session request with no callback behind it, so both modes
are exercised.

- regress.c drives a server with the pivot on, one with a shell
  callback and one without, and checks accept() stops at
  ACCEPT_SERVER_USERAUTH_SENT
- regress.c pins the context setter, the session's inheritance of it,
  and that turning it on after accept() established the session still
  returns
- regress.c re-enters a parked accept() with output still queued, which
  is the one path that flushes before reading the state, and pins that
  it leaves the state on the stop
- unit.c checks DoChannelRequest() refuses a shell, exec and subsystem
  request with no callback once the pivot is on
2026-09-10 17:14:23 -07:00
Yosuke Shimizu c039486e52 scp: return directory entries from the Zephyr entry walk
- The WOLFSSH_ZEPHYR branch of FindNextDirEntry() loops while the
  entry name is "." or "..", matching the POSIX and Windows
  branches, in place of while (1).
- tests/api.c gains test_wolfSSH_SCP_SendRecursiveEntry(), staging a
  directory holding one file and driving three
  WOLFSSH_SCP_RECURSIVE_REQUEST calls through wsScpSendCallback(),
  checking the entry name, size, and bytes placed in buf.  dirPath
  is rooted at CONFIG_WOLFSSH_SFTP_DEFAULT_DIR under WOLFSSH_ZEPHYR
  and at "./scp_recur_entry" otherwise.
- The test is gated on WOLFSSH_SCP, with WOLFSSH_SCP_USER_CALLBACKS,
  NO_FILESYSTEM and NO_WOLFSSH_DIR unset, carries an empty stub
  otherwise, and is called from wolfSSH_ApiTest().
- scpStageRecurFile() writes that fixture file.

Issue: F-13315
2026-09-10 15:28:17 -07:00
Yosuke Shimizu d3ad9fa5ed internal: gate auth-agent channel opens on the client's request
- DoChannelOpen refuses an auth-agent open on a server endpoint, and on
  a client with the agent disabled or connectState below
  CONNECT_CLIENT_CHANNEL_AGENT_REQUEST_SENT.
- Both answer OPEN_ADMINISTRATIVELY_PROHIBITED; the ssh->agent check
  stays as the resource check behind them.
- Cover the refusals before the request, with the agent disabled, over
  an accepting channelOpenCb, and on a server, plus the accepted open
  past the request; each refusal asserts ssh->error stays clean.
- Move TestAgentChannelNullAgentSendsOpenFail to a client harness, and
  have the agent tests set the agent flag explicitly.

Issue: F-13389
2026-09-10 15:28:04 -07:00
Hideki Miyazaki 343582a61e tests: Key regress.c's Windows portability macros on _WIN32
TEST_NULL_DEVICE, TEST_SETENV, TEST_UNSETENV, and TEST_MKDIR were
gated on USE_WINDOWS_API, while the arpa/inet.h/direct.h include
guard a few lines above keys on _WIN32. Whether mkdir(), setenv(),
and /dev/null work as expected is a libc availability question, not
a wolfSSH API-selection one, so _WIN32 is the more direct fit and
now both guards in this file agree.

No behavior change: USE_WINDOWS_API is only ever set when _WIN32 is
already defined, so this does not change which branch any current
build takes.

Verified with the MinGW cross compiler both with WOLFSSH_SFTP defined
and without, and confirmed a plain Linux build still passes
tests/regress.test and tests/unit.test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 82032bddfe tests: Stop TestKnownHostsLastEntry depending on WMKDIR
A local run with ./configure --enable-smallstack (no --enable-sftp)
showed WMKDIR as an implicit, undeclared function under MinGW.
wolfssh/port.h only defines WMKDIR when WOLFSSH_SFTP, WOLFSSH_SCP, or
WOLFSSH_SSHD is enabled, but TestKnownHostsLastEntry itself is gated
on WOLFSSL_BASE64_ENCODE alone, so it compiles in configurations
where none of those three are on and WMKDIR does not exist.

Add a small TEST_MKDIR macro next to the existing TEST_SETENV and
TEST_UNSETENV ones, backed directly by _mkdir() under USE_WINDOWS_API
and mkdir() otherwise, and use it in place of WMKDIR. The Windows
branch needs direct.h for _mkdir's declaration; include it next to
the existing _WIN32 guard around arpa/inet.h at the top of the file.

Verified with the MinGW cross compiler both with WOLFSSH_SFTP defined
and without, and confirmed a plain Linux build configured with
--enable-smallstack (no --enable-sftp) still passes tests/regress.test
and tests/unit.test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 53683a758c tests: Use the Windows null device in the known_hosts regression test
The next mingw-regress failure after the previous fix was
TestKnownHostsLastEntry failing its "ready" assertion. It calls
open("/dev/null", O_RDONLY) to point stdin at EOF while it drives the
known_hosts prompt, but Windows has no /dev/null; the call simply
fails there, so open() returns -1 and every check gated on ready is
skipped.

Add a TEST_NULL_DEVICE macro that resolves to "NUL" under
USE_WINDOWS_API and "/dev/null" otherwise, and use it at both call
sites in this file: TestKnownHostsLastEntry, and the same pattern in
TestPasswordEofNoCrash a bit earlier, which happens to be masked in
CI by its own isatty() guard but would hit the same bug if ever run
against a real terminal on Windows.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 6d163a87d9 Fix remaining MinGW build errors in the regress/unit test build
apps/wolfssh/common.c declared a CONSOLE_SCREEN_BUFFER_INFO local in
ClientSetEcho that nothing ever read; the Windows echo toggling never
grew the code that would have used it. Drop the unused declaration.

wolfssh/port.h defined WSTRSEP as strsep(), a BSD extension MSVCRT
and MinGW do not provide. Add a portable wstrsep() in src/port.c,
matching the wstrnstr/wstrncat/wstrdup pattern already used for other
missing string functions, and route WSTRSEP through it under
USE_WINDOWS_API.

tests/regress.c called the two argument POSIX mkdir(path, mode) and
setenv()/unsetenv() directly in TestKnownHostsLastEntry. Use the
existing WMKDIR macro for the directory creation, and add small
TEST_SETENV/TEST_UNSETENV macros backed by _putenv_s() on Windows so
the HOME juggling this test does still works there.

src/wolfsftp.c had three separate issues in code paths that had never
been compiled before this job existed. wolfSSH_SFTP_RecvOpen declared
a flagsAndAttrs DWORD that nothing read, since WS_CreateFileA is
called with a hardcoded FILE_ATTRIBUTE_NORMAL instead.
wolfSSH_SFTP_RecvOpenDir compared a signed loop counter against a
sizeof expression while building ssh->driveList, so make the counter
word32. wolfSSH_SFTP_Put passed &state->rSz, an int, to ReadFile()'s
DWORD* output parameter; read into a local DWORD and copy it into
state->rSz afterward, since that field is also assigned from
WFREAD() on non-Windows builds.

src/wolfterm.c's wolfSSH_DoOSC never used its handle parameter. Mark
it with WOLFSSH_UNUSED rather than removing it, since the parameter
matches the signature its two call sites already pass and future OSC
handling such as window titles is a natural use for it.

Verified against a real x86_64-w64-mingw32 cross compiler with a
config.h edited to match the sizes and header availability the
actual Windows CI run reported (SIZEOF_LONG 4, HAVE_SYS_IOCTL_H
undefined, and so on): every file this job compiles builds cleanly
under the same -Werror flag set. Also reconfirmed a clean, unmodified
Linux build still passes both tests/regress.test and tests/unit.test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 83a2246471 Fix three MinGW -Werror build failures in the regress/unit test build
MinGW-w64 does not ship arpa/inet.h; guard tests/regress.c's include
the same way apps/wolfsshd/auth.c already does, since htonl/ntohl end
up declared via the winsock2.h wolfSSL's headers pull in later in the
same translation unit.

wolfssh/test.h guarded its MSVC #pragma warning(disable:4996) with
USE_WINDOWS_API alone, which is also true for MinGW's gcc; gcc treats
the unrecognized pragma as an error under -Werror. Require _MSC_VER
too, matching the existing ALIGN16 pragma guard in wolfssh/internal.h.

wolfSSH_CleanPath's Windows/Nucleus drive-letter cleanup re-declared
`i` in a nested scope, shadowing the function's own `i` used by every
other loop in the function. Hoist `j` to the function's declarations
(guarded by the same #if so non-Windows/Nucleus builds don't get an
unused-variable warning) and drop the now-unnecessary block so the
loop reuses the outer `i`.

Verified locally: autoreconf + configure + make tests/regress.test
tests/unit.test builds clean and both binaries pass on Linux.
2026-09-10 15:27:50 -07:00
Aidan Keefe 4553b3c659 Skoll review and Github review
Added more test vectors as well as locking in behavior of VerifyProtoId
Added round trip test for proto id
Comment fixes
New define for proto id min size
2026-09-10 15:27:30 -07:00
Aidan Keefe 155a4e1c47 Validate a custom SSH proto ID string
wolfSSH_CTX_SetSshProtoIdStr() now rejects a string that is not
CRLF-terminated, exceeds 255 bytes counting the terminator, or
carries a CR or LF in the body. DoKexInit() subtracts the two
terminator bytes from the length when hashing it, so an
unterminated string underflowed the hash length.

Issue: F-10571

partial progress
2026-09-10 15:27:30 -07:00
John Safranek e7a05229ad agent: commit the open on a completed flush
wolfSSH_AGENT_ChannelOpen() settles its channel and agent on whether
the open reached the peer, which SendPacketDelivered() answers from the
flush count. The highwater callback runs after the last byte is out, so
its failure arrives as this send's return, and the rollback read that
as a send that never left.

- give SendPacketDelivered() external linkage, contract in internal.h
- note in agent.h that an error raised once the open is on the wire
  leaves the channel open
- test a failing highwater callback: the channel and the agent stand,
  and the poll after it is the idempotent one
2026-09-08 11:50:40 -05:00
John Safranek 6164ec39d5 agent: refuse a channel open after a disconnect
wolfSSH_AGENT_ChannelOpen() answers a poll on a session that is over
with WS_FATAL_ERROR and WS_DISCONNECT in ssh->error, the shape every
other public sender uses: no channel opened, nothing on the wire,
RFC 4253 section 11.1. wolfSSH_accept() gates the open it drives, so
the new public entry point is the only way in.

- promote SendAfterDisconnect() to WOLFSSH_LOCAL so agent.c uses the
  same helper as every other public sender
- leave an open queued before the disconnect unflushed, the rule
  wolfSSH_shutdown() applies to all but its own disconnect
- keep WS_DISCONNECT in ssh->error at the accept() call site, which
  used to overwrite it with the status the open returns
2026-09-08 11:50:40 -05:00
John Safranek 41f171d982 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
2026-09-08 11:50:40 -05:00
JacobBarthelmeh 161c5ae24f expand CI test for non-fpki build, fix check for error state with CertEnumCertificatesInStore, add extended key usage type check if present, fix for EKU checks with X509 use and add test case 2026-09-05 01:32:02 -07:00
JacobBarthelmeh ed9bf644d0 use proper free for cert store version, fix echoserver on mac os 2026-09-05 01:32:02 -07:00
JacobBarthelmeh 4baba585d5 fix for flags handling, ocsp case, macro guards, unused variable, changed default from MY to required to be set
enable SHA1 with windows cert store test case

expand test cases, adjust to authorized key file, minor dead code adjustments

add more documentation, refactor duplicate code sections, clean up test cases, more adjustments to logging spamming protections
2026-09-05 01:32:02 -07:00
JacobBarthelmeh 13c2adf34b add Windows cert store use with signing and add example arguments
add Windows cert store test case

make windows cert feature default disabled and simplify macro guard

additional unit tests, advertise x509 and pubkey, use CN to match username, build check for WOLFSSL_SYS_CA_CERTS, fix for CM ref count

additional build test, uniform enum name, fail on unkown cert store ecc curve, tie in of loading whole cert store for sys CA's
2026-09-05 01:32:02 -07:00
John Safranek fb15a05ed0 tests: cover the forward cleanup callback
WOLFSSH_FWD_LOCAL_CLEANUP now fires, and what must not fire matters as
much as what must: the gate is the channel's own setup record, not its
type.

- a peer-opened direct-tcpip channel reports SETUP, CHANNEL_ID, then
  CLEANUP when it closes
- a forwarding channel this side opened reports no cleanup, and a
  session channel no forwarding action at all
- an open that fails after a successful setup still reports the cleanup,
  and a setup that reported failure draws none
- freeing the session, or the channel with wolfSSH_ChannelFree(),
  reports it, and the session free after a channel free does not
  report it again
2026-09-03 21:43:30 -05:00
John Safranek 6485062d3d internal: send LOCAL_CLEANUP on forward delete
WOLFSSH_FWD_LOCAL_CLEANUP is part of the public WS_FwdCbAction contract
and the library never sent it, so an application's handler never ran and
every peer-opened forward leaked what its setup callback allocated.
ChannelDelete() now sends it, so a peer close, an open refused after the
setup ran, wolfSSH_ChannelFree(), and freeing the session all report it.

- record the setup in a new fwdSetupTxd bit on WOLFSSH_CHANNEL: a
  locally opened forward gets no LOCAL_SETUP, so the channel type alone
  cannot say whether the application holds anything
- clear the bit when the cleanup goes out, so a channel reports it once
- pass the channel's id in the port parameter, the way
  WOLFSSH_FWD_CHANNEL_ID does, so an application with two forwards can
  tell which one ended
- TestDirectTcpipFwdCbRejectsChannelId now counts three callback calls
2026-09-03 21:43:30 -05:00
John Safranek ee7fa04bec ssh: flush any queued output at teardown
wolfSSH_shutdown() flushes whatever a short send left in the output
buffer, not just a queued disconnect. A rejected auth's
USERAUTH_FAILURE has no channel, and DoChannelClose() retires the
channel as soon as the close is bundled, so neither had anything
left to carry the retry.

- The gate still refuses a flush once the peer has disconnected,
  unless our own disconnect is the thing queued.
- A flush that finishes on a live session clears the WS_WANT_WRITE
  the short send latched, as the disconnected path already did.
- ssh.h documents the widened flush and the WS_WANT_WRITE a short
  one leaves wolfSSH_shutdown() returning.
- Tests cover the rejected auth with no channel and the close whose
  channel DoChannelClose() already retired.
- A flush that short-sends on a live channel leaves the teardown
  queued behind it. The retry adds no second EOF or close, and skips
  the close wait while its own flush is still short.
- A teardown send that drained the leftovers settles the flush; one
  that failed outranks it, since a reset leaves the buffer intact.
2026-09-03 16:12:17 -05:00
John Safranek 54610fd36e tests: cover the exec and subsystem req callbacks
Only the shell hook had coverage, so nothing held DoChannelRequest() to
handing the command and session type to the exec and subsystem callbacks
or to answering with what they return.

- assert each callback sees the session type and the command string the
  request carried, and the ctx set on the session
- assert an accepting callback draws CHANNEL_SUCCESS and a rejecting one
  CHANNEL_FAILURE, whether it rejects with a WS_ error or a bare nonzero
2026-09-03 15:31:58 -05:00
John Safranek 3c6c1e85d8 tests: cover the channel close callback
wolfSSH_CTX_SetChannelCloseCb() had no caller in tests/, examples/ or
apps/, so nothing held DoChannelClose() to running the hook before it
retires the channel.

- assert the callback runs with the closing channel's id and the ctx
  set on the session
- assert the channel is still on the list inside the callback and gone
  by the time the caller is told
- a rejecting callback changes nothing: the return is discarded and the
  close completes
2026-09-03 15:31:58 -05:00
John Safranek 6804e999e1 tests: cover the channel open response callbacks
The confirmation and failure hooks reach an application only through
DoChannelOpenConf() and DoChannelOpenFail(), and nothing in tests/ or
apps/ registered either, so both arms shipped unexercised.

- assert the confirm callback runs with the peer's channel id, window
  and packet size already recorded
- assert the failure callback runs while the channel is still findable,
  before DoChannelOpenFail() removes it
- count each hook separately, so a test can tell which one ran
- a rejecting confirm hook fails the receive and leaves the open
  unfinished; a rejecting failure hook fails it and leaves the channel
  on the list
- seed the unconfirmed channel through ChannelNew() and ChannelAppend(),
  the state an outstanding open leaves behind
2026-09-03 15:31:58 -05:00
John Safranek 79456c2002 Refuse a session channel open on a client
RFC 4254 6.1 has a session open travelling client-to-server, so
DoChannelOpen refuses one on a client endpoint in the type switch,
ahead of ChannelNew and any channelOpenCb. OpenSSH and Dropbear
refuse it outright too, with no application opt-in.

- Answer OPEN_ADMINISTRATIVELY_PROHIBITED, as the forwarded-tcpip
  direction gate below it does.
- Widen the client harness guard in regress.c so it builds without
  WOLFSSH_FWD, and hoist AcceptChannelOpenCb out of the forwarding
  block.
- Cover the refusal, and the gate outranking an accepting
  channelOpenCb.

Issue: F-8836
2026-09-03 10:40:16 -05:00
John Safranek b13704af1d Cover and document the client KEX role check
TestClientOnlyKexMsgsBlocked now asserts that 31 and 33, the ids a
client does receive, stay allowed where the handshake expects them, so
widening the role check into a 30-34 range fails the suite. The
IsMessageAllowed() comment records the receive-only policy and the
per-KEX-method id namespace.

- assert id 33 is allowed once expectMsgId is MSGID_KEXDH_GEX_REPLY
- assert expectMsgId starts at MSGID_NONE, so the role check is what
  rejects the blocked ids
- recheck all three blocked ids during a rekey on an established session
2026-09-03 10:32:18 -05:00
Aidan Keefe 557f3df42d Validate a custom SSH proto ID string
wolfSSH_CTX_SetSshProtoIdStr() now rejects a string that is not
CRLF-terminated, exceeds 255 bytes counting the terminator, or
carries a CR or LF in the body. DoKexInit() subtracts the two
terminator bytes from the length when hashing it, so an
unterminated string underflowed the hash length.

Issue: F-10571
2026-09-02 15:36:18 -07:00
Emma Stensland ea530fe05d wolfsftp: Improve POSIX and Windows write/append handling
Ensures EOF appending on all POSIX systems and fails short writes on Windows.
Consolidates the Windows SFTP flag matrix testing with shared helpers.
2026-09-02 15:35:09 -07:00
Emma Stensland 6a71292401 internal: Refactor and gate ML-DSA composite key handling
Gates ML-DSA composites behind WOLFSSH_NO_MLDSA_COMPOSITES.
Deduplicates key handling and uses heap allocation for
composite buffers when compiling for small stacks.
2026-09-02 15:35:09 -07:00
John Safranek 5e72c76683 Rebind a request in flight to the folded forward
FwdRemoteSettle() folds a port-0 forward the peer bound onto a
registration already standing at that port. A request still in its
send window resolved its forward before the send, so it names the
survivor from here on, the same as the queued replies do.

- Add FwdPendingRebind() and call it ahead of the unlink, so a cancel
  whose send pumped in the answer commits naming a forward rather
  than nothing.
- Regression test drives the port-0 answer in from the cancel's send
  callback.
2026-09-02 15:18:32 -05:00
John Safranek cb211506e2 Fold a forward the send settled onto its bind
FwdPendingCommit() looks for a registration of the bind its entry names
before linking it. An answer pumped in from the send can settle a
port-pending forward onto that port, and the fold in FwdRemoteSettle()
runs before the committing entry joins the list.

- replies queued against the folded entry move to the one that stands
  at the bind
- test drives that answer from the IO send callback; the bind takes two
  registrations without the fold, and one cancel leaves one matching
2026-09-02 15:18:32 -05:00
John Safranek e53c157049 Resolve a forward request once, before its send
A request looked its registration up again at commit, after the send had
run the application's send and highwater callbacks. A callback that
reenters the library sends behind the request that ran it, so the lookup
found what the callback did rather than what the request asked for: a
cancel from a first setup's send found nothing to take, and that setup's
commit then registered the forward the peer had just been told to drop.

- Requests in flight hang off ssh->fwdPendingHead for the length of
  their send, so a reentrant request resolves to the forward the request
  it interrupted is registering, not to nothing.
- FwdRemotePrepare() settles what the request names and holds it on the
  pending; FwdPendingCommit() uses that instead of a fresh lookup, which
  also drops the dedupe branch the double lookup needed.
- FwdRemoteUnlink() voids the pending pointers along with the queued
  slots, so freeing a forward mid-send leaves no commit naming it, and
  FwdPendingDiscard() goes through it rather than freeing raw.
- FwdRemoteMatch() reads the pending list under the rules the scan over
  registered forwards uses: a setup mid-send is on the wire and can feed
  channels; a cancel behind it or a refusal on its slot stops matching.
- Drop pend->bindAddr and pend->bindPort, which nothing reads now.
- Tests drive a cancel, a setup, an inbound forwarded-tcpip open and a
  mid-send REQUEST_FAILURE from the IO send callback, the window the
  highwater tests no longer reach.

Issue: ZD-22195
2026-09-02 15:18:32 -05:00
John Safranek 79a7f29933 Match forwarded-tcpip on every client session
The match only ran once wolfSSH_FwdRemoteSetup() had registered
something, so a client that never called it had every forwarded-tcpip
open go unchecked -- the case RFC 4254 7.2 is clearest about, since a
session that asked for no forward has nothing an open could answer for.

- Refuse the open whenever it names no registration, empty list
  included. wolfSSH_SetFwdRemoteMatch(WOLFSSH_FWD_MATCH_OFF) is the
  opt-out for an application that keeps its own bind list.
- Drop fwdRemoteTracked, which nothing reads now.
- wolfSSH_global_request() cannot carry the bind address and port RFC
  4254 7.1 puts after the want-reply boolean, so no request that
  registers a forward can be framed outside wolfSSH_FwdRemoteSetup().

Issue: ZD-22195
2026-09-02 15:18:32 -05:00
Reda Chouk 6802df0c52 Reject the KEX requests a client never receives
This is just a follow-up PR to (#1221), that mirrors pretty much the same changes for the client side too.
2026-09-02 11:20:06 -07:00
Hideki Miyazaki 88c0e34b7e Addressed review comments 2026-09-02 09:17:30 -07:00
Hideki Miyazaki 1c5fbfe0b5 Addressed review comments 2026-09-02 09:17:30 -07:00