# Bounded wolfSSH parser contract Run from the project root (installed pinned sources and a host C compiler required): ```sh 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](../../docs/ssh_parser_remaining_review.md) 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 `DoChannelFailure` now bounds exactly one recipient ID, verifies the local channel, and consumes it before returning the existing fatal `WS_CHANOPEN_FAILED`. No state or channel mutation is introduced. - The server subset of PR902 rejects every name except exact `ssh-userauth` before 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_FWD` disabled. - The follow-up closes `DoChannelRequest` prefix/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:** `DoIgnore` calls `GetSkip`; `GetSkip` uses `GetSize` and accepts an empty string ending exactly at the payload boundary. `DoServiceRequest` validates the full string with `GetSize` before `GetString`, retaining the old strict `< WOLFSSH_MAX_NAMESZ` limit rather than upstream's truncation behavior. Failure leaves the caller index and client state untouched. Successful state transition remains exactly the old one for `ssh-userauth`; the 2026-09-16 addition above rejects other names. The original `GetSize` already uses bounded subtraction and needs no change. `GetString` now uses it and rejects zero output capacity before subtraction/copy; ordinary bounded truncation semantics remain unchanged. - **881 subset:** `DoChannelWindowAdjust` rejects addition exceeding the 32-bit maximum with `WS_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 `DoUserAuthRequestEcc` and `DoUserAuthRequestEd25519` use OR. `GetSize` bounds the incoming span first; unequal lengths short-circuit before `memcmp`, and equal lengths compare exactly the expected span. Valid matching ECDSA/Ed25519 types follow the original crypto path. Existing error normalization remains (`WS_CRYPTO_FAILED` for key parsing, `WS_INVALID_ALGO_ID` for 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 `GetSize` establishes `sz <= 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 return `WS_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](../../docs/ssh_key_validation_review.md), 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.