Apply hash-pinned generated edits for CVE-2025-14942 while keeping wolfSSH 1.4.20 managed sources unchanged. Add the ABI header overlay, provenance records, and real state-machine interoperability contracts.
wolfSSH 1.4.20 restricted ordering correction
Project modification: 2026-09-16. This is a bounded project-profile correction for the CVE-2025-14942 ordering defect, not a complete wolfSSH 1.4.22/1.5.0 backport or general-purpose upstream-fix claim. wolfSSH remains pinned to 1.4.20; wolfSSL and all existing password/parser/crypto protections remain pinned and intact. No managed source is edited.
Inputs and provenance
793.patch, 819.patch, 840.patch, 855.patch, and 921.patch are the exact
upstream mail-patch responses fetched on 2026-09-16. provenance.json records
URLs, byte SHA-256 hashes, and every embedded full commit ID. PR endpoints can
change: the archived bytes and commit identities, not a future PR response,
identify this review. Commit pages are recoverable as
https://github.com/wolfSSL/wolfssh/commit/<commit>.
delta.json is the authoritative executable consolidated delta, not a claim
that all archived hunks apply. Its exact old/new edits are applied before the
existing parser/password edits by tools/security_overrides.py; every anchor
must occur exactly once. It pins:
| Input | Original SHA-256 |
|---|---|
src/internal.c |
81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9 |
src/ssh.c |
a4f479ff87eea0980ec1ebdf2c7dd090da473780181b695a56799cb9611f4366 |
wolfssh/internal.h |
8e417149a68f8a6c0506957adf014b3e6c1727a723536826ce5fb0c9e1f1aba3 |
Original copyright/license notices remain in each generated file, preceded by
project modification/provenance notices. Configuration needs no network,
patch(1), Git, or fuzzy matching. All inputs validate before outputs are written.
Prerequisite disposition
- PR793: retain stream/channel read rekey fencing, stream-send fatal/error
convention, EOF/window-adjust send checks and send/receive direction constants.
Exclude example, SFTP, workflow and upstream-test changes. The intermediate
IsMessageAllowedKeyingimplementation was removed by PR855 itself; it is not resurrected. Local send checks are distinct from receive checks. The follow-up corrects a misapplied EOF hunk:SendChannelEofnow checks before lookup, serialization oreofTxdmutation. The extraSendChannelExitguard is retained intentionally as local hardening, usingMSGID_CHANNEL_REQUEST(exit-status), not falsely attributed to PR793's EOF hunk. Real shutdown/rekey tests cover both. - PR819: retain
extInfoSentABI field, but supersede its negotiation/send logic by disabling EXT_INFO uniformly. The field stays zero; no reset-on-rekey or exactly-once extension continuation is needed. - PR840: initialize keying to zero; maintain independent SELF/PEER bits; reject duplicate peer KEXINIT; respond only if SELF was not already keying; reject peer NEWKEYS while SELF still owes NEWKEYS; clear PEER and release the handshake only after successful incoming key installation.
- PR855: retain
expectMsgId, message IDs/ranges, and expected-reply writes before nonblocking sends. Adapt the clientDoKexDhReplyhunk to 1.4.20'suseEccKyberspelling; no PQ algorithm is enabled. Replace permissive range fallthrough with the explicit restricted gates described below. Upstream test-only entry points/build rules are not imported. Its new log macro is local to generatedinternal.c: the pinnedlog.hhas a different WLOG implementation, so neitherlog.hnorlog.cneeds an ABI/source change. - PR921: set server
expectMsgId = MSGID_NEWKEYSbeforeSendNewKeys, including WANT_WRITE. The former post-NEWKEYS EXT_INFO call is deliberately removed.
Local additions also set the server's expected INIT after valid KEXINIT, retain
that expectation after skipping a wrong optimistic INIT guess, reject absent
expectations instead of accepting arbitrary KEX packets, and only allow client
rekey dispatch after successful/queued KEXINIT with a live handshake. Client
rekey dispatch includes the final CONNECT_DONE state.
Restricted protocol contract
- Negotiate only the existing project KEX profile: Curve25519-SHA256 and ECDH-P256. A caller trying to widen the algorithm list to DH/GEX/PQ gets a negotiation error. Those continuation paths are not represented as supported.
- Receive transport notifications 1–4 without consuming an expected KEX reply. KEXINIT is legal only before the peer has begun this exchange; other KEX messages require a live handshake and an exact nonzero expectation. Acceptance consumes that expectation once; the handler sets the next expectation.
- Reject authentication/service/connection traffic after peer KEXINIT until peer NEWKEYS. When only SELF has initiated rekey, pre-peer-KEXINIT in-flight traffic remains legal according to the authentication phase (RFC4253).
- Server requires keyed service request, then service acceptance before userauth requests, then completed authentication before connection messages. Reject wrong-direction and repeated authentication/service messages. Client rejects premature auth results/channel messages and accepts auth responses only in its request phase. Keyboard-interactive is outside the project profile and incoming INFO_RESPONSE is rejected before dispatch.
- Send-side EOF/window-adjust checks never mutate receive expectations. Existing channel-data rekey fencing is preserved.
NEWKEYS/backpressure and the EXT_INFO choice
SendNewKeys bundles NEWKEYS with the old sending keys, installs the new sending
keys, then calls wolfSSH_SendPacket. WS_WANT_WRITE means that same bundled
packet remains in the bounded output buffer. Clear SELF on SUCCESS or
WANT_WRITE, retain PEER and the expected peer NEWKEYS. The existing
accept/connect/worker flush paths send the remaining bytes; they must not call
SendNewKeys again. A fatal send does not clear SELF. Peer NEWKEYS installs the
receive keys and releases the handshake once; premature/duplicate NEWKEYS fails.
Previously SendKexDhReply called SendExtInfo only on SUCCESS from NEWKEYS. A
partial send skipped it with no continuation. Rather than invent another pending
send state and its handshake-lifetime rules, this profile:
- Does not append
ext-info-cto client KEXINIT. - Ignores peers' extension willingness; sets
sendExtInfoto zero on KEXINIT. - Removes the post-NEWKEYS extension send and makes
SendExtInforeturnWS_NOT_COMPILEDeven if called directly. - Rejects incoming EXT_INFO in every phase, since it was never negotiated.
This follows RFC8308's optional-negotiation model. There is no
server-sig-algs advertisement. The existing KeyAccepted setter remains valid
but no longer produces that extension on the wire. This matters particularly to
RSA-SHA2 discovery: RSA user keys are outside the project's enrolled/advertised
profile, and RSA interoperability is not claimed. Host tests establish that
OpenSSH 10.2p1 still authenticates using Ed25519, P-256 and passwords with both
project KEX algorithms, initial KEX and rekeys, without receiving EXT_INFO.
Other clients and hardware remain validation work.
Header overlay and build ownership
The ABI changes require all consumers to use the generated header, not only
the relocated C file. cmake/security_overrides.cmake installs
security_overrides/wolfssh_include/wolfssh/internal.h with BEFORE PUBLIC include
propagation and a PUBLIC forced include. The original header's include guard
prevents a later vendor-first include path from defining a stale layout. A dated
ABI marker rejects an original header forcibly included before the overlay,
rather than silently skipping the corrected layout. Existing
unmodified wolfSSH files and transitive application consumers therefore use the
same layout. Source properties are retained; header originals, generated header,
and delta.json are configure dependencies. Compiler dependencies track the
forced header too. There are now eight source overrides plus one header.
Tests intentionally put the original include root before the overlay when
compiling full host translation units. The SDK CMake fixture checks PUBLIC
propagation to library, direct and transitive consumers. Forced headers use joined
-include/path arguments: PlatformIO sorts app flags and deduplicates component
flags, which breaks split option/operand pairs. The installed adapter/SCons test
compiles a real Xtensa consumer and reproduces the failure with a split-option
mutation. The crypto guard remains enabled with its existing joined argument.
The obsolete unused EXT_INFO name constant is removed, without relaxing warnings.
Validation and boundaries
Run python3 tests/wolfssh_order_contract/run.py --interop and the related
commands listed in that test's README. Tests execute the actual generated full
internal.c and ssh.c with real wolfCrypt, bounded host IO, hostile message-ID
matrices, partial writes, both roles and all three rekey initiation directions.
Guard-removal mutations must fail. The parser/password suites separately preserve
prior protections; SDK tests exercise real override/overlay CMake wiring.
Follow-up validation on 2026-09-16: authorized pio run PASS in 21.31 seconds,
94,340 B linked RAM / 1,768,701 B flash. Strict auth, protocol, crypto and
SDK override tests against the rebuilt production artifacts PASS. Ordering tests
PASS 8,028 checks and seven rejected mutations; parser tests PASS 3,124 cases in
each stack mode. No upload, erase, dependency upgrade or device operation.
This does not establish stack/heap reserve, timing, arbitrary-algorithm,
strict-KEX-extension, full-client compatibility or phase-wide security sign-off.