- Enforce exact service and channel names with bounded failure parsing - Add hash-pinned offline notice assembly and regression coverage - Record advisory dispositions, provenance, integration evidence, and remaining gates
13 KiB
Bounded wolfSSH parser contract
Run from the project root (installed pinned sources and a host C compiler required):
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/run.py
CCACHE_DISABLE=1 python3 tests/wolfssh_parser_contract/review.py --profile
CCACHE_DISABLE=1 python3 tests/wolfssh_auth_contract/run.py --host-only
CCACHE_DISABLE=1 python3 tests/sdk_security_overrides/run.py
No download, PlatformIO, managed-component edit, production build-tree regeneration,
or device operation is performed. The runner verifies the original internal.c
SHA-256, calls the production render_entry, writes and reads back its generated
bytes in a temporary directory, and extracts complete actual functions. C tests
run with guard pages and UBSan trap instrumentation, both with and without
WOLFSSH_SMALL_STACK. The existing SDK suite separately tests generation and
CMake source replacement using fixtures. This is not a claim that an existing
production generated file or firmware binary contains these edits.
2026-09-16 remaining-parser review
The finite applicability review closes PR899's current-profile disposition and corrects the earlier description of PR902: 902 is service-name validation, not channel-callback hardening.
- PR899 client RSA/ECC key-skip hunks remain unapplied: RSA is disabled, and the sole client host-key call chain is blocked by current server KEX ordering.
- The current
DoChannelFailurenow bounds exactly one recipient ID, verifies the local channel, and consumes it before returning the existing fatalWS_CHANOPEN_FAILED. No state or channel mutation is introduced. - The server subset of PR902 rejects every name except exact
ssh-userauthbefore publishing the index/state transition. The owner closes on the error; unlike upstream, no best-effort disconnect packet is queued. Client service acceptance stays unchanged and unreachable in the current server role. - PR918/919 forwarding fixes are not applied with
WOLFSSH_FWDdisabled. - The follow-up closes
DoChannelRequestprefix/NUL aliases: all nine recognized names require exact length before exact byte comparison. Supported branch bodies and unknown-request success/ACK handling remain unchanged. Production enables TERM but not SHELL/AGENT: PTY/exit branches are present, window-change and agent branches absent. Disabled branches are tested separately without enabling firmware features.
pr899.patch, pr902.patch, pr918.patch, pr919.patch and provenance.json
archive bytes fetched on 2026-09-16 and verified equal to the corresponding upstream
commit patches. Tests pin hashes, commits and URLs independently. review.py
reverses only the independently specified new notice, two initial handler deltas
and nine exact channel-name predicates and
requires the whole prior original+ordering+parser generated-source hash. This
fences unchanged client parsers, packet dispatch, crypto callers and request branch bodies without
normalizing away edits. The existing exact original hash and exact-once anchors
remain mandatory. No ordering delta or crypto configuration is changed.
--profile reads the unique saved production compile command, checks whether its
input is exactly the known prior baseline or fresh current source, replays real
Xtensa feature preprocessing, and syntax-checks a temporary fresh source. A prior
input is explicitly reported as not regenerated, never current-build evidence.
Existing strict build-registration suites still reject stale generated bytes.
No network or production build-tree writes occur in either test command.
Earlier upstream evidence and retained implementation scope
Official diffs fetched and inspected on 2026-09-15:
- https://github.com/wolfSSL/wolfssh/pull/892.diff
- https://github.com/wolfSSL/wolfssh/pull/881.diff
- https://github.com/wolfSSL/wolfssh/pull/899.diff
- https://github.com/wolfSSL/wolfssh/pull/880.diff
These are PR URLs, not immutable commit pins. The authoritative local inputs remain
original wolfSSH 1.4.20, SHA-256
81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9,
plus the exact-once checked-in edits in tools/security_overrides.py. No repinning
or wholesale upstream patch application occurs.
Covered:
- 892 subset:
DoIgnorecallsGetSkip;GetSkipusesGetSizeand accepts an empty string ending exactly at the payload boundary.DoServiceRequestvalidates the full string withGetSizebeforeGetString, retaining the old strict< WOLFSSH_MAX_NAMESZlimit rather than upstream's truncation behavior. Failure leaves the caller index and client state untouched. Successful state transition remains exactly the old one forssh-userauth; the 2026-09-16 addition above rejects other names. The originalGetSizealready uses bounded subtraction and needs no change.GetStringnow uses it and rejects zero output capacity before subtraction/copy; ordinary bounded truncation semantics remain unchanged. - 881 subset:
DoChannelWindowAdjustrejects addition exceeding the 32-bit maximum withWS_OVERFLOW_E, leaving the channel window unchanged. The parsed index still advances, as upstream does. No new include is needed for the explicit word32 maximum. Unknown channels and truncated fields stay rejected. - 880 subset: both key/signature type checks in
DoUserAuthRequestEccandDoUserAuthRequestEd25519use OR.GetSizebounds the incoming span first; unequal lengths short-circuit beforememcmp, and equal lengths compare exactly the expected span. Valid matching ECDSA/Ed25519 types follow the original crypto path. Existing error normalization remains (WS_CRYPTO_FAILEDfor key parsing,WS_INVALID_ALGO_IDfor signature type mismatch). - Local signature-framing correction: the ECC r/s parser uses the checked
end of the declared signature sub-blob, not the enclosing field size. The
preceding
GetSizeestablishessz <= signatureSz - i, so calculating that end cannot wrap. Both mpints must exactly consume the sub-blob, and the sub-blob must exactly consume the enclosing signature field. Ed25519 likewise rejects bytes outside its declared signature string before starting message verification. These framing errors returnWS_BUFFER_E. This intentionally rejects previously tolerated malformed trailing bytes; valid SSH signature framing and the crypto calls/digest/message construction are unchanged. This is a local correction verified against the pinned implementation and key-validation review, not a claim that these framing edits came from PR 880.
Reachability evidence: the pinned DoPacket dispatches IGNORE, SERVICE_REQUEST,
and CHANNEL_WINDOW_ADJUST to these handlers. DoUserAuthRequestPublicKey calls
DoUserAuthRequestEcc/DoUserAuthRequestEd25519 for ECDSA/Ed25519 authentication,
both enabled in this server's reviewed profile. Advertisement is not treated as a parser dispatch filter.
Explicitly deferred (not fixed by this slice)
- 892: client
DoServiceAccept, agent key preparation, daemon authentication, Windows terminal changes. Password framing/wiping is the existing local correction, intentionally not replaced with upstream's later formulation. - 899: unused client key skips and Windows port/terminal hunks are deliberately not applied; their current-profile applicability is resolved in the new review, not a claim that the dependency is fixed for clients. CHANNEL_FAILURE is covered by the bounded local adaptation above.
- 880: certificate RSA, agent, daemon, terminal, TPM and SCP changes are not applied. No complete PR-880 closure is claimed.
- Message ordering/state machine (including CVE-2025-14942) has its separate restricted-profile suite. Standalone ECC curve-name/key-blob semantic validation, other parsers, client behavior and broader crypto advisories are outside this slice. ECC point/import validation belongs to the separate crypto-policy owner and is not changed here. Existing account/key authorization, numeric r/s validity and Ed25519 raw signature-size/crypto validity checks remain owned by their existing layers.
Exact channel-request and application-gate tests
channel_request.py extracts complete generated DoChannelRequest, GetBoolean,
GetUint32, GetSize, GetString, GetStringAlloc and GetStringRef functions.
It independently hash-pins/extracts the actual application accept_shell,
reject_channel_request and complete process_handshake, and checks callback
registration and per-slot context wiring. channel_request.c runs those real
bodies with channel/context layouts, routing, allocation and platform doubles.
This is not a task/broker/console integration test.
Five feature profiles run in both stack modes with guard pages and UBSan traps: production TERM-only, no-terminal, SHELL-only, TERM+SHELL and TERM+SHELL+AGENT. Alternative flags affect host fixtures only. The matrix covers all nine names: valid requests; every proper prefix (including empty); suffixes; embedded NULs and same-length wrong bytes at every position; 31–65-byte names; every header, name, boolean and payload truncation; nonzero offsets; oversized/wrapping declared lengths; reply/no-reply; invalid channel and send failure; resize callback absence and failure. Instrumented comparisons assert the read length equals the initialized name length, catching removed short-name guards even inside the accessible stack buffer. Unknown/disabled names must not parse payload fields or invoke callbacks, but retain existing success/ACK/consumption behavior.
Real callback/admission cases verify shell-only routing for both roles; null callback context and absent shell callback; authentication/principal/currentness requirements; rejected exec/subsystem after a prior shell, even with no reply; and optional PTY callback behavior. Malformed exec/subsystem payloads still invoke the existing rejecting callbacks and never become shell admission, matching the unchanged handler bodies.
2,737 cases per stack mode for each TERM-containing profile, 2,735 for each
of the other two profiles. 18 prefix/length-guard mutations and two actual
application admission-gate mutations are rejected, in addition to the existing
11 parser mutations. The independent whole-source reversal checks that only the
nine predicates change within DoChannelRequest, with no branch refactor.
Earlier parser test boundaries
The C matrix exercises zero/truncated/exact/oversized/wrapping lengths, invalid and nonzero offsets, zero-capacity output, copy canaries, window overflow boundary pairs, unknown channels, ECC equal-length mismatches, shorter/longer matching prefixes, empty types and every key/signature truncation. Expected ECC and Ed25519 type bytes end at a protected page, testing unequal-length short-circuit safety. Crypto and channel lookup are doubles; tests establish parser gating, not real signature or point validation. Numeric errors, context/channel layouts and name capacity are host doubles, not production ABI verification.
The follow-up matrix in auth_framing.c covers every truncated ECC sub-blob
boundary with a complete r/s pair still available beyond that boundary, oversized
and wrapping nested lengths, malformed r/s lengths, inner/outer trailing bytes,
and physically guard-page-ended frames. Instrumented ato32 also asserts that
nested length reads cannot use accessible bytes outside the sub-blob. Signature
input and surrounding canaries stay unchanged. Valid 32-byte and sign-padded
33-byte r/s encodings reach conversion with their bytes/lengths intact. Ed25519
covers both labels, all truncations, shortened/oversized/wrapping/trailing
signature strings, exact raw-signature forwarding and unchanged streamed message
bytes. Both paths retain crypto rejection behavior using doubles.
Validation: 3,258 cases per stack mode (both pass with UBSan trap mode), plus
11 guard-removal mutations rejected: ECC nested read bound, inner/outer exact
consumption, Ed25519 key/signature OR checks, Ed25519 exact consumption, service
length/byte equality and channel-failure bounded read/exact end/known recipient.
remaining.c adds 134 counted cases, plus assertions for null arguments and an
unknown channel: all service truncations, prefix/suffix/embedded-NUL names,
nonzero offsets, channel truncation/trailing bytes, and wrapping indices. The
mutation copies exist only in temporary test files; core dumps are disabled for
those intentionally failing runs. These are framing-valid fixtures with crypto
doubles, not independently verified real signatures.
The runner also compares complete password, packet dispatch, public-key dispatch and unused key parsers against the pre-parser generated baseline to fence accidental changes. The independent whole-source hash contract separately preserves the exact ordering and all prior parser changes while allowing only this review's three corrections. The separate auth suite executes its 135 password/control-flow cases, including payload wipe, callback framing and asynchronous pending retention. No whole-library fuzzing, real SSH exchange, firmware compile, hardware timing or security sign-off is implied.