Add restricted wolfSSH ordering fix

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.
This commit is contained in:
2026-09-16 14:04:34 +02:00
parent 4d3bb490c9
commit bea33e1c95
28 changed files with 4653 additions and 64 deletions
+155
View File
@@ -0,0 +1,155 @@
# 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
`IsMessageAllowedKeying` implementation 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: `SendChannelEof` now checks before lookup,
serialization or `eofTxd` mutation. The extra `SendChannelExit` guard is retained
intentionally as local hardening, using `MSGID_CHANNEL_REQUEST` (exit-status),
not falsely attributed to PR793's EOF hunk. Real shutdown/rekey tests cover both.
- **PR819:** retain `extInfoSent` ABI 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 client `DoKexDhReply` hunk to 1.4.20's
`useEccKyber` spelling; 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 generated `internal.c`: the pinned `log.h` has a different WLOG
implementation, so neither `log.h` nor `log.c` needs an ABI/source change.
- **PR921:** set server `expectMsgId = MSGID_NEWKEYS` before `SendNewKeys`, 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 14 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:
1. Does not append `ext-info-c` to client KEXINIT.
2. Ignores peers' extension willingness; sets `sendExtInfo` to zero on KEXINIT.
3. Removes the post-NEWKEYS extension send and makes `SendExtInfo` return
`WS_NOT_COMPILED` even if called directly.
4. 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.