Files
ESP32_Serial_Swiss_Army_Knife/tests/wolfssh_parser_contract/README.md
T
Commander1024 51f835c22f Harden SSH parsing and add notice tooling
- 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
2026-09-16 15:06:38 +02:00

13 KiB
Raw Blame History

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 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:

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, 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; 3165-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.